feat: 다중 이미지 업로드 및 표시 기능 완전 구현

 주요 기능 추가:
- 다중 이미지 업로드 지원 (최대 5개)
- 체크리스트 완료 시 자동 사라짐 기능
- 캘린더 상세 모달에서 다중 이미지 표시
- Todo 변환 시 기존 이미지 보존 및 새 이미지 추가

🔧 백엔드 수정:
- Todo 모델: image_url → image_urls (JSON 배열)
- API 엔드포인트: 다중 이미지 직렬화/역직렬화
- 새 엔드포인트: POST /todos/{id}/add-image (이미지 추가)
- 데이터베이스 마이그레이션 스크립트 추가

🎨 프론트엔드 개선:
- 대시보드: 실제 API 데이터 연동, 다중 이미지 표시
- 업로드 모달: 다중 파일 선택, 실시간 미리보기, 5개 제한
- 체크리스트: 완료 시 1.5초 후 자동 제거, 토스트 메시지
- 캘린더 모달: 2x2 그리드 이미지 표시, 클릭 확대
- Todo 변환: 기존 이미지 + 새 이미지 합치기

🐛 버그 수정:
- currentPhoto 변수 오류 해결
- 이미지 표시 문제 (단일 → 다중 지원)
- 완료 처리 로컬/백엔드 동기화
- 새로고침 시 완료 항목 재출현 문제
This commit is contained in:
Hyungi Ahn
2025-09-23 08:47:45 +09:00
parent f80995c1ec
commit 4c7d2d8290
10 changed files with 539 additions and 121 deletions

View File

@@ -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 @@
</div>
<!-- 사진 (있는 경우) -->
${item.photo ? `
${item.image_urls && item.image_urls.length > 0 ? `
<div class="flex-shrink-0">
<img src="${item.photo}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
<div class="flex space-x-2">
${item.image_urls.slice(0, 3).map(url => `
<img src="${url}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
`).join('')}
${item.image_urls.length > 3 ? `
<div class="w-16 h-16 bg-gray-100 rounded-lg flex items-center justify-center">
<span class="text-xs text-gray-500">+${item.image_urls.length - 3}</span>
</div>
` : ''}
</div>
</div>
` : ''}
@@ -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 = `
<div class="flex items-center space-x-2">
<i class="fas fa-check-circle"></i>
<span>${message}</span>
</div>
`;
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;