✨ 주요 기능 추가: - 다중 이미지 업로드 지원 (최대 5개) - 체크리스트 완료 시 자동 사라짐 기능 - 캘린더 상세 모달에서 다중 이미지 표시 - Todo 변환 시 기존 이미지 보존 및 새 이미지 추가 🔧 백엔드 수정: - Todo 모델: image_url → image_urls (JSON 배열) - API 엔드포인트: 다중 이미지 직렬화/역직렬화 - 새 엔드포인트: POST /todos/{id}/add-image (이미지 추가) - 데이터베이스 마이그레이션 스크립트 추가 🎨 프론트엔드 개선: - 대시보드: 실제 API 데이터 연동, 다중 이미지 표시 - 업로드 모달: 다중 파일 선택, 실시간 미리보기, 5개 제한 - 체크리스트: 완료 시 1.5초 후 자동 제거, 토스트 메시지 - 캘린더 모달: 2x2 그리드 이미지 표시, 클릭 확대 - Todo 변환: 기존 이미지 + 새 이미지 합치기 🐛 버그 수정: - currentPhoto 변수 오류 해결 - 이미지 표시 문제 (단일 → 다중 지원) - 완료 처리 로컬/백엔드 동기화 - 새로고침 시 완료 항목 재출현 문제
2326 lines
107 KiB
HTML
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>
|