Files
Todo-Project/frontend/upload.html
Hyungi Ahn 0b967a84fa 🚀 시놀로지 배포 준비 완료
 주요 변경사항:
- 단일 docker-compose.yml로 통합 (로컬/시놀로지 환경 지원)
- 시놀로지 볼륨 매핑 설정 (volume1: 이미지, volume3: 데이터)
- 통합 배포 가이드 및 자동 배포 스크립트 추가
- 완전한 Memos 스타일 워크플로우 구현

🎯 새로운 기능:
- 📝 메모 작성 (upload.html) - 이미지 업로드 지원
- 📥 수신함 (inbox.html) - 메모 편집 및 Todo/보드 변환
-  Todo 목록 (todo-list.html) - 오늘 할 일 관리
- 📋 보드 (board.html) - 프로젝트 관리, 접기/펼치기, 이미지 지원
- 📚 아카이브 (archive.html) - 완료된 보드 보관
- 🔐 초기 설정 화면 - 관리자 계정 생성

🔧 기술적 개선:
- 이미지 업로드/편집 완전 지원
- 반응형 디자인 및 모바일 최적화
- 보드 완료 후 자동 숨김 처리
- 메모 편집 시 제목 필드 제거
- 테스트 로그인 버튼 제거 (프로덕션 준비)
- 과거 코드 정리 (TodoService, CalendarSyncService 등)

📦 배포 관련:
- env.synology.example - 시놀로지 환경 설정 템플릿
- SYNOLOGY_DEPLOYMENT_GUIDE.md - 상세한 배포 가이드
- deploy-synology.sh - 원클릭 자동 배포 스크립트
- Nginx 정적 파일 서빙 및 이미지 프록시 설정

🗑️ 정리된 파일:
- 사용하지 않는 HTML 페이지들 (dashboard, calendar, checklist 등)
- 복잡한 통합 서비스들 (integrations 폴더)
- 중복된 시놀로지 설정 파일들
2025-09-24 09:12:39 +09:00

854 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>새 메모 - Todo Project</title>
<link rel="icon" type="image/x-icon" href="static/icons/favicon.ico">
<link rel="apple-touch-icon" href="static/icons/apple-touch-icon.png">
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#d4af37">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Todo Project">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+KR:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--parchment: #f7f3e9;
--parchment-dark: #f0ead6;
--ink: #2c1810;
--ink-light: #5d4e37;
--sepia: #8b7355;
--gold: #d4af37;
--shadow: rgba(139, 115, 85, 0.2);
}
body {
font-family: 'Noto Serif KR', serif;
background: linear-gradient(135deg, #f7f3e9 0%, #f0ead6 100%);
background-attachment: fixed;
color: var(--ink);
}
.parchment-container {
background: var(--parchment);
background-image:
radial-gradient(circle at 25% 25%, rgba(139, 115, 85, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(139, 115, 85, 0.05) 0%, transparent 50%);
border: 2px solid var(--sepia);
border-radius: 8px;
box-shadow:
0 8px 32px var(--shadow),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
position: relative;
}
.parchment-container::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, var(--gold), var(--sepia), var(--gold));
border-radius: 10px;
z-index: -1;
opacity: 0.3;
}
.memo-textarea {
background: transparent;
border: none;
outline: none;
resize: none;
font-family: 'Noto Serif KR', serif;
font-size: 1.1rem;
line-height: 1.8;
color: var(--ink);
width: 100%;
min-height: 200px;
padding: 2rem;
background-image: repeating-linear-gradient(
transparent,
transparent 1.7rem,
rgba(139, 115, 85, 0.1) 1.7rem,
rgba(139, 115, 85, 0.1) 1.8rem
);
}
.memo-textarea::placeholder {
color: var(--ink-light);
opacity: 0.6;
font-style: italic;
}
.memo-textarea:focus {
background-image: repeating-linear-gradient(
transparent,
transparent 1.7rem,
rgba(139, 115, 85, 0.2) 1.7rem,
rgba(139, 115, 85, 0.2) 1.8rem
);
}
.action-button {
background: linear-gradient(135deg, var(--sepia), var(--ink-light));
color: var(--parchment);
border: 2px solid var(--gold);
border-radius: 25px;
padding: 0.75rem 2rem;
font-family: 'Noto Serif KR', serif;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 4px 15px var(--shadow);
}
.action-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px var(--shadow);
background: linear-gradient(135deg, var(--ink-light), var(--ink));
}
.photo-button {
background: var(--parchment-dark);
border: 2px dashed var(--sepia);
color: var(--ink-light);
border-radius: 12px;
padding: 1rem;
transition: all 0.3s ease;
font-family: 'Noto Serif KR', serif;
}
.photo-button:hover {
background: var(--parchment);
border-color: var(--gold);
color: var(--ink);
transform: translateY(-1px);
}
.mobile-upload {
display: none;
}
@media (max-width: 768px) {
.desktop-upload {
display: none;
}
.mobile-upload {
display: block;
}
.memo-textarea {
min-height: 180px;
padding: 1.5rem;
font-size: 1rem;
}
/* 모바일에서 최근 메모 섹션 간소화 */
.parchment-container {
margin-bottom: 1rem;
}
/* 모바일에서 헤더 패딩 줄이기 */
.header-vintage .max-w-4xl {
padding: 1rem;
}
/* 모바일에서 메인 패딩 줄이기 */
main {
padding: 1rem;
}
/* 모바일에서 키보드 대응을 위한 스타일 */
.memo-textarea:focus {
/* 포커스 시 더 부드러운 전환 */
transition: all 0.3s ease;
}
/* iOS Safari에서 키보드로 인한 뷰포트 변화 대응 */
body {
/* 키보드가 나타날 때 레이아웃 깨짐 방지 */
-webkit-overflow-scrolling: touch;
}
}
.photo-preview {
background: var(--parchment-dark);
border: 1px solid var(--sepia);
border-radius: 8px;
padding: 1rem;
margin-top: 1rem;
}
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.75rem;
}
.photo-item {
position: relative;
aspect-ratio: 1;
border-radius: 6px;
overflow: hidden;
border: 2px solid var(--sepia);
}
.photo-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-remove {
position: absolute;
top: 0.25rem;
right: 0.25rem;
background: var(--ink);
color: var(--parchment);
border: none;
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.75rem;
}
.header-vintage {
background: linear-gradient(135deg, var(--parchment), var(--parchment-dark));
border-bottom: 3px solid var(--gold);
box-shadow: 0 2px 10px var(--shadow);
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.recent-item {
transition: all 0.2s ease;
}
.recent-item:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--shadow);
}
.nav-button {
background-color: var(--sepia);
color: white;
padding: 0.5rem 0.75rem;
border-radius: 6px;
transition: all 0.2s ease;
border: 1px solid var(--sepia);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
font-weight: 500;
text-decoration: none;
min-width: 70px;
}
.nav-button:hover {
background-color: var(--gold);
border-color: var(--gold);
color: var(--ink);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.nav-button.active {
background-color: var(--gold);
border-color: var(--gold);
color: var(--ink);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
</style>
</head>
<body>
<!-- 헤더 -->
<header class="header-vintage">
<div class="max-w-6xl mx-auto px-4 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center">
<h1 class="text-xl font-semibold" style="color: var(--ink);">
<i class="fas fa-feather-alt mr-2" style="color: var(--gold);"></i>
새 메모
</h1>
</div>
<div class="flex space-x-1">
<a href="upload.html" class="nav-button active">
<i class="fas fa-feather-alt mr-1"></i>메모
</a>
<a href="inbox.html" class="nav-button">
<i class="fas fa-inbox mr-1"></i>수신함
</a>
<a href="todo-list.html" class="nav-button">
<i class="fas fa-tasks mr-1"></i>Todo
</a>
<a href="board.html" class="nav-button">
<i class="fas fa-clipboard mr-1"></i>보드
</a>
<a href="archive.html" class="nav-button">
<i class="fas fa-archive mr-1"></i>아카이브
</a>
</div>
</div>
</div>
</header>
<!-- 메인 컨텐츠 -->
<main class="max-w-4xl mx-auto px-4 py-8">
<!-- 최근 업로드 항목 (상단으로 이동) -->
<div class="mb-6">
<div class="parchment-container">
<div class="px-6 py-4">
<h3 class="text-lg font-medium mb-4 text-center" style="color: var(--ink);">
<i class="fas fa-history mr-2" style="color: var(--gold);"></i>
최근 저장된 메모
</h3>
<div id="recentUploads" class="space-y-3">
<!-- 최근 업로드 항목들이 여기에 표시됩니다 -->
</div>
</div>
</div>
</div>
<!-- 메인 입력 폼 -->
<div class="parchment-container">
<form id="uploadForm">
<!-- 사진 버튼들 (상단으로 이동) -->
<div class="px-6 pt-6 pb-4">
<!-- 데스크톱용 -->
<div class="desktop-upload">
<button type="button" onclick="selectFile()" class="photo-button w-full text-center">
<i class="fas fa-images mr-2"></i>
사진 첨부
</button>
<input type="file" id="desktopFileInput" accept="image/*" multiple class="hidden">
</div>
<!-- 모바일용 -->
<div class="mobile-upload">
<div class="grid grid-cols-2 gap-3">
<button type="button" onclick="openCamera()" class="photo-button text-center">
<i class="fas fa-camera mb-2 block text-lg"></i>
<span class="text-sm">사진 촬영</span>
</button>
<button type="button" onclick="openGallery()" class="photo-button text-center">
<i class="fas fa-images mb-2 block text-lg"></i>
<span class="text-sm">갤러리</span>
</button>
</div>
<input type="file" id="cameraInput" accept="image/*" capture="camera" multiple class="hidden">
<input type="file" id="galleryInput" accept="image/*" multiple class="hidden">
</div>
<!-- 사진 미리보기 -->
<div id="photoPreviewGrid" class="hidden mt-4">
<div class="photo-preview">
<div class="flex justify-between items-center mb-3">
<span class="text-sm font-medium" style="color: var(--ink-light);">
첨부된 사진 (<span id="photoCount">0</span>/5)
</span>
<button type="button" onclick="removeAllPhotos()"
class="text-xs" style="color: var(--sepia);">
<i class="fas fa-trash mr-1"></i>모두 삭제
</button>
</div>
<div id="photoGrid" class="photo-grid"></div>
</div>
</div>
</div>
<!-- 메모 입력 영역 (하단으로 이동) -->
<textarea
id="uploadContent"
class="memo-textarea"
placeholder="오늘 있었던 일, 떠오른 생각, 중요한 메모... 무엇이든 자유롭게 적어보세요."
required
autofocus></textarea>
<!-- 저장 버튼 -->
<div class="px-6 pb-6">
<div class="text-center">
<button type="submit" class="action-button">
<i class="fas fa-save mr-2"></i>
메모 저장
</button>
</div>
</div>
</form>
</div>
</main>
<!-- 로딩 오버레이 -->
<div id="loadingOverlay" class="hidden fixed inset-0 flex items-center justify-center z-50" style="background: rgba(44, 24, 16, 0.7);">
<div class="parchment-container p-6 flex items-center space-x-3">
<div class="animate-spin rounded-full h-6 w-6 border-b-2" style="border-color: var(--gold);"></div>
<span style="color: var(--ink); font-family: 'Noto Serif KR', serif;">메모 저장 중...</span>
</div>
</div>
<!-- JavaScript -->
<script src="static/js/api.js"></script>
<script src="static/js/auth.js"></script>
<script src="static/js/image-utils.js"></script>
<script>
let currentPhotos = [];
// 페이지 초기화
document.addEventListener('DOMContentLoaded', () => {
// 인증 확인 (여러 토큰 키 확인)
const token = localStorage.getItem('authToken') || localStorage.getItem('token');
if (!token) {
console.log('토큰이 없습니다. 대시보드로 이동합니다.');
window.location.href = 'index.html';
return;
}
console.log('인증 토큰 확인됨:', token ? '있음' : '없음');
// 이벤트 리스너 설정
setupEventListeners();
// 최근 업로드 항목 로드
loadRecentUploads();
});
function setupEventListeners() {
// 폼 제출
document.getElementById('uploadForm').addEventListener('submit', handleUploadSubmit);
// 파일 입력 이벤트
document.getElementById('desktopFileInput').addEventListener('change', handleFileSelect);
document.getElementById('cameraInput').addEventListener('change', handleFileSelect);
document.getElementById('galleryInput').addEventListener('change', handleFileSelect);
// 드래그 앤 드롭
const uploadArea = document.querySelector('.desktop-upload .border-dashed');
if (uploadArea) {
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('drop', handleDrop);
}
// 모바일 키보드 대응 설정
setupMobileKeyboardHandling();
}
// 모바일 키보드 대응 설정
function setupMobileKeyboardHandling() {
const textarea = document.getElementById('uploadContent');
// iOS Safari와 Android Chrome에서 키보드 대응
textarea.addEventListener('focus', function() {
// 모바일 디바이스 감지
if (window.innerWidth <= 768) {
// 약간의 지연 후 스크롤 (키보드 애니메이션 완료 대기)
setTimeout(() => {
// textarea를 화면 중앙으로 부드럽게 스크롤
textarea.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}, 300);
}
});
// 키보드가 사라질 때 처리
textarea.addEventListener('blur', function() {
if (window.innerWidth <= 768) {
// 키보드가 사라진 후 전체 레이아웃 복원
setTimeout(() => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 100);
}
});
// 뷰포트 크기 변화 감지 (키보드 나타남/사라짐)
let initialViewportHeight = window.innerHeight;
window.addEventListener('resize', function() {
const currentHeight = window.innerHeight;
const heightDifference = initialViewportHeight - currentHeight;
// 키보드가 나타났을 때 (높이가 150px 이상 줄어들었을 때)
if (heightDifference > 150 && document.activeElement === textarea) {
setTimeout(() => {
textarea.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
});
}, 100);
}
});
// 터치 시작 시 현재 뷰포트 높이 업데이트
document.addEventListener('touchstart', function() {
initialViewportHeight = window.innerHeight;
});
}
// 파일 선택
function selectFile() {
document.getElementById('desktopFileInput').click();
}
// 카메라 열기
function openCamera() {
document.getElementById('cameraInput').click();
}
// 갤러리 열기
function openGallery() {
document.getElementById('galleryInput').click();
}
// 드래그 오버
function handleDragOver(e) {
e.preventDefault();
e.currentTarget.classList.add('border-purple-300', 'bg-purple-50');
}
// 드롭
function handleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('border-purple-300', 'bg-purple-50');
const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'));
processFiles(files);
}
// 파일 선택 처리
function handleFileSelect(e) {
const files = Array.from(e.target.files);
processFiles(files);
}
// 파일 처리
async function processFiles(files) {
if (currentPhotos.length + files.length > 5) {
alert('최대 5장까지만 업로드할 수 있습니다.');
return;
}
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
let processedFile = file;
// 간단한 이미지 압축 시도
try {
// 파일 크기가 2MB 이상인 경우 압축 시도
if (file.size > 2 * 1024 * 1024) {
console.log(`큰 파일 감지 (${(file.size / 1024 / 1024).toFixed(2)}MB), 압축 시도:`, file.name);
const compressed = await compressImageSimple(file, 0.7, 1920);
if (compressed && compressed.size < file.size) {
processedFile = compressed;
console.log(`압축 성공: ${(file.size / 1024 / 1024).toFixed(2)}MB → ${(compressed.size / 1024 / 1024).toFixed(2)}MB`);
} else {
console.warn('압축 효과 없음, 원본 사용:', file.name);
}
} else {
console.log('작은 파일, 압축 생략:', file.name);
}
} catch (compressionError) {
console.warn('이미지 압축 실패, 원본 사용:', compressionError);
}
const base64 = await convertFileToBase64(processedFile);
currentPhotos.push({
file: processedFile,
base64: base64,
name: file.name
});
console.log('파일 처리 완료:', file.name);
} catch (error) {
console.error('이미지 처리 실패:', error);
alert(`이미지 처리에 실패했습니다: ${file.name}`);
}
}
}
updatePhotoPreview();
}
// 간단한 이미지 압축 함수
function compressImageSimple(file, quality = 0.7, maxWidth = 1920) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = function() {
// 비율 유지하면서 크기 조정
let { width, height } = img;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
canvas.width = width;
canvas.height = height;
// 이미지 그리기
ctx.drawImage(img, 0, 0, width, height);
// Blob으로 변환
canvas.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('압축 실패'));
}
},
'image/jpeg',
quality
);
};
img.onerror = () => reject(new Error('이미지 로드 실패'));
img.src = URL.createObjectURL(file);
});
}
// Base64 변환
function convertFileToBase64(file) {
return new Promise((resolve, reject) => {
// 파일이 유효한 Blob/File인지 확인
if (!file || !(file instanceof Blob || file instanceof File)) {
reject(new Error('유효하지 않은 파일 객체입니다.'));
return;
}
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => {
console.error('FileReader 오류:', error);
reject(error);
};
try {
reader.readAsDataURL(file);
} catch (error) {
console.error('readAsDataURL 호출 오류:', error);
reject(error);
}
});
}
// 사진 미리보기 업데이트
function updatePhotoPreview() {
const previewGrid = document.getElementById('photoPreviewGrid');
const photoGrid = document.getElementById('photoGrid');
const photoCount = document.getElementById('photoCount');
if (currentPhotos.length === 0) {
previewGrid.classList.add('hidden');
return;
}
previewGrid.classList.remove('hidden');
photoCount.textContent = currentPhotos.length;
photoGrid.innerHTML = currentPhotos.map((photo, index) => `
<div class="photo-item">
<img src="${photo.base64}" alt="${photo.name}">
<button type="button" class="photo-remove" onclick="removePhoto(${index})">
<i class="fas fa-times text-sm"></i>
</button>
</div>
`).join('');
}
// 사진 제거
function removePhoto(index) {
currentPhotos.splice(index, 1);
updatePhotoPreview();
}
// 모든 사진 제거
function removeAllPhotos() {
currentPhotos = [];
updatePhotoPreview();
}
// 업로드 제출
async function handleUploadSubmit(e) {
e.preventDefault();
const content = document.getElementById('uploadContent').value.trim();
if (!content) {
alert('메모를 입력해주세요.');
return;
}
try {
document.getElementById('loadingOverlay').classList.remove('hidden');
const todoData = {
title: null, // 메모는 제목 없음
description: content,
category: 'memo',
image_urls: currentPhotos.map(photo => photo.base64)
};
console.log('업로드 데이터:', todoData);
const response = await TodoAPI.createTodo(todoData);
console.log('업로드 성공:', response);
// 성공 시 폼 초기화 및 메시지 표시
showSuccessMessage('메모가 성공적으로 저장되었습니다!');
clearForm();
} catch (error) {
console.error('업로드 실패:', error);
alert('업로드에 실패했습니다. 다시 시도해주세요.');
} finally {
document.getElementById('loadingOverlay').classList.add('hidden');
}
}
// 성공 메시지 표시
function showSuccessMessage(message) {
// 기존 메시지 제거
const existingMessage = document.querySelector('.success-message');
if (existingMessage) {
existingMessage.remove();
}
// 새 메시지 생성
const messageDiv = document.createElement('div');
messageDiv.className = 'success-message fixed top-4 left-1/2 transform -translate-x-1/2 z-50 px-6 py-3 rounded-lg shadow-lg';
messageDiv.style.background = 'var(--gold)';
messageDiv.style.color = 'var(--ink)';
messageDiv.style.fontFamily = 'Noto Serif KR, serif';
messageDiv.innerHTML = `<i class="fas fa-check-circle mr-2"></i>${message}`;
document.body.appendChild(messageDiv);
// 3초 후 제거
setTimeout(() => {
messageDiv.remove();
}, 3000);
}
// 폼 초기화
function clearForm() {
document.getElementById('uploadContent').value = '';
removeAllPhotos();
document.getElementById('uploadContent').focus();
// 최근 업로드 목록 새로고침
loadRecentUploads();
}
// 최근 업로드 항목 로드
async function loadRecentUploads() {
try {
const response = await TodoAPI.getTodos(null, 'memo');
const recentItems = response.slice(0, 3); // 최근 3개만
renderRecentUploads(recentItems);
} catch (error) {
console.error('최근 업로드 로드 실패:', error);
document.getElementById('recentUploads').innerHTML = `
<div class="text-center py-4" style="color: var(--ink-light);">
<i class="fas fa-exclamation-triangle mr-2"></i>
최근 메모를 불러올 수 없습니다.
</div>
`;
}
}
// 최근 업로드 항목 렌더링
function renderRecentUploads(items) {
const container = document.getElementById('recentUploads');
if (!items || items.length === 0) {
container.innerHTML = `
<div class="text-center py-8" style="color: var(--ink-light);">
<i class="fas fa-inbox mr-2"></i>
아직 저장된 메모가 없습니다.
</div>
`;
return;
}
container.innerHTML = items.map(item => {
const createdAt = new Date(item.created_at);
const timeAgo = getTimeAgo(createdAt);
const hasImages = item.image_urls && item.image_urls.length > 0;
const content = item.description || item.title || '내용 없음';
return `
<div class="recent-item p-3 rounded-lg border" style="background: var(--parchment-dark); border-color: var(--sepia);">
<div class="flex items-start justify-between">
<div class="flex-1">
<p class="text-sm line-clamp-2 mb-2" style="color: var(--ink);">
${content.length > 80 ? content.substring(0, 80) + '...' : content}
</p>
<div class="flex items-center text-xs" style="color: var(--ink-light);">
<i class="fas fa-clock mr-1"></i>
<span>${timeAgo}</span>
${hasImages ? `<i class="fas fa-images ml-3 mr-1"></i><span>${item.image_urls.length}장</span>` : ''}
</div>
</div>
${hasImages && item.image_urls[0] ? `
<div class="ml-3 flex-shrink-0">
<img src="${item.image_urls[0]}" alt="첨부 이미지"
class="w-12 h-12 object-cover rounded border"
style="border-color: var(--sepia);">
</div>
` : ''}
</div>
</div>
`;
}).join('');
}
// 시간 경과 표시
function getTimeAgo(date) {
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return '방금 전';
if (diffMins < 60) return `${diffMins}분 전`;
if (diffHours < 24) return `${diffHours}시간 전`;
if (diffDays < 7) return `${diffDays}일 전`;
return date.toLocaleDateString('ko-KR', {
month: 'short',
day: 'numeric'
});
}
// 전역 함수 등록
window.selectFile = selectFile;
window.openCamera = openCamera;
window.openGallery = openGallery;
window.removePhoto = removePhoto;
window.removeAllPhotos = removeAllPhotos;
</script>
</body>
</html>