Files
Todo-Project/frontend/dashboard.html
Hyungi Ahn 4c7d2d8290 feat: 다중 이미지 업로드 및 표시 기능 완전 구현
 주요 기능 추가:
- 다중 이미지 업로드 지원 (최대 5개)
- 체크리스트 완료 시 자동 사라짐 기능
- 캘린더 상세 모달에서 다중 이미지 표시
- Todo 변환 시 기존 이미지 보존 및 새 이미지 추가

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

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

🐛 버그 수정:
- currentPhoto 변수 오류 해결
- 이미지 표시 문제 (단일 → 다중 지원)
- 완료 처리 로컬/백엔드 동기화
- 새로고침 시 완료 항목 재출현 문제
2025-09-23 08:47:45 +09:00

2326 lines
107 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>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #3b82f6; /* 하늘색 */
--primary-dark: #2563eb; /* 진한 하늘색 */
--success: #10b981; /* 초록색 */
--warning: #f59e0b; /* 주황색 */
--danger: #ef4444; /* 빨간색 */
--gray-50: #f9fafb; /* 연한 회색 */
--gray-100: #f3f4f6; /* 회색 */
--gray-200: #e5e7eb; /* 중간 회색 */
--gray-300: #d1d5db; /* 진한 회색 */
}
body {
background-color: var(--gray-50);
}
.btn-primary {
background-color: var(--primary);
color: white;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* 캘린더 스타일 */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background-color: var(--gray-200);
border-radius: 0.5rem;
overflow: hidden;
}
.calendar-day {
background-color: white;
min-height: 140px;
padding: 8px;
position: relative;
transition: all 0.2s;
}
.calendar-day:hover {
background-color: var(--gray-50);
}
.calendar-day.other-month {
background-color: #fafafa;
color: #9ca3af;
}
.calendar-day.today {
background-color: #eff6ff;
border: 2px solid var(--primary);
}
.calendar-header {
background-color: var(--gray-100);
padding: 12px 8px;
text-align: center;
font-weight: 600;
color: var(--gray-700);
}
.day-number {
font-weight: 600;
margin-bottom: 4px;
}
.day-items {
display: flex;
flex-direction: column;
gap: 2px;
}
.day-item {
font-size: 10px;
padding: 2px 4px;
border-radius: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.day-item.todo {
background-color: #dbeafe;
color: #1e40af;
border-left: 3px solid var(--primary);
}
.day-item.calendar {
background-color: #fef3c7;
color: #92400e;
border-left: 3px solid var(--warning);
}
.day-item.checklist {
background-color: #f0fdf4;
color: #166534;
border-left: 3px solid var(--success);
}
/* 우선순위별 스타일 */
.day-item.high {
border-left-width: 4px;
font-weight: 600;
}
.day-item.medium {
border-left-width: 3px;
}
.day-item.low {
border-left-width: 2px;
opacity: 0.8;
}
/* 완료된 항목 */
.day-item.completed {
opacity: 0.6;
text-decoration: line-through;
}
/* 모바일 일일 뷰 */
.daily-view {
display: none;
}
.daily-item {
background: white;
border-radius: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
margin-bottom: 12px;
}
.daily-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.time-indicator {
width: 4px;
height: 100%;
border-radius: 2px;
}
.time-indicator.todo {
background-color: var(--primary);
}
.time-indicator.calendar {
background-color: var(--warning);
}
/* 체크리스트 스타일 */
.checklist-item {
background: white;
border-radius: 0.5rem;
padding: 12px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.checklist-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.checklist-item.completed {
opacity: 0.6;
background-color: #f9fafb;
}
.checkbox-custom {
width: 18px;
height: 18px;
border: 2px solid #d1d5db;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.checkbox-custom.checked {
background-color: var(--success);
border-color: var(--success);
color: white;
}
/* 반응형 디자인 */
@media (max-width: 768px) {
.desktop-view {
display: none !important;
}
.daily-view {
display: block !important;
}
.calendar-day {
min-height: 80px;
padding: 4px;
}
.day-item {
font-size: 9px;
padding: 1px 3px;
}
/* 모바일 업로드 모달 */
.desktop-upload {
display: none !important;
}
.mobile-upload {
display: block !important;
}
}
@media (min-width: 769px) {
/* 데스크톱 업로드 모달 */
.mobile-upload {
display: none !important;
}
.desktop-upload {
display: block !important;
}
}
@media (max-width: 640px) {
.calendar-day {
min-height: 60px;
padding: 2px;
}
.day-number {
font-size: 12px;
}
}
</style>
</head>
<body>
<div class="min-h-screen">
<!-- 헤더 -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<button onclick="goBack()" class="mr-4 text-gray-500 hover:text-gray-700">
<i class="fas fa-arrow-left text-xl"></i>
</button>
<i class="fas fa-chart-line text-2xl text-blue-500 mr-3"></i>
<h1 class="text-xl font-semibold text-gray-800">대시보드</h1>
<span class="ml-3 text-sm text-gray-500" id="currentDate"></span>
</div>
<div class="flex items-center space-x-4">
<!-- 인덱스 페이지 이동 배너 -->
<button onclick="goToIndex()" class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-blue-600 hover:to-indigo-700 transition-all transform hover:scale-105">
<i class="fas fa-home mr-2"></i>Todo 관리
</button>
<!-- 업로드 버튼 (강조) -->
<button onclick="openUploadModal()" class="bg-gradient-to-r from-green-500 to-emerald-600 text-white px-6 py-2 rounded-lg text-sm font-medium hover:from-green-600 hover:to-emerald-700 transition-all transform hover:scale-105 shadow-lg">
<i class="fas fa-cloud-upload-alt mr-2"></i>업로드
</button>
<div class="h-6 w-px bg-gray-300"></div>
<button onclick="goToToday()" class="text-sm text-blue-600 hover:text-blue-800">
<i class="fas fa-calendar-day mr-1"></i>오늘
</button>
<div class="h-6 w-px bg-gray-300"></div>
<span class="text-sm text-gray-600" id="currentUser"></span>
<button onclick="logout()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
</div>
</div>
</header>
<!-- 메인 컨텐츠 -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- 데스크톱 뷰: 캘린더 중심 레이아웃 -->
<div class="desktop-view">
<!-- 상단 통계 카드 -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8">
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl p-4 border border-blue-200">
<div class="flex items-center">
<div class="p-2 bg-blue-500 rounded-lg">
<i class="fas fa-tasks text-white text-lg"></i>
</div>
<div class="ml-3">
<p class="text-xs font-medium text-blue-600">전체 Todo</p>
<p class="text-xl font-bold text-blue-900" id="totalTodos">0</p>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-xl p-4 border border-green-200">
<div class="flex items-center">
<div class="p-2 bg-green-500 rounded-lg">
<i class="fas fa-check-circle text-white text-lg"></i>
</div>
<div class="ml-3">
<p class="text-xs font-medium text-green-600">완료됨</p>
<p class="text-xl font-bold text-green-900" id="completedTodos">0</p>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-xl p-4 border border-orange-200">
<div class="flex items-center">
<div class="p-2 bg-orange-500 rounded-lg">
<i class="fas fa-clock text-white text-lg"></i>
</div>
<div class="ml-3">
<p class="text-xs font-medium text-orange-600">진행 중</p>
<p class="text-xl font-bold text-orange-900" id="inProgressTodos">0</p>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl p-4 border border-purple-200">
<div class="flex items-center">
<div class="p-2 bg-purple-500 rounded-lg">
<i class="fas fa-calendar-day text-white text-lg"></i>
</div>
<div class="ml-3">
<p class="text-xs font-medium text-purple-600">오늘 할 일</p>
<p class="text-xl font-bold text-purple-900" id="todayTodos">0</p>
</div>
</div>
</div>
<!-- 체크리스트 카드 (이미지 미리보기 포함) -->
<div class="bg-gradient-to-br from-teal-50 to-teal-100 rounded-xl p-4 border border-teal-200">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="p-2 bg-teal-500 rounded-lg">
<i class="fas fa-check-square text-white text-lg"></i>
</div>
<div class="ml-3">
<p class="text-xs font-medium text-teal-600">체크리스트</p>
<p class="text-xl font-bold text-teal-900" id="checklistCount">0</p>
</div>
</div>
<!-- 이미지 미리보기 영역 -->
<div id="checklistImagePreview" class="hidden">
<div class="flex -space-x-1">
<!-- 최대 3개의 이미지 썸네일이 여기에 표시됩니다 -->
</div>
</div>
</div>
</div>
</div>
<!-- 캘린더 섹션 (메인) -->
<div class="bg-white rounded-xl shadow-lg border border-gray-200 mb-8">
<!-- 캘린더 헤더 -->
<div class="border-b border-gray-200 p-6">
<div class="flex justify-between items-center">
<div class="flex items-center space-x-4">
<button onclick="previousMonth()" class="p-3 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors">
<i class="fas fa-chevron-left text-lg"></i>
</button>
<h2 class="text-3xl font-bold text-gray-800" id="currentMonth"></h2>
<button onclick="nextMonth()" class="p-3 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors">
<i class="fas fa-chevron-right text-lg"></i>
</button>
</div>
<div class="flex items-center space-x-6">
<div class="flex items-center space-x-2 text-sm">
<div class="w-4 h-4 bg-blue-200 border-l-4 border-blue-500 rounded-sm"></div>
<span class="text-gray-600 font-medium">Todo</span>
</div>
<div class="flex items-center space-x-2 text-sm">
<div class="w-4 h-4 bg-yellow-200 border-l-4 border-yellow-500 rounded-sm"></div>
<span class="text-gray-600 font-medium">캘린더</span>
</div>
<div class="flex items-center space-x-2 text-sm">
<div class="w-4 h-4 bg-green-200 border-l-4 border-green-500 rounded-sm"></div>
<span class="text-gray-600 font-medium">체크리스트</span>
</div>
</div>
</div>
</div>
<!-- 캘린더 그리드 -->
<div class="p-6">
<div class="calendar-grid" id="calendarGrid">
<!-- 요일 헤더 -->
<div class="calendar-header"></div>
<div class="calendar-header"></div>
<div class="calendar-header"></div>
<div class="calendar-header"></div>
<div class="calendar-header"></div>
<div class="calendar-header"></div>
<div class="calendar-header"></div>
<!-- 캘린더 날짜들이 여기에 동적으로 추가됩니다 -->
</div>
</div>
</div>
<!-- 체크리스트 섹션 (개선된 버전) -->
<div class="bg-white rounded-xl shadow-lg border border-gray-200">
<!-- 체크리스트 헤더 -->
<div class="border-b border-gray-200 p-6">
<div class="flex justify-between items-center">
<div class="flex items-center">
<div class="p-2 bg-green-100 rounded-lg mr-3">
<i class="fas fa-check-square text-green-600 text-lg"></i>
</div>
<div>
<h3 class="text-xl font-bold text-gray-800">체크리스트</h3>
<p class="text-sm text-gray-500">업로드된 항목들 • Todo나 캘린더로 변경 가능</p>
</div>
</div>
<div class="text-right">
<div class="text-sm text-gray-600 mb-1">진행률</div>
<div class="flex items-center space-x-2">
<div class="w-24 bg-gray-200 rounded-full h-2">
<div id="checklistProgressBar" class="bg-green-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<span id="checklistProgress" class="text-sm font-medium text-gray-700">0/0</span>
</div>
</div>
</div>
</div>
<!-- 체크리스트 내용 -->
<div class="p-6">
<div id="checklistItems" class="space-y-3 max-h-96 overflow-y-auto">
<!-- 체크리스트 항목들이 여기에 추가됩니다 -->
<div class="text-center py-8 text-gray-500">
<i class="fas fa-clipboard-list text-4xl mb-3 opacity-50"></i>
<p>아직 체크리스트 항목이 없습니다.</p>
<p class="text-sm">위의 업로드 버튼을 클릭해서 새로운 항목을 추가해보세요!</p>
<p class="text-xs mt-2 text-gray-400">추가된 항목은 여기서 Todo나 캘린더로 변경할 수 있습니다.</p>
</div>
</div>
</div>
</div>
</div>
<!-- 모바일 뷰 -->
<div class="daily-view">
<!-- 오늘 날짜 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-800" id="todayDate"></h2>
<p class="text-gray-600" id="todayWeekday"></p>
</div>
</div>
<!-- 오늘의 일정 -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-calendar-day text-blue-500 mr-2"></i>오늘의 일정
</h3>
<div id="todayItems">
<!-- 오늘의 항목들이 여기에 추가됩니다 -->
</div>
</div>
<!-- 모바일 체크리스트 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800">
<i class="fas fa-check-square text-green-500 mr-2"></i>체크리스트
</h3>
<div class="text-sm text-gray-600">
<span id="mobileChecklistProgress">0/0</span>
</div>
</div>
<div id="mobileChecklistItems">
<!-- 모바일 체크리스트 항목들 -->
</div>
</div>
</div>
</main>
</div>
<!-- 업로드 모달 -->
<div id="uploadModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-xl shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<!-- 모달 헤더 -->
<div class="flex justify-between items-center p-6 border-b">
<h3 class="text-lg font-semibold text-gray-800">
<i class="fas fa-plus-circle text-blue-500 mr-2"></i>새 항목 등록
</h3>
<button onclick="closeUploadModal()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 모달 내용 -->
<div class="p-6">
<form id="uploadForm" class="space-y-4">
<!-- 메모 입력 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">메모</label>
<input type="text" id="uploadContent" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="메모를 입력하세요..." required>
</div>
<!-- 사진 업로드 영역 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">사진 (선택사항)</label>
<!-- 데스크톱용 파일 선택 -->
<div class="desktop-upload">
<div class="border-2 border-dashed border-gray-200 rounded-lg p-6 text-center hover:border-blue-300 transition-colors">
<i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-3"></i>
<p class="text-gray-600 mb-2">파일을 선택하거나 드래그하여 업로드</p>
<button type="button" onclick="selectFile()" class="text-blue-600 hover:text-blue-800 font-medium">
파일 선택
</button>
<input type="file" id="desktopFileInput" accept="image/*" multiple class="hidden">
</div>
</div>
<!-- 모바일용 카메라/갤러리 선택 -->
<div class="mobile-upload hidden">
<div class="grid grid-cols-2 gap-3">
<button type="button" onclick="openCamera()" class="border-2 border-dashed border-gray-200 rounded-lg p-4 text-center hover:border-blue-300 transition-colors">
<i class="fas fa-camera text-2xl text-gray-400 mb-2"></i>
<p class="text-sm text-gray-600">카메라</p>
</button>
<button type="button" onclick="openGallery()" class="border-2 border-dashed border-gray-200 rounded-lg p-4 text-center hover:border-blue-300 transition-colors">
<i class="fas fa-images text-2xl text-gray-400 mb-2"></i>
<p class="text-sm text-gray-600">갤러리</p>
</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="photoPreview" class="hidden mt-4">
<div class="relative">
<img id="previewImage" class="w-full h-48 object-cover rounded-lg" alt="미리보기">
<button type="button" onclick="removePhoto()" class="absolute top-2 right-2 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600">
<i class="fas fa-times text-sm"></i>
</button>
</div>
<div class="mt-2 text-sm text-gray-600" id="photoInfo"></div>
</div>
</div>
<!-- 버튼 -->
<div class="flex space-x-3 pt-4">
<button type="submit" class="btn-primary flex-1 py-3 px-4 rounded-lg font-medium">
<i class="fas fa-plus mr-2"></i>등록하기
</button>
<button type="button" onclick="closeUploadModal()" class="px-4 py-3 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">
취소
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 로딩 오버레이 -->
<div id="loadingOverlay" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-60">
<div class="bg-white rounded-lg p-6 flex items-center space-x-3">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span class="text-gray-700">처리 중...</span>
</div>
</div>
<!-- JavaScript -->
<script src="static/js/api.js?v=20250921110800"></script>
<script src="static/js/auth.js?v=20250921110800"></script>
<script src="static/js/image-utils.js?v=20250921110800"></script>
<script>
let currentDate = new Date();
let calendarData = {};
let checklistData = [];
// 페이지 초기화
document.addEventListener('DOMContentLoaded', () => {
initializeDashboard();
});
// 대시보드 초기화
async function initializeDashboard() {
// 인증 상태 확인
const token = localStorage.getItem('authToken');
const user = localStorage.getItem('currentUser');
console.log('대시보드 초기화 시작...');
console.log('토큰 존재:', token ? '있음' : '없음');
console.log('사용자 정보:', user);
if (!token) {
console.log('인증 토큰이 없습니다. 로그인 페이지로 이동합니다.');
window.location.href = 'index.html';
return;
}
updateCurrentDate();
await loadCalendarData();
await loadChecklistData();
renderCalendar();
renderDailyView();
renderChecklist();
}
// 현재 날짜 업데이트
function updateCurrentDate() {
const now = new Date();
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
document.getElementById('currentDate').textContent = now.toLocaleDateString('ko-KR', {
month: 'long',
day: 'numeric'
});
document.getElementById('currentMonth').textContent = currentDate.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long'
});
document.getElementById('todayDate').textContent = now.toLocaleDateString('ko-KR', {
month: 'long',
day: 'numeric'
});
document.getElementById('todayWeekday').textContent = now.toLocaleDateString('ko-KR', {
weekday: 'long'
});
}
// 캘린더 데이터 로드
async function loadCalendarData() {
try {
console.log('캘린더 데이터 로딩 시작...');
// 인증 토큰 확인
const token = localStorage.getItem('authToken');
if (!token) {
console.error('인증 토큰이 없습니다!');
window.location.href = 'index.html';
return;
}
console.log('인증 토큰 존재:', token.substring(0, 20) + '...');
// API에서 Todo 데이터 가져오기
const todos = await TodoAPI.getTodos();
console.log('API에서 받은 Todo 데이터:', todos);
// 캘린더 형식으로 변환
calendarData = {};
todos.forEach(todo => {
if (todo.due_date) {
// 모든 날짜를 한국 시간 기준으로 처리
let dateStr;
// ISO 문자열을 Date 객체로 파싱
const date = new Date(todo.due_date);
// 한국 시간으로 변환하여 YYYY-MM-DD 형식으로 표시
dateStr = date.toLocaleDateString('en-CA', {
timeZone: 'Asia/Seoul'
});
console.log(`🔍 Todo "${todo.title}":`,
'category:', todo.category,
'due_date:', todo.due_date,
'parsed_date:', date.toISOString(),
'kst_display_date:', dateStr
);
if (!calendarData[dateStr]) {
calendarData[dateStr] = [];
}
// 카테고리에 따라 타입 설정
let itemType = 'todo';
if (todo.category === 'calendar') itemType = 'calendar';
else if (todo.category === 'checklist') itemType = 'checklist';
calendarData[dateStr].push({
id: todo.id,
type: itemType,
title: todo.title,
time: '', // 시간 정보 제거
priority: todo.priority,
status: todo.status
});
}
});
console.log('캘린더 데이터 로드 완료:');
Object.keys(calendarData).forEach(date => {
console.log(`📅 ${date}:`, calendarData[date].map(item => `${item.title} (${item.type})`));
});
} catch (error) {
console.error('캘린더 데이터 로드 실패:', error);
console.error('오류 상세:', error.message);
// 오류 시 빈 데이터로 초기화
calendarData = {};
}
}
// 체크리스트 데이터 로드
async function loadChecklistData() {
try {
console.log('체크리스트 데이터 로딩 시작...');
// 인증 토큰 확인
const token = localStorage.getItem('authToken');
if (!token) {
console.error('체크리스트: 인증 토큰이 없습니다!');
return;
}
// API에서 체크리스트 타입의 Todo 데이터 가져오기
const todos = await TodoAPI.getTodos();
console.log('체크리스트용 Todo 데이터:', todos);
// 체크리스트 항목들의 원본 데이터 확인
todos.filter(todo => todo.category === 'checklist').forEach((todo, index) => {
console.log(`🔍 원본 API 데이터 ${index + 1}:`, {
id: todo.id,
title: todo.title,
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' && todo.status !== 'completed')
.map(todo => ({
id: todo.id,
title: todo.title,
description: todo.description,
created_at: todo.created_at,
completed: todo.status === 'completed',
image_urls: todo.image_urls || [] // 다중 이미지 URL 배열
}));
console.log('체크리스트 데이터 로드 완료:', checklistData);
// 체크리스트 카운트 업데이트
const checklistCountEl = document.getElementById('checklistCount');
if (checklistCountEl) {
checklistCountEl.textContent = checklistData.length;
}
// 체크리스트 이미지 미리보기 업데이트
updateChecklistImagePreview();
// 체크리스트 렌더링
renderChecklist();
} catch (error) {
console.error('체크리스트 데이터 로드 실패:', error);
console.error('오류 상세:', error.message);
// 오류 시 빈 데이터로 초기화
checklistData = [];
// 오류 시 카운트를 0으로 설정
const checklistCountEl = document.getElementById('checklistCount');
if (checklistCountEl) {
checklistCountEl.textContent = '0';
}
// 오류 시 이미지 미리보기 숨기기
const previewContainer = document.getElementById('checklistImagePreview');
if (previewContainer) {
previewContainer.classList.add('hidden');
}
}
}
// 캘린더 렌더링
function renderCalendar() {
const calendarGrid = document.getElementById('calendarGrid');
if (!calendarGrid) return;
// 기존 날짜 요소들 제거 (헤더는 유지)
const existingDays = calendarGrid.querySelectorAll('.calendar-day');
existingDays.forEach(day => day.remove());
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// 월의 첫 번째 날과 마지막 날
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// 캘린더 시작일 (이전 달의 마지막 주 포함)
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay());
// 캘린더 종료일 (다음 달의 첫 주 포함)
const endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()));
const today = new Date();
for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) {
// 한국 시간 기준으로 날짜 문자열 생성 (calendarData 키와 일치시키기 위해)
const dateStr = date.toLocaleDateString('en-CA', {
timeZone: 'Asia/Seoul'
});
const isCurrentMonth = date.getMonth() === month;
const isToday = date.toDateString() === today.toDateString();
const dayData = calendarData[dateStr] || [];
console.log(`📆 캘린더 날짜: ${date.getDate()}일, dateStr: ${dateStr}, 데이터 개수: ${dayData.length}`);
// 날짜 요소 생성
const dayElement = document.createElement('div');
dayElement.className = `calendar-day ${!isCurrentMonth ? 'other-month' : ''} ${isToday ? 'today' : ''}`;
dayElement.onclick = () => selectDate(dateStr);
// 날짜 번호
const dayNumber = document.createElement('div');
dayNumber.className = 'day-number';
dayNumber.textContent = date.getDate();
dayElement.appendChild(dayNumber);
// 날짜 항목들
const dayItems = document.createElement('div');
dayItems.className = 'day-items';
dayData.forEach(item => {
const priorityClass = item.priority || 'medium';
const statusClass = item.status === 'completed' ? 'completed' : '';
const itemElement = document.createElement('div');
itemElement.className = `day-item ${item.type} ${priorityClass} ${statusClass}`;
itemElement.title = item.title; // 시간 정보 제거
itemElement.textContent = item.title;
itemElement.style.cursor = 'pointer';
itemElement.onclick = (e) => {
e.stopPropagation();
showItemDetailModal(item.id, item.type);
};
dayItems.appendChild(itemElement);
});
dayElement.appendChild(dayItems);
calendarGrid.appendChild(dayElement);
}
}
// 일일 뷰 렌더링 (모바일)
function renderDailyView() {
const todayItems = document.getElementById('todayItems');
if (!todayItems) return;
// 오늘 날짜를 한국 시간 기준으로 가져오기
const today = new Date().toLocaleDateString('en-CA', {
timeZone: 'Asia/Seoul'
});
const todayData = calendarData[today] || [];
console.log('🗓️ 오늘 날짜 (KST):', today, '오늘의 데이터:', todayData);
if (todayData.length === 0) {
todayItems.innerHTML = `
<div class="text-center py-8 text-gray-500">
<i class="fas fa-calendar-day text-3xl mb-3 opacity-50"></i>
<p>오늘 예정된 일정이 없습니다.</p>
</div>
`;
return;
}
todayItems.innerHTML = todayData.map(item => `
<div class="daily-item p-4">
<div class="flex items-center space-x-3">
<div class="time-indicator ${item.type}"></div>
<div class="flex-1">
<h4 class="font-medium text-gray-900">${item.title}</h4>
<p class="text-sm text-gray-600">
<i class="fas fa-calendar mr-1"></i>${item.type === 'todo' ? '시작일' : '마감일'}
</p>
</div>
<div class="text-xs px-2 py-1 rounded-full ${item.type === 'todo' ? 'bg-blue-100 text-blue-800' : 'bg-yellow-100 text-yellow-800'}">
${item.type === 'todo' ? 'Todo' : '캘린더'}
</div>
</div>
</div>
`).join('');
}
// 체크리스트 렌더링
function renderChecklist() {
console.log('🎯 체크리스트 렌더링 시작, 데이터:', checklistData);
const checklistItems = document.getElementById('checklistItems');
const mobileChecklistItems = document.getElementById('mobileChecklistItems');
const checklistProgress = document.getElementById('checklistProgress');
const mobileChecklistProgress = document.getElementById('mobileChecklistProgress');
const completed = checklistData.filter(item => item.completed).length;
const total = checklistData.length;
const progressText = `${completed}/${total} 완료`;
if (checklistProgress) checklistProgress.textContent = progressText;
if (mobileChecklistProgress) mobileChecklistProgress.textContent = `${completed}/${total}`;
const html = checklistData.map((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'
});
return `
<div class="checklist-item bg-white rounded-lg p-4 border border-gray-200 hover:border-gray-300 transition-colors">
<div class="flex items-start justify-between">
<!-- 내용 -->
<div class="flex-1 min-w-0 mr-4">
<div class="flex items-start space-x-3">
<!-- 이미지 썸네일 (있는 경우) -->
${item.image_urls && item.image_urls.length > 0 ? `
<div class="flex-shrink-0">
<div class="flex space-x-1">
${item.image_urls.slice(0, 2).map(url => `
<img src="${url}"
alt="첨부 이미지"
class="w-12 h-12 object-cover rounded border border-gray-200 cursor-pointer hover:border-gray-400 transition-colors"
onclick="showImagePreview('${url}', '${item.title}')"
title="클릭하여 크게 보기">
`).join('')}
${item.image_urls.length > 2 ? `
<div class="w-12 h-12 bg-gray-100 rounded flex items-center justify-center border border-gray-200">
<span class="text-xs text-gray-500">+${item.image_urls.length - 2}</span>
</div>
` : ''}
</div>
</div>
` : ''}
<!-- 텍스트 내용 -->
<div class="flex-1 min-w-0">
<h4 class="text-gray-900 font-medium mb-1">${item.title}</h4>
${item.description ? `<p class="text-sm text-gray-600 mb-2">${item.description}</p>` : ''}
<div class="flex items-center space-x-3 text-sm text-gray-500">
<span>등록: ${formatDate(item.created_at)}</span>
${item.image_urls && item.image_urls.length > 0 ? `<span class="flex items-center text-blue-600"><i class="fas fa-image mr-1"></i>이미지 ${item.image_urls.length}개</span>` : ''}
</div>
</div>
</div>
</div>
<!-- 액션 버튼들 -->
<div class="flex items-center space-x-2">
<button onclick="completeItem('${item.id}')"
class="px-3 py-1 bg-green-100 text-green-700 rounded-md text-sm font-medium hover:bg-green-200 transition-colors"
title="완료 처리">
<i class="fas fa-check mr-1"></i>완료
</button>
<button onclick="convertToTodo('${item.id}')"
class="px-3 py-1 bg-blue-100 text-blue-700 rounded-md text-sm font-medium hover:bg-blue-200 transition-colors"
title="Todo로 변경">
<i class="fas fa-calendar-day mr-1"></i>Todo
</button>
<button onclick="convertToCalendar('${item.id}')"
class="px-3 py-1 bg-orange-100 text-orange-700 rounded-md text-sm font-medium hover:bg-orange-200 transition-colors"
title="캘린더로 변경">
<i class="fas fa-calendar-times mr-1"></i>캘린더
</button>
</div>
</div>
</div>
`;
}).join('');
console.log('🎨 생성된 HTML 길이:', html.length);
console.log('🎨 HTML 미리보기:', html.substring(0, 200) + '...');
if (checklistItems) {
checklistItems.innerHTML = html;
console.log('✅ 데스크톱 체크리스트 업데이트 완료');
}
if (mobileChecklistItems) {
mobileChecklistItems.innerHTML = html;
console.log('✅ 모바일 체크리스트 업데이트 완료');
}
}
// 항목 완료 처리
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'
});
} 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 = `
<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);
}
// Todo로 변경
function convertToTodo(itemId) {
const item = checklistData.find(item => item.id === itemId);
if (item) {
showConversionModal(itemId, 'todo', item.title, item.description || '');
}
}
// 캘린더로 변경
function convertToCalendar(itemId) {
const item = checklistData.find(item => item.id === itemId);
if (item) {
showConversionModal(itemId, 'calendar', item.title, item.description || '');
}
}
// 변환 모달 표시
function showConversionModal(itemId, targetCategory, currentTitle, currentDescription) {
const categoryInfo = {
'todo': { name: 'Todo', icon: 'calendar-day', color: 'blue', dateLabel: '시작일' },
'calendar': { name: '캘린더', icon: 'calendar-times', color: 'orange', dateLabel: '마감일' }
};
const info = categoryInfo[targetCategory];
const modalHtml = `
<div id="conversionModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-xl shadow-xl max-w-md w-full">
<!-- 모달 헤더 -->
<div class="flex justify-between items-center p-6 border-b">
<h3 class="text-lg font-semibold text-gray-800">
<i class="fas fa-${info.icon} text-${info.color}-500 mr-2"></i>${info.name}로 변경
</h3>
<button onclick="closeConversionModal()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 모달 내용 -->
<div class="p-6">
<!-- 제목 입력 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">제목</label>
<input type="text" id="conversionTitle" value="${currentTitle}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-${info.color}-500 focus:border-${info.color}-500"
placeholder="제목을 입력하세요">
</div>
<!-- 설명 입력 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">설명 (선택사항)</label>
<textarea id="conversionDescription" rows="3"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-${info.color}-500 focus:border-${info.color}-500"
placeholder="상세 설명을 입력하세요">${currentDescription}</textarea>
</div>
<!-- 날짜 입력 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">${info.dateLabel}</label>
<input type="date" id="conversionDate"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-${info.color}-500 focus:border-${info.color}-500"
required>
</div>
<!-- 이미지 첨부 -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">이미지 첨부 (선택사항)</label>
<div class="border-2 border-dashed border-gray-200 rounded-lg p-4 text-center hover:border-gray-300 transition-colors">
<input type="file" id="conversionFile" class="hidden" accept="image/*" onchange="handleImageSelect(event)">
<div id="imageDropArea" onclick="document.getElementById('conversionFile').click()" class="cursor-pointer">
<i class="fas fa-image text-2xl text-gray-400 mb-2"></i>
<p class="text-sm text-gray-600">이미지를 선택하거나 드래그하여 업로드</p>
<p class="text-xs text-gray-500 mt-1">JPG, PNG, GIF 등 이미지 파일만 가능</p>
</div>
<div id="selectedImage" class="hidden mt-3">
<img id="imagePreview" class="max-w-full h-32 object-cover rounded border mx-auto mb-2">
<div class="flex items-center justify-between p-2 bg-gray-50 rounded border">
<span id="imageName" class="text-sm text-gray-700"></span>
<button type="button" onclick="removeSelectedImage()" class="text-red-500 hover:text-red-700">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 모달 푸터 -->
<div class="flex justify-end space-x-3 p-6 border-t bg-gray-50 rounded-b-xl">
<button onclick="closeConversionModal()"
class="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
취소
</button>
<button onclick="saveConversion('${itemId}', '${targetCategory}')"
class="px-4 py-2 text-white bg-${info.color}-500 rounded-lg hover:bg-${info.color}-600">
${info.name}로 변경
</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 오늘 날짜를 기본값으로 설정
const today = new Date().toISOString().split('T')[0];
document.getElementById('conversionDate').value = today;
// 모달 외부 클릭 시 닫기
const modal = document.getElementById('conversionModal');
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeConversionModal();
}
});
// ESC 키로 모달 닫기
const handleEscKey = (e) => {
if (e.key === 'Escape') {
closeConversionModal();
document.removeEventListener('keydown', handleEscKey);
}
};
document.addEventListener('keydown', handleEscKey);
}
// 변환 모달 닫기
function closeConversionModal() {
const modal = document.getElementById('conversionModal');
if (modal) {
modal.remove();
}
}
// 변환 저장
async function saveConversion(itemId, targetCategory) {
const title = document.getElementById('conversionTitle').value.trim();
const description = document.getElementById('conversionDescription').value.trim();
const date = document.getElementById('conversionDate').value;
const fileInput = document.getElementById('conversionFile');
if (!title) {
alert('제목을 입력해주세요.');
return;
}
if (!date) {
alert('날짜를 선택해주세요.');
return;
}
try {
// 기존 항목의 이미지 정보 가져오기
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];
// 이미지 파일인지 다시 한번 확인
if (!file.type.startsWith('image/')) {
alert('이미지 파일만 업로드 가능합니다.');
return;
}
// Base64로 인코딩하여 저장
newImageUrl = await convertFileToBase64(file);
console.log('🖼️ 새 이미지 추가:', newImageUrl ? '성공' : '실패');
}
const updateData = {
title: title,
description: description,
category: targetCategory
};
// 날짜 설정 (한국 시간 기준으로 저장)
if (date) {
// 선택된 날짜를 그대로 한국 시간으로 저장
const kstDateTime = date + 'T00:00:00+09:00'; // 한국 시간 문자열
updateData.due_date = kstDateTime;
console.log('변환 모달 날짜 설정 - 선택된 날짜 (KST):', date, '저장:', kstDateTime);
}
// 이미지 배열 처리: 기존 이미지 + 새 이미지 (최대 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);
const categoryNames = {
'todo': 'Todo',
'calendar': '캘린더'
};
alert(`항목이 ${categoryNames[targetCategory]}로 변경되었습니다!`);
closeConversionModal();
await initializeDashboard();
} catch (error) {
console.error('변환 저장 실패:', error);
alert('변환에 실패했습니다.');
}
}
// 이미지 선택 처리
function handleImageSelect(event) {
const file = event.target.files[0];
if (file) {
// 이미지 파일인지 확인
if (!file.type.startsWith('image/')) {
alert('이미지 파일만 업로드 가능합니다.');
event.target.value = '';
return;
}
const imageName = document.getElementById('imageName');
const selectedImage = document.getElementById('selectedImage');
const imageDropArea = document.getElementById('imageDropArea');
const imagePreview = document.getElementById('imagePreview');
imageName.textContent = file.name;
selectedImage.classList.remove('hidden');
imageDropArea.style.display = 'none';
// 이미지 미리보기 생성
const reader = new FileReader();
reader.onload = function(e) {
imagePreview.src = e.target.result;
};
reader.readAsDataURL(file);
}
}
// 선택된 이미지 제거
function removeSelectedImage() {
const fileInput = document.getElementById('conversionFile');
const selectedImage = document.getElementById('selectedImage');
const imageDropArea = document.getElementById('imageDropArea');
const imagePreview = document.getElementById('imagePreview');
fileInput.value = '';
selectedImage.classList.add('hidden');
imageDropArea.style.display = 'block';
imagePreview.src = '';
}
// 파일을 Base64로 변환
function convertFileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// 파일 다운로드
function downloadFile(fileUrl, fileName) {
if (!fileUrl) {
alert('다운로드할 파일이 없습니다.');
return;
}
const link = document.createElement('a');
link.href = fileUrl;
link.download = fileName || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// 항목 상세 정보 모달 표시
async function showItemDetailModal(itemId, itemType) {
try {
console.log('상세 정보 모달 표시:', itemId, itemType);
// API에서 상세 정보 가져오기
const item = await TodoAPI.getTodoById(itemId);
console.log('상세 정보:', item);
const categoryInfo = {
'todo': { name: 'Todo', icon: 'calendar-day', color: 'blue' },
'calendar': { name: '캘린더', icon: 'calendar-times', color: 'orange' },
'checklist': { name: '체크리스트', icon: 'check-square', color: 'green' }
};
const info = categoryInfo[item.category] || categoryInfo['todo'];
// 이미지 파일 확인
const hasImages = item.image_urls && item.image_urls.length > 0;
// 첨부 파일 확인 및 타입 분류
const hasFile = item.file_url && item.file_name;
let fileType = 'unknown';
let fileIcon = 'fa-file';
let fileColor = 'gray';
if (hasFile) {
const fileName = item.file_name.toLowerCase();
const fileExtension = fileName.split('.').pop();
// 이미지 파일
if (/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(fileName) || item.file_url.includes('data:image/')) {
fileType = 'image';
fileIcon = 'fa-image';
fileColor = 'green';
}
// PDF 파일
else if (/\.pdf$/i.test(fileName)) {
fileType = 'pdf';
fileIcon = 'fa-file-pdf';
fileColor = 'red';
}
// Excel 파일
else if (/\.(xlsx?|xls|csv)$/i.test(fileName)) {
fileType = 'excel';
fileIcon = 'fa-file-excel';
fileColor = 'green';
}
// Word 파일
else if (/\.(docx?|doc)$/i.test(fileName)) {
fileType = 'word';
fileIcon = 'fa-file-word';
fileColor = 'blue';
}
// PowerPoint 파일
else if (/\.(pptx?|ppt)$/i.test(fileName)) {
fileType = 'powerpoint';
fileIcon = 'fa-file-powerpoint';
fileColor = 'orange';
}
// 텍스트 파일
else if (/\.(txt|md|json|xml|html|css|js|py|java|cpp|c|h)$/i.test(fileName)) {
fileType = 'text';
fileIcon = 'fa-file-alt';
fileColor = 'blue';
}
// 압축 파일
else if (/\.(zip|rar|7z|tar|gz)$/i.test(fileName)) {
fileType = 'archive';
fileIcon = 'fa-file-archive';
fileColor = 'purple';
}
// 기타 파일
else {
fileType = 'other';
fileIcon = 'fa-file';
fileColor = 'gray';
}
}
const modalHtml = `
<div id="itemDetailModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<!-- 모달 헤더 -->
<div class="flex justify-between items-center p-6 border-b bg-${info.color}-50">
<div class="flex items-center">
<div class="p-2 bg-${info.color}-100 rounded-lg mr-3">
<i class="fas fa-${info.icon} text-${info.color}-600 text-lg"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800">${info.name} 상세 정보</h3>
<p class="text-sm text-gray-600">생성일: ${formatDate(item.created_at)}</p>
</div>
</div>
<button onclick="closeItemDetailModal()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 모달 내용 -->
<div class="p-6">
<!-- 제목 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">제목</label>
<input type="text" id="detailTitle" value="${item.title}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-${info.color}-500 focus:border-${info.color}-500">
</div>
<!-- 설명 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
<textarea id="detailDescription" rows="3"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-${info.color}-500 focus:border-${info.color}-500">${item.description || ''}</textarea>
</div>
<!-- 날짜 -->
${item.due_date ? `
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">${item.category === 'todo' ? '시작일' : '마감일'}</label>
<div class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg text-gray-800">
${(() => {
// 모든 날짜를 한국 시간 기준으로 처리
const date = new Date(item.due_date);
return date.toLocaleDateString('en-CA', {
timeZone: 'Asia/Seoul'
});
})()}
</div>
</div>
` : ''}
<!-- 이미지 미리보기 -->
${hasImages ? `
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">첨부 이미지 (${item.image_urls.length}개)</label>
<div class="border border-gray-200 rounded-lg p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
${item.image_urls.map((url, index) => `
<div class="relative">
<img src="${url}" alt="첨부 이미지 ${index + 1}"
class="w-full h-48 object-cover rounded-lg shadow-sm cursor-pointer hover:shadow-md transition-shadow"
onclick="showImagePreview('${url}', '${item.title} - 이미지 ${index + 1}')">
<div class="absolute top-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
${index + 1}/${item.image_urls.length}
</div>
</div>
`).join('')}
</div>
</div>
</div>
` : ''}
<!-- 첨부 파일 -->
${hasFile ? `
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">첨부 파일</label>
<div class="border border-gray-200 rounded-lg p-4">
${fileType === 'image' ? `
<!-- 이미지 미리보기 -->
<div class="mb-3">
<img src="${item.file_url}" alt="${item.file_name}"
class="max-w-full h-auto max-h-64 rounded-lg shadow-sm cursor-pointer"
onclick="window.open('${item.file_url}', '_blank')">
<p class="text-xs text-gray-500 mt-1 text-center">클릭하면 원본 크기로 볼 수 있습니다</p>
</div>
` : ''}
<!-- 파일 정보 및 다운로드 -->
<div class="flex items-center justify-between bg-gray-50 p-3 rounded-lg">
<div class="flex items-center">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-${fileColor}-100 mr-3">
<i class="fas ${fileIcon} text-${fileColor}-600 text-lg"></i>
</div>
<div>
<span class="text-sm font-medium text-gray-700 block">${item.file_name}</span>
<span class="text-xs text-gray-500">${(() => {
switch(fileType) {
case 'image': return '이미지 파일';
case 'pdf': return 'PDF 문서';
case 'excel': return 'Excel 파일';
case 'word': return 'Word 문서';
case 'powerpoint': return 'PowerPoint 파일';
case 'text': return '텍스트 파일';
case 'archive': return '압축 파일';
default: return '파일';
}
})()}</span>
</div>
</div>
<div class="flex space-x-2">
${fileType === 'image' ? `
<button onclick="window.open('${item.file_url}', '_blank')"
class="px-3 py-1 bg-green-100 text-green-700 rounded-md text-sm font-medium hover:bg-green-200 transition-colors">
<i class="fas fa-eye mr-1"></i>보기
</button>
` : ''}
<button onclick="downloadFile('${item.file_url}', '${item.file_name}')"
class="px-3 py-1 bg-blue-100 text-blue-700 rounded-md text-sm font-medium hover:bg-blue-200 transition-colors">
<i class="fas fa-download mr-1"></i>다운로드
</button>
</div>
</div>
</div>
</div>
` : ''}
</div>
<!-- 모달 푸터 -->
<div class="flex justify-between p-6 border-t bg-gray-50 rounded-b-xl">
<div class="flex flex-wrap gap-2">
${item.status !== 'completed' ? `
<button onclick="delayItem('${item.id}')"
class="px-4 py-2 text-orange-700 bg-orange-100 rounded-lg hover:bg-orange-200 transition-colors">
<i class="fas fa-clock mr-1"></i>지연
</button>
<button onclick="quickDelay('${item.id}', 3)"
class="px-3 py-2 text-orange-700 bg-orange-50 border border-orange-200 rounded-lg hover:bg-orange-100 transition-colors text-sm">
+3일
</button>
<button onclick="quickDelay('${item.id}', 5)"
class="px-3 py-2 text-orange-700 bg-orange-50 border border-orange-200 rounded-lg hover:bg-orange-100 transition-colors text-sm">
+5일
</button>
<button onclick="completeItemFromModal('${item.id}')"
class="px-4 py-2 text-green-700 bg-green-100 rounded-lg hover:bg-green-200 transition-colors">
<i class="fas fa-check mr-1"></i>완료
</button>
` : `
<button onclick="reopenItem('${item.id}')"
class="px-4 py-2 text-blue-700 bg-blue-100 rounded-lg hover:bg-blue-200 transition-colors">
<i class="fas fa-undo mr-1"></i>재오픈
</button>
`}
</div>
<div class="flex space-x-3">
<button onclick="closeItemDetailModal()"
class="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
취소
</button>
<button onclick="saveItemDetails('${item.id}')"
class="px-4 py-2 text-white bg-${info.color}-500 rounded-lg hover:bg-${info.color}-600">
저장
</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 모달 외부 클릭 시 닫기
const modal = document.getElementById('itemDetailModal');
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeItemDetailModal();
}
});
// ESC 키로 모달 닫기
const handleEscKey = (e) => {
if (e.key === 'Escape') {
closeItemDetailModal();
document.removeEventListener('keydown', handleEscKey);
}
};
document.addEventListener('keydown', handleEscKey);
} catch (error) {
console.error('상세 정보 로드 실패:', error);
alert('상세 정보를 불러오는데 실패했습니다.');
}
}
// 상세 정보 모달 닫기
function closeItemDetailModal() {
const modal = document.getElementById('itemDetailModal');
if (modal) {
modal.remove();
}
}
// 항목 상세 정보 저장
async function saveItemDetails(itemId) {
try {
const title = document.getElementById('detailTitle').value.trim();
const description = document.getElementById('detailDescription').value.trim();
if (!title) {
alert('제목을 입력해주세요.');
return;
}
const updateData = {
title: title,
description: description
};
await TodoAPI.updateTodo(itemId, updateData);
alert('변경사항이 저장되었습니다!');
closeItemDetailModal();
await initializeDashboard();
} catch (error) {
console.error('저장 실패:', error);
alert('저장에 실패했습니다.');
}
}
// 항목 지연 (날짜 선택)
function delayItem(itemId) {
console.log('지연 버튼 클릭됨:', itemId);
// 현재 날짜 가져오기 (한국 시간 기준)
const today = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' });
// 날짜 선택 모달 생성
const modalHtml = `
<div id="datePickerModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[70] p-4">
<div class="bg-white rounded-xl shadow-xl max-w-sm w-full">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4 text-center">날짜 선택</h3>
<div class="mb-4">
<input type="date" id="datePickerInput"
value="${today}"
min="${today}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 text-lg">
</div>
<div class="flex space-x-3">
<button onclick="closeDatePickerModal()"
class="flex-1 px-4 py-3 text-gray-600 bg-gray-200 rounded-lg hover:bg-gray-300 transition-colors">
취소
</button>
<button onclick="confirmDateChange('${itemId}')"
class="flex-1 px-4 py-3 text-white bg-orange-600 rounded-lg hover:bg-orange-700 transition-colors">
확인
</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 날짜 입력 필드에 포커스하여 달력 위젯 활성화
setTimeout(() => {
const input = document.getElementById('datePickerInput');
if (input) {
input.focus();
// 일부 브라우저에서는 클릭이 필요할 수 있음
input.click();
}
}, 100);
}
// 날짜 변경 확인
async function confirmDateChange(itemId) {
const dateInput = document.getElementById('datePickerInput');
const newDate = dateInput.value;
if (!newDate) {
alert('날짜를 선택해주세요.');
return;
}
try {
// 입력된 날짜를 그대로 한국 시간으로 저장
const kstDateTime = newDate + 'T00:00:00+09:00';
console.log('지연 설정 - 선택된 날짜 (KST):', newDate, '저장:', kstDateTime);
await TodoAPI.updateTodo(itemId, {
due_date: kstDateTime
});
alert(`날짜가 ${newDate}로 변경되었습니다.`);
closeDatePickerModal();
closeItemDetailModal();
await initializeDashboard();
} catch (error) {
console.error('지연 처리 실패:', error);
alert('지연 처리에 실패했습니다.');
}
}
// 날짜 선택 모달 닫기
function closeDatePickerModal() {
const modal = document.getElementById('datePickerModal');
if (modal) {
modal.remove();
}
}
// 빠른 지연 (3일, 5일)
async function quickDelay(itemId, days) {
try {
// 현재 날짜에서 지정된 일수만큼 더하기
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() + days);
// 한국 시간 기준으로 날짜 문자열 생성
const newDate = currentDate.toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' });
const kstDateTime = newDate + 'T00:00:00+09:00';
console.log(`빠른 지연 (+${days}일) - 새 날짜 (KST):`, newDate, '저장:', kstDateTime);
await TodoAPI.updateTodo(itemId, {
due_date: kstDateTime
});
alert(`일정이 ${days}일 지연되었습니다. (${newDate})`);
closeItemDetailModal();
await initializeDashboard();
} catch (error) {
console.error('빠른 지연 실패:', error);
alert('지연 처리에 실패했습니다.');
}
}
// 모달에서 완료 처리
async function completeItemFromModal(itemId) {
try {
await TodoAPI.updateTodo(itemId, {
status: 'completed'
});
alert('항목이 완료 처리되었습니다!');
closeItemDetailModal();
await initializeDashboard();
} catch (error) {
console.error('완료 처리 실패:', error);
alert('완료 처리에 실패했습니다.');
}
}
// 항목 재오픈
async function reopenItem(itemId) {
try {
await TodoAPI.updateTodo(itemId, {
status: 'pending'
});
alert('항목이 재오픈되었습니다!');
closeItemDetailModal();
await initializeDashboard();
} catch (error) {
console.error('재오픈 실패:', error);
alert('재오픈에 실패했습니다.');
}
}
// 날짜 포맷팅
function formatDate(dateString) {
if (!dateString) return '날짜 없음';
const date = new Date(dateString);
if (isNaN(date.getTime())) return '날짜 없음';
const now = new Date();
const diffTime = now - date;
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
if (diffHours < 1) return '방금 전';
if (diffHours < 24) return `${diffHours}시간 전`;
return date.toLocaleDateString('ko-KR', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// 이전 달
function previousMonth() {
currentDate.setMonth(currentDate.getMonth() - 1);
updateCurrentDate();
renderCalendar();
}
// 다음 달
function nextMonth() {
currentDate.setMonth(currentDate.getMonth() + 1);
updateCurrentDate();
renderCalendar();
}
// 오늘로 이동
function goToToday() {
currentDate = new Date();
updateCurrentDate();
renderCalendar();
renderDailyView();
}
// 날짜 선택
function selectDate(dateStr) {
console.log('선택된 날짜:', dateStr);
// TODO: 선택된 날짜의 상세 정보 표시
}
// 뒤로 가기
function goBack() {
window.location.href = 'index.html';
}
// 분류 센터로 이동
function goToClassify() {
window.location.href = 'classify.html';
}
// 업로드 모달 관련 변수
let currentPhotos = [];
// 업로드 모달 열기
function openUploadModal() {
document.getElementById('uploadModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
// 업로드 모달 닫기
function closeUploadModal() {
document.getElementById('uploadModal').classList.add('hidden');
document.body.style.overflow = 'auto';
clearUploadForm();
}
// 업로드 폼 초기화
function clearUploadForm() {
document.getElementById('uploadForm').reset();
removeAllPhotos();
}
// 파일 선택 (데스크톱)
function selectFile() {
document.getElementById('desktopFileInput').click();
}
// 카메라 열기 (모바일)
function openCamera() {
document.getElementById('cameraInput').click();
}
// 갤러리 열기 (모바일)
function openGallery() {
document.getElementById('galleryInput').click();
}
// 모든 사진 제거
function removeAllPhotos() {
currentPhotos = [];
updatePhotoPreview();
// 파일 입력 초기화
const inputs = ['desktopFileInput', 'cameraInput', 'galleryInput'];
inputs.forEach(id => {
const input = document.getElementById(id);
if (input) input.value = '';
});
}
// 개별 사진 제거
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;
// 최대 5개 제한 확인
const remainingSlots = 5 - currentPhotos.length;
if (remainingSlots <= 0) {
alert('최대 5개의 이미지만 업로드할 수 있습니다.');
return;
}
const filesToProcess = Array.from(files).slice(0, remainingSlots);
try {
showLoading(true);
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
});
}
updatePhotoPreview();
if (filesToProcess.length < files.length) {
alert(`최대 5개까지만 업로드됩니다. ${filesToProcess.length}개가 추가되었습니다.`);
}
} catch (error) {
console.error('이미지 처리 실패:', error);
alert('이미지 처리에 실패했습니다.');
} finally {
showLoading(false);
}
}
// 사진 미리보기 업데이트
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 = `
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">업로드된 이미지 (${currentPhotos.length}/5)</span>
<button type="button" onclick="removeAllPhotos()" class="text-red-600 hover:text-red-800 text-sm">
전체 삭제
</button>
</div>
<div class="grid grid-cols-2 gap-3">
${currentPhotos.map((photo, index) => `
<div class="relative">
<img src="${photo.data}" class="w-full h-24 object-cover rounded-lg" alt="${photo.name}">
<button type="button" onclick="removePhoto(${index})"
class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600">
<i class="fas fa-times text-xs"></i>
</button>
<div class="mt-1 text-xs text-gray-500 truncate">${photo.name}</div>
</div>
`).join('')}
</div>
</div>
`;
}
}
// 파일을 Base64로 변환
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// 업로드 폼 제출
async function handleUploadSubmit(event) {
event.preventDefault();
console.log('🚀 업로드 시작 - currentPhotos:', currentPhotos.length > 0 ? `${currentPhotos.length}개 이미지` : '이미지 없음');
const content = document.getElementById('uploadContent').value.trim();
if (!content) {
alert('메모를 입력해주세요.');
return;
}
try {
showLoading(true);
const itemData = {
content: content,
image_urls: currentPhotos.map(photo => photo.data),
created_at: new Date().toISOString()
};
// API 호출하여 항목 저장 (체크리스트로 바로 저장)
const todoData = {
title: content,
description: content,
category: 'checklist' // 체크리스트로 바로 저장
};
// 이미지가 있을 때만 image_urls 추가
if (currentPhotos.length > 0) {
todoData.image_urls = currentPhotos.map(photo => photo.data);
console.log('📸 이미지 데이터 포함:', {
hasImage: true,
imageCount: currentPhotos.length,
totalSize: currentPhotos.reduce((sum, photo) => sum + photo.size, 0)
});
} else {
console.log('📸 이미지 데이터 없음');
}
console.log('API 요청 데이터:', {
title: todoData.title,
description: todoData.description,
category: todoData.category,
hasImageUrl: !!todoData.image_url,
imageUrlLength: todoData.image_url ? todoData.image_url.length : 0
});
await TodoAPI.createTodo(todoData);
console.log('새 항목 등록 완료:', itemData);
// 성공 메시지
alert('체크리스트에 항목이 등록되었습니다!');
// 모달 닫기 및 데이터 새로고침
closeUploadModal();
await initializeDashboard();
} catch (error) {
console.error('항목 등록 실패:', error);
alert('항목 등록에 실패했습니다.');
} finally {
showLoading(false);
}
}
// 로딩 표시
function showLoading(show) {
const overlay = document.getElementById('loadingOverlay');
if (overlay) {
if (show) {
overlay.classList.remove('hidden');
} else {
overlay.classList.add('hidden');
}
}
}
// 이벤트 리스너 설정
document.addEventListener('DOMContentLoaded', () => {
// 파일 입력 이벤트 리스너
const fileInputs = ['desktopFileInput', 'cameraInput', 'galleryInput'];
fileInputs.forEach(id => {
const input = document.getElementById(id);
if (input) {
input.addEventListener('change', handlePhotoUpload);
}
});
// 업로드 폼 이벤트 리스너
const uploadForm = document.getElementById('uploadForm');
if (uploadForm) {
uploadForm.addEventListener('submit', handleUploadSubmit);
}
// 드래그 앤 드롭 (데스크톱)
const desktopUpload = document.querySelector('.desktop-upload');
if (desktopUpload) {
desktopUpload.addEventListener('dragover', (e) => {
e.preventDefault();
e.currentTarget.classList.add('border-blue-300');
});
desktopUpload.addEventListener('dragleave', (e) => {
e.preventDefault();
e.currentTarget.classList.remove('border-blue-300');
});
desktopUpload.addEventListener('drop', (e) => {
e.preventDefault();
e.currentTarget.classList.remove('border-blue-300');
const files = e.dataTransfer.files;
if (files.length > 0) {
const input = document.getElementById('desktopFileInput');
if (input) {
input.files = files;
handlePhotoUpload({ target: input });
}
}
});
}
// 모달 외부 클릭 시 닫기
const modal = document.getElementById('uploadModal');
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeUploadModal();
}
});
}
});
// 전역 함수 등록
window.previousMonth = previousMonth;
window.nextMonth = nextMonth;
window.goToToday = goToToday;
window.selectDate = selectDate;
window.goBack = goBack;
window.openUploadModal = openUploadModal;
window.closeUploadModal = closeUploadModal;
window.selectFile = selectFile;
window.openCamera = openCamera;
window.openGallery = openGallery;
window.removePhoto = removePhoto;
window.goToClassify = goToClassify;
window.completeItem = completeItem;
window.convertToTodo = convertToTodo;
window.convertToCalendar = convertToCalendar;
window.showConversionModal = showConversionModal;
window.closeConversionModal = closeConversionModal;
window.saveConversion = saveConversion;
window.handleImageSelect = handleImageSelect;
window.removeSelectedImage = removeSelectedImage;
window.downloadFile = downloadFile;
// 이미지 미리보기 모달 표시
function showImagePreview(imageUrl, title) {
const modalHtml = `
<div id="imagePreviewModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-[80] p-4">
<div class="relative max-w-4xl max-h-full">
<!-- 닫기 버튼 -->
<button onclick="closeImagePreview()"
class="absolute -top-10 right-0 text-white hover:text-gray-300 text-2xl z-10"
title="닫기">
<i class="fas fa-times"></i>
</button>
<!-- 이미지 -->
<img src="${imageUrl}"
alt="${title}"
class="max-w-full max-h-[80vh] object-contain rounded-lg shadow-2xl">
<!-- 제목 -->
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white p-4 rounded-b-lg">
<h3 class="text-lg font-medium">${title}</h3>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 모달 외부 클릭 시 닫기
const modal = document.getElementById('imagePreviewModal');
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeImagePreview();
}
});
// ESC 키로 모달 닫기
const handleEscKey = (e) => {
if (e.key === 'Escape') {
closeImagePreview();
document.removeEventListener('keydown', handleEscKey);
}
};
document.addEventListener('keydown', handleEscKey);
}
// 이미지 미리보기 모달 닫기
function closeImagePreview() {
const modal = document.getElementById('imagePreviewModal');
if (modal) {
modal.remove();
}
}
// 체크리스트 이미지 미리보기 업데이트
function updateChecklistImagePreview() {
const previewContainer = document.getElementById('checklistImagePreview');
if (!previewContainer) return;
// 이미지가 있는 체크리스트 항목들 필터링 (최대 3개)
const itemsWithImages = checklistData
.filter(item => item.image_url)
.slice(0, 3);
if (itemsWithImages.length === 0) {
previewContainer.classList.add('hidden');
return;
}
// 이미지 썸네일 HTML 생성
const thumbnailsHtml = itemsWithImages.map((item, index) => `
<div class="relative">
<img src="${item.image_url}"
alt="${item.title}"
class="w-8 h-8 object-cover rounded-full border-2 border-white shadow-sm cursor-pointer hover:scale-110 transition-transform"
onclick="showImagePreview('${item.image_url}', '${item.title}')"
title="${item.title}">
${index === 2 && checklistData.filter(item => item.image_url).length > 3 ? `
<div class="absolute inset-0 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
<span class="text-white text-xs font-bold">+${checklistData.filter(item => item.image_url).length - 3}</span>
</div>
` : ''}
</div>
`).join('');
previewContainer.querySelector('.flex').innerHTML = thumbnailsHtml;
previewContainer.classList.remove('hidden');
}
window.showImagePreview = showImagePreview;
window.closeImagePreview = closeImagePreview;
window.updateChecklistImagePreview = updateChecklistImagePreview;
window.showItemDetailModal = showItemDetailModal;
window.closeItemDetailModal = closeItemDetailModal;
window.saveItemDetails = saveItemDetails;
window.delayItem = delayItem;
window.confirmDateChange = confirmDateChange;
window.closeDatePickerModal = closeDatePickerModal;
window.quickDelay = quickDelay;
window.completeItemFromModal = completeItemFromModal;
window.reopenItem = reopenItem;
// 문서 클릭 시 메뉴 숨기기
document.addEventListener('click', (e) => {
if (!e.target.closest('.relative')) {
document.querySelectorAll('[id^="categoryMenu-"]').forEach(menu => {
menu.classList.add('hidden');
});
}
});
</script>
</body>
</html>