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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user