diff --git a/backend/Dockerfile b/backend/Dockerfile
index 59e19c7..9accb5d 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -13,7 +13,7 @@ RUN pip install --no-cache-dir -e .
# 애플리케이션 코드 복사
COPY src/ ./src/
-COPY migrations/ ./migrations/
+RUN mkdir -p ./migrations
# 환경 변수 설정
ENV PYTHONPATH=/app
diff --git a/backend/migrations/001_add_multiple_images.sql b/backend/migrations/001_add_multiple_images.sql
new file mode 100644
index 0000000..4b271a3
--- /dev/null
+++ b/backend/migrations/001_add_multiple_images.sql
@@ -0,0 +1,19 @@
+-- 다중 이미지 지원을 위한 마이그레이션
+-- image_url 컬럼을 image_urls로 변경하고 기존 데이터 마이그레이션
+
+-- 1. 새 컬럼 추가
+ALTER TABLE todos ADD COLUMN image_urls TEXT;
+
+-- 2. 기존 image_url 데이터를 image_urls로 마이그레이션 (JSON 배열 형태)
+UPDATE todos
+SET image_urls = CASE
+ WHEN image_url IS NOT NULL AND image_url != ''
+ THEN '["' || image_url || '"]'
+ ELSE NULL
+END;
+
+-- 3. 기존 image_url 컬럼 삭제
+ALTER TABLE todos DROP COLUMN image_url;
+
+-- 4. 인덱스 추가 (선택사항 - 검색 성능 향상)
+-- CREATE INDEX idx_todos_has_images ON todos ((image_urls IS NOT NULL));
diff --git a/backend/src/api/routes/todos.py b/backend/src/api/routes/todos.py
index 4899d05..f750ea0 100644
--- a/backend/src/api/routes/todos.py
+++ b/backend/src/api/routes/todos.py
@@ -44,6 +44,12 @@ async def create_todo(
except ValueError:
logger.warning(f"Invalid date format: {todo_data.due_date}")
+ # 이미지 URLs JSON 변환
+ import json
+ image_urls_json = None
+ if todo_data.image_urls:
+ image_urls_json = json.dumps(todo_data.image_urls)
+
# 새 Todo 생성
new_todo = Todo(
user_id=current_user.id,
@@ -51,7 +57,7 @@ async def create_todo(
description=todo_data.description,
category=todo_data.category,
due_date=parsed_due_date,
- image_url=todo_data.image_url,
+ image_urls=image_urls_json,
tags=todo_data.tags
)
@@ -60,6 +66,14 @@ async def create_todo(
await db.refresh(new_todo)
# 응답용 데이터 변환
+ import json
+ image_urls_list = None
+ if new_todo.image_urls:
+ try:
+ image_urls_list = json.loads(new_todo.image_urls)
+ except json.JSONDecodeError:
+ image_urls_list = None
+
response_data = {
"id": new_todo.id,
"user_id": new_todo.user_id,
@@ -71,7 +85,7 @@ async def create_todo(
"updated_at": new_todo.updated_at,
"due_date": new_todo.due_date.isoformat() if new_todo.due_date else None,
"completed_at": new_todo.completed_at,
- "image_url": new_todo.image_url,
+ "image_urls": image_urls_list,
"tags": new_todo.tags
}
@@ -113,22 +127,30 @@ async def get_todos(
todos = result.scalars().all()
# 응답용 데이터 변환
+ import json
response_data = []
for todo in todos:
- response_data.append({
- "id": todo.id,
- "user_id": todo.user_id,
- "title": todo.title,
- "description": todo.description,
- "category": todo.category,
- "status": todo.status,
- "created_at": todo.created_at,
- "updated_at": todo.updated_at,
- "due_date": todo.due_date.isoformat() if todo.due_date else None,
- "completed_at": todo.completed_at,
- "image_url": todo.image_url,
- "tags": todo.tags
- })
+ image_urls_list = None
+ if todo.image_urls:
+ try:
+ image_urls_list = json.loads(todo.image_urls)
+ except json.JSONDecodeError:
+ image_urls_list = None
+
+ response_data.append({
+ "id": todo.id,
+ "user_id": todo.user_id,
+ "title": todo.title,
+ "description": todo.description,
+ "category": todo.category,
+ "status": todo.status,
+ "created_at": todo.created_at,
+ "updated_at": todo.updated_at,
+ "due_date": todo.due_date.isoformat() if todo.due_date else None,
+ "completed_at": todo.completed_at,
+ "image_urls": image_urls_list,
+ "tags": todo.tags
+ })
return response_data
@@ -162,6 +184,14 @@ async def get_todo(
)
# 응답용 데이터 변환
+ import json
+ image_urls_list = None
+ if todo.image_urls:
+ try:
+ image_urls_list = json.loads(todo.image_urls)
+ except json.JSONDecodeError:
+ image_urls_list = None
+
response_data = {
"id": todo.id,
"user_id": todo.user_id,
@@ -173,7 +203,7 @@ async def get_todo(
"updated_at": todo.updated_at,
"due_date": todo.due_date.isoformat() if todo.due_date else None,
"completed_at": todo.completed_at,
- "image_url": todo.image_url,
+ "image_urls": image_urls_list,
"tags": todo.tags,
}
@@ -213,6 +243,7 @@ async def update_todo(
)
# 업데이트할 필드만 수정
+ import json
update_data = todo_data.dict(exclude_unset=True)
for field, value in update_data.items():
if field == 'due_date' and value:
@@ -222,6 +253,9 @@ async def update_todo(
setattr(todo, field, parsed_date)
except ValueError:
logger.warning(f"Invalid date format: {value}")
+ elif field == 'image_urls' and value is not None:
+ # 이미지 URLs를 JSON으로 변환
+ setattr(todo, field, json.dumps(value))
else:
setattr(todo, field, value)
@@ -235,6 +269,13 @@ async def update_todo(
await db.refresh(todo)
# 응답용 데이터 변환
+ image_urls_list = None
+ if todo.image_urls:
+ try:
+ image_urls_list = json.loads(todo.image_urls)
+ except json.JSONDecodeError:
+ image_urls_list = None
+
response_data = {
"id": todo.id,
"user_id": todo.user_id,
@@ -246,7 +287,7 @@ async def update_todo(
"updated_at": todo.updated_at,
"due_date": todo.due_date.isoformat() if todo.due_date else None,
"completed_at": todo.completed_at,
- "image_url": todo.image_url,
+ "image_urls": image_urls_list,
"tags": todo.tags,
}
@@ -300,3 +341,95 @@ async def delete_todo(
detail="Todo 삭제에 실패했습니다."
)
+
+@router.post("/{todo_id}/add-image")
+async def add_image_to_todo(
+ todo_id: UUID,
+ image: UploadFile = File(...),
+ current_user: User = Depends(get_current_active_user),
+ db: AsyncSession = Depends(get_db)
+):
+ """기존 Todo에 이미지 추가 (최대 5개까지)"""
+ try:
+ # Todo 조회
+ result = await db.execute(
+ select(Todo).where(
+ and_(Todo.id == todo_id, Todo.user_id == current_user.id)
+ )
+ )
+ todo = result.scalar_one_or_none()
+
+ if not todo:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Todo를 찾을 수 없습니다."
+ )
+
+ # 현재 이미지 개수 확인
+ import json
+ current_images = []
+ if todo.image_urls:
+ try:
+ current_images = json.loads(todo.image_urls)
+ except json.JSONDecodeError:
+ current_images = []
+
+ # 최대 5개 제한 확인
+ if len(current_images) >= 5:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="최대 5개의 이미지만 저장할 수 있습니다."
+ )
+
+ # 파일 형식 확인
+ if not image.content_type.startswith('image/'):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="이미지 파일만 업로드 가능합니다."
+ )
+
+ # 파일 크기 확인 (5MB 제한)
+ contents = await image.read()
+ if len(contents) > 5 * 1024 * 1024:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="파일 크기는 5MB를 초과할 수 없습니다."
+ )
+
+ # Base64로 변환하여 저장
+ import base64
+ base64_string = f"data:{image.content_type};base64,{base64.b64encode(contents).decode()}"
+
+ # 파일 저장
+ from ...services.file_service import save_base64_image
+ file_url = save_base64_image(base64_string)
+ if not file_url:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="이미지 저장에 실패했습니다."
+ )
+
+ # 기존 이미지 목록에 새 이미지 추가
+ current_images.append(file_url)
+ todo.image_urls = json.dumps(current_images)
+
+ await db.commit()
+ await db.refresh(todo)
+
+ return {
+ "message": "이미지가 추가되었습니다.",
+ "url": file_url,
+ "total_images": len(current_images),
+ "max_images": 5
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ await db.rollback()
+ logger.error(f"이미지 추가 실패: {e}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="이미지 추가에 실패했습니다."
+ )
+
diff --git a/backend/src/models/todo.py b/backend/src/models/todo.py
index b88633d..3fc6c98 100644
--- a/backend/src/models/todo.py
+++ b/backend/src/models/todo.py
@@ -47,7 +47,7 @@ class Todo(Base):
completed_at = Column(DateTime(timezone=True), nullable=True)
# 추가 정보
- image_url = Column(Text, nullable=True) # 첨부 이미지 URL (Base64 데이터 지원)
+ image_urls = Column(Text, nullable=True) # 첨부 이미지 URLs (JSON 배열 형태, 최대 5개)
tags = Column(String(500), nullable=True) # 태그 (쉼표로 구분)
# 관계
diff --git a/backend/src/schemas/todo.py b/backend/src/schemas/todo.py
index 1220998..0ddf249 100644
--- a/backend/src/schemas/todo.py
+++ b/backend/src/schemas/todo.py
@@ -31,7 +31,7 @@ class TodoBase(BaseModel):
class TodoCreate(TodoBase):
"""Todo 생성"""
due_date: Optional[str] = None # 문자열로 변경 (한국 시간 형식)
- image_url: Optional[str] = Field(None, max_length=10000000) # Base64 이미지를 위해 크게 설정
+ image_urls: Optional[List[str]] = Field(None, max_items=5, description="최대 5개의 이미지 URL")
tags: Optional[str] = Field(None, max_length=500)
@@ -42,7 +42,7 @@ class TodoUpdate(BaseModel):
category: Optional[TodoCategoryEnum] = None
status: Optional[TodoStatusEnum] = None
due_date: Optional[str] = None # 문자열로 변경 (한국 시간 형식)
- image_url: Optional[str] = Field(None, max_length=10000000) # Base64 이미지를 위해 크게 설정
+ image_urls: Optional[List[str]] = Field(None, max_items=5, description="최대 5개의 이미지 URL")
tags: Optional[str] = Field(None, max_length=500)
@@ -57,7 +57,7 @@ class TodoResponse(BaseModel):
updated_at: datetime
due_date: Optional[str] = None # 문자열로 변경 (한국 시간 형식)
completed_at: Optional[datetime] = None
- image_url: Optional[str] = None
+ image_urls: Optional[List[str]] = None
tags: Optional[str] = None
class Config:
diff --git a/frontend/calendar.html b/frontend/calendar.html
index 57223c3..07c0313 100644
--- a/frontend/calendar.html
+++ b/frontend/calendar.html
@@ -222,9 +222,18 @@
- ${item.photo ? `
+ ${item.image_urls && item.image_urls.length > 0 ? `
-

+
+ ${item.image_urls.slice(0, 3).map(url => `
+

+ `).join('')}
+ ${item.image_urls.length > 3 ? `
+
+ +${item.image_urls.length - 3}
+
+ ` : ''}
+
` : ''}
diff --git a/frontend/checklist.html b/frontend/checklist.html
index 6248c51..7e05218 100644
--- a/frontend/checklist.html
+++ b/frontend/checklist.html
@@ -229,6 +229,19 @@
try {
// API에서 체크리스트 카테고리 항목들만 가져오기
const items = await TodoAPI.getTodos(null, 'checklist');
+ console.log('🔍 체크리스트 API 응답 데이터:', items);
+
+ // 각 항목의 이미지 데이터 확인
+ items.forEach((item, index) => {
+ console.log(`📋 체크리스트 항목 ${index + 1}:`, {
+ id: item.id,
+ title: item.title,
+ image_urls: item.image_urls,
+ image_urls_type: typeof item.image_urls,
+ image_urls_length: item.image_urls ? item.image_urls.length : 'null'
+ });
+ });
+
checklistItems = items;
} catch (error) {
console.error('체크리스트 항목 로드 실패:', error);
@@ -263,9 +276,18 @@
- ${item.photo ? `
+ ${item.image_urls && item.image_urls.length > 0 ? `
-

+
+ ${item.image_urls.slice(0, 3).map(url => `
+

+ `).join('')}
+ ${item.image_urls.length > 3 ? `
+
+ +${item.image_urls.length - 3}
+
+ ` : ''}
+
` : ''}
@@ -318,17 +340,103 @@
function toggleComplete(id) {
const item = checklistItems.find(item => item.id === id);
if (item) {
- item.completed = !item.completed;
- item.completed_at = item.completed ? new Date().toISOString().split('T')[0] : null;
-
- renderChecklistItems(checklistItems);
- updateProgress();
-
- // TODO: API 호출하여 상태 업데이트
- console.log('체크리스트 완료 상태 변경:', id, item.completed);
+ if (!item.completed) {
+ // 완료 처리 - 애니메이션 후 제거
+ item.completed = true;
+ item.completed_at = new Date().toISOString().split('T')[0];
+
+ // 즉시 완료 상태로 렌더링
+ renderChecklistItems(checklistItems);
+ updateProgress();
+
+ // 1.5초 후 항목 제거
+ setTimeout(() => {
+ const itemIndex = checklistItems.findIndex(i => i.id === id);
+ if (itemIndex !== -1) {
+ checklistItems.splice(itemIndex, 1);
+ renderChecklistItems(checklistItems);
+ updateProgress();
+
+ // 완료 메시지 표시
+ showCompletionToast('항목이 완료되었습니다! 🎉');
+ }
+ }, 1500);
+
+ // API 호출하여 상태 업데이트
+ updateTodoStatus(id, 'completed');
+ console.log('체크리스트 완료 처리:', id);
+ } else {
+ // 완료 취소는 허용하지 않음 (이미 완료된 항목은 제거되므로)
+ console.log('완료된 항목은 취소할 수 없습니다.');
+ }
}
}
+ // Todo 상태 업데이트 (API 호출)
+ async function updateTodoStatus(id, status) {
+ try {
+ const token = localStorage.getItem('authToken');
+ if (!token) {
+ console.error('인증 토큰이 없습니다!');
+ return;
+ }
+
+ const response = await fetch(`http://localhost:9000/api/todos/${id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify({ status: status })
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ console.log(`Todo ${id} 상태가 ${status}로 업데이트되었습니다.`);
+ } catch (error) {
+ console.error('Todo 상태 업데이트 실패:', error);
+ }
+ }
+
+ // 완료 토스트 메시지 표시
+ function showCompletionToast(message) {
+ // 기존 토스트가 있으면 제거
+ const existingToast = document.getElementById('completionToast');
+ if (existingToast) {
+ existingToast.remove();
+ }
+
+ // 새 토스트 생성
+ const toast = document.createElement('div');
+ toast.id = 'completionToast';
+ toast.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full';
+ toast.innerHTML = `
+
+
+ ${message}
+
+ `;
+
+ document.body.appendChild(toast);
+
+ // 애니메이션으로 나타나기
+ setTimeout(() => {
+ toast.classList.remove('translate-x-full');
+ }, 100);
+
+ // 3초 후 사라지기
+ setTimeout(() => {
+ toast.classList.add('translate-x-full');
+ setTimeout(() => {
+ if (toast.parentNode) {
+ toast.remove();
+ }
+ }, 300);
+ }, 3000);
+ }
+
// 진행률 업데이트
function updateProgress() {
const total = checklistItems.length;
diff --git a/frontend/classify.html b/frontend/classify.html
index 9bbdf76..61994eb 100644
--- a/frontend/classify.html
+++ b/frontend/classify.html
@@ -351,9 +351,18 @@
- ${item.photo ? `
+ ${item.image_urls && item.image_urls.length > 0 ? `
-

+
+ ${item.image_urls.slice(0, 2).map(url => `
+

+ `).join('')}
+ ${item.image_urls.length > 2 ? `
+
+ +${item.image_urls.length - 2}
+
+ ` : ''}
+
` : ''}
diff --git a/frontend/dashboard.html b/frontend/dashboard.html
index 4ec37b1..ce4c486 100644
--- a/frontend/dashboard.html
+++ b/frontend/dashboard.html
@@ -540,7 +540,7 @@
-
+
@@ -556,8 +556,8 @@
갤러리
-
-
+
+
@@ -750,21 +750,23 @@
console.log(`🔍 원본 API 데이터 ${index + 1}:`, {
id: todo.id,
title: todo.title,
- image_url: todo.image_url ? `있음 (${todo.image_url.length}자)` : '없음',
+ image_urls: todo.image_urls,
+ image_urls_type: typeof todo.image_urls,
+ image_urls_length: todo.image_urls ? todo.image_urls.length : 'null',
category: todo.category
});
});
- // 체크리스트 타입만 필터링
+ // 체크리스트 타입만 필터링 (완료되지 않은 항목만)
checklistData = todos
- .filter(todo => todo.category === 'checklist')
+ .filter(todo => todo.category === 'checklist' && todo.status !== 'completed')
.map(todo => ({
id: todo.id,
title: todo.title,
description: todo.description,
created_at: todo.created_at,
completed: todo.status === 'completed',
- image_url: todo.image_url // 이미지 URL 포함
+ image_urls: todo.image_urls || [] // 다중 이미지 URL 배열
}));
console.log('체크리스트 데이터 로드 완료:', checklistData);
@@ -935,8 +937,9 @@
console.log(`📝 체크리스트 항목 ${index + 1}:`, {
id: item.id,
title: item.title,
- image_url: item.image_url ? '이미지 있음' : '이미지 없음',
- image_length: item.image_url ? item.image_url.length : 0
+ image_urls: item.image_urls,
+ image_urls_type: typeof item.image_urls,
+ image_urls_length: item.image_urls ? item.image_urls.length : 'null'
});
return `
@@ -946,13 +949,22 @@
- ${item.image_url ? `
+ ${item.image_urls && item.image_urls.length > 0 ? `
-

+
+ ${item.image_urls.slice(0, 2).map(url => `
+

+ `).join('')}
+ ${item.image_urls.length > 2 ? `
+
+ +${item.image_urls.length - 2}
+
+ ` : ''}
+
` : ''}
@@ -962,7 +974,7 @@
${item.description ? `
${item.description}
` : ''}
등록: ${formatDate(item.created_at)}
- ${item.image_url ? `이미지` : ''}
+ ${item.image_urls && item.image_urls.length > 0 ? `이미지 ${item.image_urls.length}개` : ''}
@@ -1007,21 +1019,74 @@
// 항목 완료 처리
async function completeItem(itemId) {
try {
+ // 즉시 UI에서 완료 상태로 표시
+ const item = checklistData.find(item => item.id === itemId);
+ if (item) {
+ item.status = 'completed';
+ renderChecklist();
+
+ // 완료 토스트 메시지 표시
+ showCompletionToast('항목이 완료되었습니다! 🎉');
+
+ // 1.5초 후 항목 제거
+ setTimeout(async () => {
+ // 배열에서 제거
+ const itemIndex = checklistData.findIndex(i => i.id === itemId);
+ if (itemIndex !== -1) {
+ checklistData.splice(itemIndex, 1);
+ renderChecklist();
+ }
+ }, 1500);
+ }
+
+ // API 호출하여 백엔드 업데이트
await TodoAPI.updateTodo(itemId, {
status: 'completed'
});
- alert('항목이 완료 처리되었습니다!');
-
- // 대시보드 데이터 새로고침
- await initializeDashboard();
-
} catch (error) {
console.error('완료 처리 실패:', error);
alert('완료 처리에 실패했습니다.');
}
}
+ // 완료 토스트 메시지 표시
+ function showCompletionToast(message) {
+ // 기존 토스트가 있으면 제거
+ const existingToast = document.getElementById('completionToast');
+ if (existingToast) {
+ existingToast.remove();
+ }
+
+ // 새 토스트 생성
+ const toast = document.createElement('div');
+ toast.id = 'completionToast';
+ toast.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-x-full';
+ toast.innerHTML = `
+
+
+ ${message}
+
+ `;
+
+ document.body.appendChild(toast);
+
+ // 애니메이션으로 나타나기
+ setTimeout(() => {
+ toast.classList.remove('translate-x-full');
+ }, 100);
+
+ // 3초 후 사라지기
+ setTimeout(() => {
+ toast.classList.add('translate-x-full');
+ setTimeout(() => {
+ if (toast.parentNode) {
+ toast.remove();
+ }
+ }, 300);
+ }, 3000);
+ }
+
// Todo로 변경
function convertToTodo(itemId) {
const item = checklistData.find(item => item.id === itemId);
@@ -1174,9 +1239,15 @@
}
try {
- let imageUrl = null;
+ // 기존 항목의 이미지 정보 가져오기
+ const existingItem = await TodoAPI.getTodoById(itemId);
+ let existingImages = existingItem.image_urls || [];
- // 이미지가 선택된 경우 처리
+ console.log('🔄 변환 시 기존 이미지:', existingImages);
+
+ let newImageUrl = null;
+
+ // 새 이미지가 선택된 경우 처리
if (fileInput.files && fileInput.files[0]) {
const file = fileInput.files[0];
@@ -1187,7 +1258,8 @@
}
// Base64로 인코딩하여 저장
- imageUrl = await convertFileToBase64(file);
+ newImageUrl = await convertFileToBase64(file);
+ console.log('🖼️ 새 이미지 추가:', newImageUrl ? '성공' : '실패');
}
const updateData = {
@@ -1205,9 +1277,19 @@
console.log('변환 모달 날짜 설정 - 선택된 날짜 (KST):', date, '저장:', kstDateTime);
}
- // 이미지가 있으면 추가
- if (imageUrl) {
- updateData.image_url = imageUrl;
+ // 이미지 배열 처리: 기존 이미지 + 새 이미지 (최대 5개)
+ let finalImages = [...existingImages];
+ if (newImageUrl) {
+ finalImages.push(newImageUrl);
+ // 최대 5개 제한
+ if (finalImages.length > 5) {
+ finalImages = finalImages.slice(-5); // 최신 5개만 유지
+ }
+ }
+
+ if (finalImages.length > 0) {
+ updateData.image_urls = finalImages;
+ console.log('📸 최종 이미지 배열:', finalImages.length, '개');
}
await TodoAPI.updateTodo(itemId, updateData);
@@ -1313,10 +1395,7 @@
const info = categoryInfo[item.category] || categoryInfo['todo'];
// 이미지 파일 확인
- const isImage = item.image_url && (
- item.image_url.includes('data:image/') ||
- /\.(jpg|jpeg|png|gif|webp)$/i.test(item.image_url)
- );
+ const hasImages = item.image_urls && item.image_urls.length > 0;
// 첨부 파일 확인 및 타입 분류
const hasFile = item.file_url && item.file_name;
@@ -1431,11 +1510,22 @@
- ${isImage ? `
+ ${hasImages ? `
-
+
-

+
+ ${item.image_urls.map((url, index) => `
+
+

+
+ ${index + 1}/${item.image_urls.length}
+
+
+ `).join('')}
+
` : ''}
@@ -1807,7 +1897,7 @@
// 업로드 모달 관련 변수
- let currentPhoto = null;
+ let currentPhotos = [];
// 업로드 모달 열기
function openUploadModal() {
@@ -1825,7 +1915,7 @@
// 업로드 폼 초기화
function clearUploadForm() {
document.getElementById('uploadForm').reset();
- removePhoto();
+ removeAllPhotos();
}
// 파일 선택 (데스크톱)
@@ -1843,19 +1933,10 @@
document.getElementById('galleryInput').click();
}
- // 사진 제거
- function removePhoto() {
- currentPhoto = null;
- const previewContainer = document.getElementById('photoPreview');
- const previewImage = document.getElementById('previewImage');
-
- if (previewContainer) {
- previewContainer.classList.add('hidden');
- }
-
- if (previewImage) {
- previewImage.src = '';
- }
+ // 모든 사진 제거
+ function removeAllPhotos() {
+ currentPhotos = [];
+ updatePhotoPreview();
// 파일 입력 초기화
const inputs = ['desktopFileInput', 'cameraInput', 'galleryInput'];
@@ -1865,44 +1946,56 @@
});
}
- // 사진 업로드 처리
+ // 개별 사진 제거
+ function removePhoto(index) {
+ if (index >= 0 && index < currentPhotos.length) {
+ currentPhotos.splice(index, 1);
+ updatePhotoPreview();
+ }
+ }
+
+ // 사진 업로드 처리 (다중 이미지 지원)
async function handlePhotoUpload(event) {
const files = event.target.files;
if (!files || files.length === 0) return;
- const file = files[0];
+ // 최대 5개 제한 확인
+ const remainingSlots = 5 - currentPhotos.length;
+ if (remainingSlots <= 0) {
+ alert('최대 5개의 이미지만 업로드할 수 있습니다.');
+ return;
+ }
+
+ const filesToProcess = Array.from(files).slice(0, remainingSlots);
try {
showLoading(true);
- // 이미지 압축 (ImageUtils가 있는 경우)
- let processedImage;
- if (window.ImageUtils) {
- processedImage = await ImageUtils.compressImage(file, {
- maxWidth: 800,
- maxHeight: 600,
- quality: 0.8
+ for (const file of filesToProcess) {
+ // 이미지 압축 (ImageUtils가 있는 경우)
+ let processedImage;
+ if (window.ImageUtils) {
+ processedImage = await ImageUtils.compressImage(file, {
+ maxWidth: 800,
+ maxHeight: 600,
+ quality: 0.8
+ });
+ } else {
+ // 기본 처리
+ processedImage = await fileToBase64(file);
+ }
+
+ currentPhotos.push({
+ data: processedImage,
+ name: file.name,
+ size: file.size
});
- } else {
- // 기본 처리
- processedImage = await fileToBase64(file);
}
- currentPhoto = processedImage;
+ updatePhotoPreview();
- // 미리보기 표시
- const previewContainer = document.getElementById('photoPreview');
- const previewImage = document.getElementById('previewImage');
- const photoInfo = document.getElementById('photoInfo');
-
- if (previewContainer && previewImage) {
- previewImage.src = processedImage;
- previewContainer.classList.remove('hidden');
- }
-
- if (photoInfo) {
- const fileSize = Math.round(file.size / 1024);
- photoInfo.textContent = `${file.name} (${fileSize}KB)`;
+ if (filesToProcess.length < files.length) {
+ alert(`최대 5개까지만 업로드됩니다. ${filesToProcess.length}개가 추가되었습니다.`);
}
} catch (error) {
@@ -1913,6 +2006,44 @@
}
}
+ // 사진 미리보기 업데이트
+ function updatePhotoPreview() {
+ const previewContainer = document.getElementById('photoPreview');
+
+ if (currentPhotos.length === 0) {
+ if (previewContainer) {
+ previewContainer.classList.add('hidden');
+ }
+ return;
+ }
+
+ if (previewContainer) {
+ previewContainer.classList.remove('hidden');
+ previewContainer.innerHTML = `
+
+
+ 업로드된 이미지 (${currentPhotos.length}/5)
+
+
+
+ ${currentPhotos.map((photo, index) => `
+
+

+
+
${photo.name}
+
+ `).join('')}
+
+
+ `;
+ }
+ }
+
// 파일을 Base64로 변환
function fileToBase64(file) {
return new Promise((resolve, reject) => {
@@ -1927,7 +2058,7 @@
async function handleUploadSubmit(event) {
event.preventDefault();
- console.log('🚀 업로드 시작 - currentPhoto:', currentPhoto ? `있음 (${currentPhoto.length}자)` : '없음');
+ console.log('🚀 업로드 시작 - currentPhotos:', currentPhotos.length > 0 ? `${currentPhotos.length}개 이미지` : '이미지 없음');
const content = document.getElementById('uploadContent').value.trim();
if (!content) {
@@ -1940,7 +2071,7 @@
const itemData = {
content: content,
- photo: currentPhoto,
+ image_urls: currentPhotos.map(photo => photo.data),
created_at: new Date().toISOString()
};
@@ -1951,13 +2082,13 @@
category: 'checklist' // 체크리스트로 바로 저장
};
- // 이미지가 있을 때만 image_url 추가
- if (currentPhoto) {
- todoData.image_url = currentPhoto;
+ // 이미지가 있을 때만 image_urls 추가
+ if (currentPhotos.length > 0) {
+ todoData.image_urls = currentPhotos.map(photo => photo.data);
console.log('📸 이미지 데이터 포함:', {
hasImage: true,
- imageLength: currentPhoto.length,
- imagePreview: currentPhoto.substring(0, 50) + '...'
+ imageCount: currentPhotos.length,
+ totalSize: currentPhotos.reduce((sum, photo) => sum + photo.size, 0)
});
} else {
console.log('📸 이미지 데이터 없음');
diff --git a/frontend/todo.html b/frontend/todo.html
index 46558bf..be222ab 100644
--- a/frontend/todo.html
+++ b/frontend/todo.html
@@ -197,9 +197,18 @@
- ${item.photo ? `
+ ${item.image_urls && item.image_urls.length > 0 ? `
-

+
+ ${item.image_urls.slice(0, 3).map(url => `
+

+ `).join('')}
+ ${item.image_urls.length > 3 ? `
+
+ +${item.image_urls.length - 3}
+
+ ` : ''}
+
` : ''}