Initial commit: Todo Project with dashboard, classification center, and upload functionality
- 📱 PWA 지원: 홈화면 추가 가능한 Progressive Web App - 🎨 M-Project 색상 스키마: 하늘색, 주황색, 회색, 흰색 일관된 디자인 - 📊 대시보드: 데스크톱 캘린더 뷰 + 모바일 일일 뷰 반응형 디자인 - 📥 분류 센터: Gmail 스타일 받은편지함으로 스마트 분류 시스템 - 🤖 AI 분류 제안: 키워드 기반 자동 분류 제안 및 일괄 처리 - 📷 업로드 모달: 데스크톱(파일 선택) + 모바일(카메라/갤러리) 최적화 - 🏷️ 3가지 분류: Todo(시작일), 캘린더(마감일), 체크리스트(무기한) - 📋 체크리스트: 진행률 표시 및 완료 토글 기능 - 🔄 시놀로지 연동 준비: 메일플러스 연동을 위한 구조 설계 - 📱 반응형 UI: 모든 페이지 모바일 최적화 완료
400
frontend/calendar.html
Normal file
@@ -0,0 +1,400 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>캘린더 - 마감 기한이 있는 일들</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);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: var(--warning);
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #d97706;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.calendar-item {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.calendar-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.deadline-urgent {
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.deadline-warning {
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.deadline-normal {
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
</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-calendar-times text-2xl text-orange-500 mr-3"></i>
|
||||
<h1 class="text-xl font-semibold text-gray-800">캘린더</h1>
|
||||
<span class="ml-3 text-sm text-gray-500">마감 기한이 있는 일들</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="goToDashboard()" class="text-blue-600 hover:text-blue-800 font-medium">
|
||||
<i class="fas fa-chart-line mr-1"></i>대시보드
|
||||
</button>
|
||||
<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-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 페이지 설명 -->
|
||||
<div class="bg-orange-50 rounded-xl p-6 mb-8">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-calendar-times text-2xl text-orange-600 mr-3"></i>
|
||||
<h2 class="text-xl font-semibold text-orange-900">캘린더 관리</h2>
|
||||
</div>
|
||||
<p class="text-orange-800 mb-4">
|
||||
마감 기한이 있는 일들을 관리합니다. 우선순위에 따라 계획적으로 진행해보세요.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-red-900 mb-1">🚨 긴급</div>
|
||||
<div class="text-red-700">3일 이내 마감</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-orange-900 mb-1">⚠️ 주의</div>
|
||||
<div class="text-orange-700">1주일 이내 마감</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-blue-900 mb-1">📅 여유</div>
|
||||
<div class="text-blue-700">1주일 이상 남음</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 및 정렬 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div class="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
||||
<button onclick="filterCalendar('all')" class="filter-tab active px-4 py-2 rounded text-sm font-medium">전체</button>
|
||||
<button onclick="filterCalendar('urgent')" class="filter-tab px-4 py-2 rounded text-sm font-medium">긴급</button>
|
||||
<button onclick="filterCalendar('warning')" class="filter-tab px-4 py-2 rounded text-sm font-medium">주의</button>
|
||||
<button onclick="filterCalendar('normal')" class="filter-tab px-4 py-2 rounded text-sm font-medium">여유</button>
|
||||
<button onclick="filterCalendar('completed')" class="filter-tab px-4 py-2 rounded text-sm font-medium">완료</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<label class="text-sm text-gray-600">정렬:</label>
|
||||
<select id="sortBy" class="border border-gray-300 rounded-lg px-3 py-1 text-sm">
|
||||
<option value="due_date">마감일 순</option>
|
||||
<option value="priority">우선순위 순</option>
|
||||
<option value="created_at">등록일 순</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 캘린더 목록 -->
|
||||
<div class="bg-white rounded-xl shadow-sm">
|
||||
<div class="p-6 border-b">
|
||||
<h3 class="text-lg font-semibold text-gray-800">
|
||||
<i class="fas fa-list text-orange-500 mr-2"></i>마감 기한별 목록
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div id="calendarList" class="divide-y divide-gray-100">
|
||||
<!-- 캘린더 항목들이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="p-12 text-center text-gray-500">
|
||||
<i class="fas fa-calendar-times text-4xl mb-4 opacity-50"></i>
|
||||
<p>아직 마감 기한이 설정된 일이 없습니다.</p>
|
||||
<p class="text-sm">메인 페이지에서 항목을 등록하고 마감 기한을 설정해보세요!</p>
|
||||
<button onclick="goBack()" class="mt-4 btn-warning px-6 py-2 rounded-lg">
|
||||
<i class="fas fa-arrow-left mr-2"></i>메인으로 돌아가기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="static/js/auth.js"></script>
|
||||
<script>
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuthStatus();
|
||||
loadCalendarItems();
|
||||
});
|
||||
|
||||
// 뒤로 가기
|
||||
function goBack() {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// 캘린더 항목 로드
|
||||
function loadCalendarItems() {
|
||||
// 임시 데이터 (실제로는 API에서 가져옴)
|
||||
const calendarItems = [
|
||||
{
|
||||
id: 1,
|
||||
content: '월말 보고서 제출',
|
||||
photo: null,
|
||||
due_date: '2024-01-25',
|
||||
status: 'active',
|
||||
priority: 'urgent',
|
||||
created_at: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '클라이언트 미팅 자료 준비',
|
||||
photo: null,
|
||||
due_date: '2024-01-30',
|
||||
status: 'active',
|
||||
priority: 'warning',
|
||||
created_at: '2024-01-16'
|
||||
}
|
||||
];
|
||||
|
||||
renderCalendarItems(calendarItems);
|
||||
}
|
||||
|
||||
// 캘린더 항목 렌더링
|
||||
function renderCalendarItems(items) {
|
||||
const calendarList = document.getElementById('calendarList');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (items.length === 0) {
|
||||
calendarList.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
calendarList.innerHTML = items.map(item => `
|
||||
<div class="calendar-item p-6 ${getDeadlineClass(item.priority)}">
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- 우선순위 아이콘 -->
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center ${getPriorityColor(item.priority)}">
|
||||
<i class="fas ${getPriorityIcon(item.priority)} text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사진 (있는 경우) -->
|
||||
${item.photo ? `
|
||||
<div class="flex-shrink-0">
|
||||
<img src="${item.photo}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-gray-900 font-medium mb-2">${item.content}</h4>
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500 mb-2">
|
||||
<span class="${getDueDateColor(item.due_date)}">
|
||||
<i class="fas fa-calendar-times mr-1"></i>마감: ${formatDate(item.due_date)}
|
||||
</span>
|
||||
<span>
|
||||
<i class="fas fa-clock mr-1"></i>등록: ${formatDate(item.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getPriorityBadgeColor(item.priority)}">
|
||||
${getPriorityText(item.priority)}
|
||||
</span>
|
||||
<span class="ml-2 text-gray-500">
|
||||
${getDaysRemaining(item.due_date)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="flex-shrink-0 flex space-x-2">
|
||||
${item.status !== 'completed' ? `
|
||||
<button onclick="completeCalendar(${item.id})" class="text-green-500 hover:text-green-700" title="완료하기">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
<button onclick="extendDeadline(${item.id})" class="text-orange-500 hover:text-orange-700" title="기한 연장">
|
||||
<i class="fas fa-calendar-plus"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="editCalendar(${item.id})" class="text-gray-400 hover:text-blue-500" title="수정하기">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 마감 기한별 클래스
|
||||
function getDeadlineClass(priority) {
|
||||
const classes = {
|
||||
urgent: 'deadline-urgent',
|
||||
warning: 'deadline-warning',
|
||||
normal: 'deadline-normal'
|
||||
};
|
||||
return classes[priority] || 'deadline-normal';
|
||||
}
|
||||
|
||||
// 우선순위별 색상
|
||||
function getPriorityColor(priority) {
|
||||
const colors = {
|
||||
urgent: 'bg-red-100 text-red-600',
|
||||
warning: 'bg-orange-100 text-orange-600',
|
||||
normal: 'bg-blue-100 text-blue-600'
|
||||
};
|
||||
return colors[priority] || 'bg-gray-100 text-gray-600';
|
||||
}
|
||||
|
||||
// 우선순위별 아이콘
|
||||
function getPriorityIcon(priority) {
|
||||
const icons = {
|
||||
urgent: 'fa-exclamation-triangle',
|
||||
warning: 'fa-exclamation',
|
||||
normal: 'fa-calendar'
|
||||
};
|
||||
return icons[priority] || 'fa-circle';
|
||||
}
|
||||
|
||||
// 우선순위별 배지 색상
|
||||
function getPriorityBadgeColor(priority) {
|
||||
const colors = {
|
||||
urgent: 'bg-red-100 text-red-800',
|
||||
warning: 'bg-orange-100 text-orange-800',
|
||||
normal: 'bg-blue-100 text-blue-800'
|
||||
};
|
||||
return colors[priority] || 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
// 우선순위 텍스트
|
||||
function getPriorityText(priority) {
|
||||
const texts = {
|
||||
urgent: '긴급',
|
||||
warning: '주의',
|
||||
normal: '여유'
|
||||
};
|
||||
return texts[priority] || '일반';
|
||||
}
|
||||
|
||||
// 마감일 색상
|
||||
function getDueDateColor(dueDate) {
|
||||
const days = getDaysUntilDeadline(dueDate);
|
||||
if (days <= 3) return 'text-red-600 font-medium';
|
||||
if (days <= 7) return 'text-orange-600 font-medium';
|
||||
return 'text-gray-600';
|
||||
}
|
||||
|
||||
// 남은 일수 계산
|
||||
function getDaysUntilDeadline(dueDate) {
|
||||
const today = new Date();
|
||||
const deadline = new Date(dueDate);
|
||||
const diffTime = deadline - today;
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
// 남은 일수 텍스트
|
||||
function getDaysRemaining(dueDate) {
|
||||
const days = getDaysUntilDeadline(dueDate);
|
||||
if (days < 0) return '기한 초과';
|
||||
if (days === 0) return '오늘 마감';
|
||||
if (days === 1) return '내일 마감';
|
||||
return `${days}일 남음`;
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR');
|
||||
}
|
||||
|
||||
// 캘린더 완료
|
||||
function completeCalendar(id) {
|
||||
console.log('캘린더 완료:', id);
|
||||
// TODO: API 호출하여 상태를 'completed'로 변경
|
||||
}
|
||||
|
||||
// 기한 연장
|
||||
function extendDeadline(id) {
|
||||
console.log('기한 연장:', id);
|
||||
// TODO: 기한 연장 모달 표시
|
||||
}
|
||||
|
||||
// 캘린더 편집
|
||||
function editCalendar(id) {
|
||||
console.log('캘린더 편집:', id);
|
||||
// TODO: 편집 모달 또는 페이지로 이동
|
||||
}
|
||||
|
||||
// 필터링
|
||||
function filterCalendar(filter) {
|
||||
console.log('필터:', filter);
|
||||
// TODO: 필터에 따라 목록 재로드
|
||||
}
|
||||
|
||||
// 대시보드로 이동
|
||||
function goToDashboard() {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
|
||||
// 전역 함수 등록
|
||||
window.goToDashboard = goToDashboard;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
413
frontend/checklist.html
Normal file
@@ -0,0 +1,413 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>체크리스트 - 기한 없는 일들</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);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--success);
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #059669;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.checklist-item.completed {
|
||||
opacity: 0.7;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.checklist-item.completed .item-content {
|
||||
text-decoration: line-through;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.checkbox-custom {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.checkbox-custom.checked {
|
||||
background-color: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.checkbox-custom:hover {
|
||||
border-color: #10b981;
|
||||
}
|
||||
</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-check-square text-2xl text-green-500 mr-3"></i>
|
||||
<h1 class="text-xl font-semibold text-gray-800">체크리스트</h1>
|
||||
<span class="ml-3 text-sm text-gray-500">기한 없는 일들</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="goToDashboard()" class="text-blue-600 hover:text-blue-800 font-medium">
|
||||
<i class="fas fa-chart-line mr-1"></i>대시보드
|
||||
</button>
|
||||
<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-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 페이지 설명 -->
|
||||
<div class="bg-green-50 rounded-xl p-6 mb-8">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-check-square text-2xl text-green-600 mr-3"></i>
|
||||
<h2 class="text-xl font-semibold text-green-900">체크리스트 관리</h2>
|
||||
</div>
|
||||
<p class="text-green-800 mb-4">
|
||||
기한이 없는 일들을 관리합니다. 언제든 할 수 있는 일들을 체크해나가세요.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-green-900 mb-1">📝 할 일</div>
|
||||
<div class="text-green-700">아직 완료하지 않은 일들</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-green-900 mb-1">✅ 완료</div>
|
||||
<div class="text-green-700">완료한 일들</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-green-900 mb-1">📊 진행률</div>
|
||||
<div class="text-green-700" id="progressText">0% 완료</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 진행률 표시 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800">
|
||||
<i class="fas fa-chart-line text-green-500 mr-2"></i>전체 진행률
|
||||
</h3>
|
||||
<div class="text-sm text-gray-600">
|
||||
<span id="completedCount">0</span> / <span id="totalCount">0</span> 완료
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-3">
|
||||
<div id="progressBar" class="bg-green-500 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 및 정렬 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div class="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
||||
<button onclick="filterChecklist('all')" class="filter-tab active px-4 py-2 rounded text-sm font-medium">전체</button>
|
||||
<button onclick="filterChecklist('active')" class="filter-tab px-4 py-2 rounded text-sm font-medium">할 일</button>
|
||||
<button onclick="filterChecklist('completed')" class="filter-tab px-4 py-2 rounded text-sm font-medium">완료</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<label class="text-sm text-gray-600">정렬:</label>
|
||||
<select id="sortBy" class="border border-gray-300 rounded-lg px-3 py-1 text-sm">
|
||||
<option value="created_at">등록일 순</option>
|
||||
<option value="completed_at">완료일 순</option>
|
||||
<option value="alphabetical">가나다 순</option>
|
||||
</select>
|
||||
<button onclick="clearCompleted()" class="text-sm text-red-600 hover:text-red-800">
|
||||
<i class="fas fa-trash mr-1"></i>완료된 항목 삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 체크리스트 목록 -->
|
||||
<div class="bg-white rounded-xl shadow-sm">
|
||||
<div class="p-6 border-b">
|
||||
<h3 class="text-lg font-semibold text-gray-800">
|
||||
<i class="fas fa-list text-green-500 mr-2"></i>체크리스트 목록
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div id="checklistList" class="divide-y divide-gray-100">
|
||||
<!-- 체크리스트 항목들이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="p-12 text-center text-gray-500">
|
||||
<i class="fas fa-check-square text-4xl mb-4 opacity-50"></i>
|
||||
<p>아직 체크리스트 항목이 없습니다.</p>
|
||||
<p class="text-sm">메인 페이지에서 기한 없는 항목을 등록해보세요!</p>
|
||||
<button onclick="goBack()" class="mt-4 btn-success px-6 py-2 rounded-lg">
|
||||
<i class="fas fa-arrow-left mr-2"></i>메인으로 돌아가기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="static/js/auth.js"></script>
|
||||
<script>
|
||||
let checklistItems = [];
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuthStatus();
|
||||
loadChecklistItems();
|
||||
});
|
||||
|
||||
// 뒤로 가기
|
||||
function goBack() {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// 체크리스트 항목 로드
|
||||
function loadChecklistItems() {
|
||||
// 임시 데이터 (실제로는 API에서 가져옴)
|
||||
checklistItems = [
|
||||
{
|
||||
id: 1,
|
||||
content: '책상 정리하기',
|
||||
photo: null,
|
||||
completed: false,
|
||||
created_at: '2024-01-15',
|
||||
completed_at: null
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '운동 계획 세우기',
|
||||
photo: null,
|
||||
completed: true,
|
||||
created_at: '2024-01-16',
|
||||
completed_at: '2024-01-18'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: '독서 목록 만들기',
|
||||
photo: null,
|
||||
completed: false,
|
||||
created_at: '2024-01-17',
|
||||
completed_at: null
|
||||
}
|
||||
];
|
||||
|
||||
renderChecklistItems(checklistItems);
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
// 체크리스트 항목 렌더링
|
||||
function renderChecklistItems(items) {
|
||||
const checklistList = document.getElementById('checklistList');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (items.length === 0) {
|
||||
checklistList.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
checklistList.innerHTML = items.map(item => `
|
||||
<div class="checklist-item p-6 ${item.completed ? 'completed' : ''}">
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- 체크박스 -->
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<div class="checkbox-custom ${item.completed ? 'checked' : ''}" onclick="toggleComplete(${item.id})">
|
||||
${item.completed ? '<i class="fas fa-check text-xs"></i>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사진 (있는 경우) -->
|
||||
${item.photo ? `
|
||||
<div class="flex-shrink-0">
|
||||
<img src="${item.photo}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="item-content text-gray-900 font-medium mb-2">${item.content}</h4>
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<span>
|
||||
<i class="fas fa-clock mr-1"></i>등록: ${formatDate(item.created_at)}
|
||||
</span>
|
||||
${item.completed && item.completed_at ? `
|
||||
<span class="text-green-600">
|
||||
<i class="fas fa-check mr-1"></i>완료: ${formatDate(item.completed_at)}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="flex-shrink-0 flex space-x-2">
|
||||
<button onclick="editChecklist(${item.id})" class="text-gray-400 hover:text-blue-500" title="수정하기">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button onclick="deleteChecklist(${item.id})" class="text-gray-400 hover:text-red-500" title="삭제하기">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 완료 상태 토글
|
||||
function toggleComplete(id) {
|
||||
const item = checklistItems.find(item => item.id === id);
|
||||
if (item) {
|
||||
item.completed = !item.completed;
|
||||
item.completed_at = item.completed ? new Date().toISOString().split('T')[0] : null;
|
||||
|
||||
renderChecklistItems(checklistItems);
|
||||
updateProgress();
|
||||
|
||||
// TODO: API 호출하여 상태 업데이트
|
||||
console.log('체크리스트 완료 상태 변경:', id, item.completed);
|
||||
}
|
||||
}
|
||||
|
||||
// 진행률 업데이트
|
||||
function updateProgress() {
|
||||
const total = checklistItems.length;
|
||||
const completed = checklistItems.filter(item => item.completed).length;
|
||||
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
|
||||
document.getElementById('totalCount').textContent = total;
|
||||
document.getElementById('completedCount').textContent = completed;
|
||||
document.getElementById('progressText').textContent = `${percentage}% 완료`;
|
||||
document.getElementById('progressBar').style.width = `${percentage}%`;
|
||||
}
|
||||
|
||||
// 완료된 항목 삭제
|
||||
function clearCompleted() {
|
||||
if (confirm('완료된 모든 항목을 삭제하시겠습니까?')) {
|
||||
checklistItems = checklistItems.filter(item => !item.completed);
|
||||
renderChecklistItems(checklistItems);
|
||||
updateProgress();
|
||||
|
||||
// TODO: API 호출하여 완료된 항목들 삭제
|
||||
console.log('완료된 항목들 삭제');
|
||||
}
|
||||
}
|
||||
|
||||
// 체크리스트 편집
|
||||
function editChecklist(id) {
|
||||
console.log('체크리스트 편집:', id);
|
||||
// TODO: 편집 모달 또는 페이지로 이동
|
||||
}
|
||||
|
||||
// 체크리스트 삭제
|
||||
function deleteChecklist(id) {
|
||||
if (confirm('이 항목을 삭제하시겠습니까?')) {
|
||||
checklistItems = checklistItems.filter(item => item.id !== id);
|
||||
renderChecklistItems(checklistItems);
|
||||
updateProgress();
|
||||
|
||||
// TODO: API 호출하여 항목 삭제
|
||||
console.log('체크리스트 삭제:', id);
|
||||
}
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR');
|
||||
}
|
||||
|
||||
// 필터링
|
||||
function filterChecklist(filter) {
|
||||
let filteredItems = checklistItems;
|
||||
|
||||
if (filter === 'active') {
|
||||
filteredItems = checklistItems.filter(item => !item.completed);
|
||||
} else if (filter === 'completed') {
|
||||
filteredItems = checklistItems.filter(item => item.completed);
|
||||
}
|
||||
|
||||
renderChecklistItems(filteredItems);
|
||||
|
||||
// 필터 탭 활성화 상태 업데이트
|
||||
document.querySelectorAll('.filter-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
console.log('필터:', filter);
|
||||
}
|
||||
|
||||
// 대시보드로 이동
|
||||
function goToDashboard() {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
|
||||
// 전역 함수 등록
|
||||
window.goToDashboard = goToDashboard;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
652
frontend/classify.html
Normal file
@@ -0,0 +1,652 @@
|
||||
<!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);
|
||||
}
|
||||
|
||||
/* 분류 카드 스타일 */
|
||||
.classify-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.classify-card:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.classify-card.selected {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* 분류 버튼 스타일 */
|
||||
.classify-btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.classify-btn.todo {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
color: #1e40af;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.classify-btn.todo:hover {
|
||||
background: linear-gradient(135deg, #bfdbfe, #93c5fd);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.classify-btn.calendar {
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
color: #92400e;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.classify-btn.calendar:hover {
|
||||
background: linear-gradient(135deg, #fde68a, #fcd34d);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.classify-btn.checklist {
|
||||
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
|
||||
color: #065f46;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.classify-btn.checklist:hover {
|
||||
background: linear-gradient(135deg, #a7f3d0, #6ee7b7);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 스마트 제안 스타일 */
|
||||
.smart-suggestion {
|
||||
background: linear-gradient(135deg, #f3e8ff, #e9d5ff);
|
||||
border: 2px solid #8b5cf6;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
/* 태그 스타일 */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.tag.selected {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 애니메이션 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 모바일 최적화 */
|
||||
@media (max-width: 768px) {
|
||||
.classify-btn {
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.classify-card {
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
</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-inbox text-2xl text-purple-500 mr-3"></i>
|
||||
<h1 class="text-xl font-semibold text-gray-800">분류 센터</h1>
|
||||
<span class="ml-3 px-2 py-1 bg-red-100 text-red-800 text-sm rounded-full" id="pendingCount">0</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="goToDashboard()" class="text-blue-600 hover:text-blue-800 font-medium">
|
||||
<i class="fas fa-chart-line mr-1"></i>대시보드
|
||||
</button>
|
||||
<button onclick="selectAll()" class="text-gray-600 hover:text-gray-800 text-sm">
|
||||
<i class="fas fa-check-square mr-1"></i>전체선택
|
||||
</button>
|
||||
<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-8">
|
||||
<!-- 상단 통계 및 필터 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<!-- 통계 카드들 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-inbox text-purple-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-600">분류 대기</p>
|
||||
<p class="text-2xl font-bold text-gray-900" id="totalPending">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-calendar-day text-blue-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-600">Todo 이동</p>
|
||||
<p class="text-2xl font-bold text-gray-900" id="todoMoved">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-calendar-times text-orange-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-600">캘린더 이동</p>
|
||||
<p class="text-2xl font-bold text-gray-900" id="calendarMoved">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-check-square text-green-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-600">체크리스트 이동</p>
|
||||
<p class="text-2xl font-bold text-gray-900" id="checklistMoved">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 및 정렬 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button onclick="filterItems('all')" class="filter-btn active px-4 py-2 rounded-lg text-sm font-medium">전체</button>
|
||||
<button onclick="filterItems('upload')" class="filter-btn px-4 py-2 rounded-lg text-sm font-medium">업로드</button>
|
||||
<button onclick="filterItems('mail')" class="filter-btn px-4 py-2 rounded-lg text-sm font-medium">메일</button>
|
||||
<button onclick="filterItems('suggested')" class="filter-btn px-4 py-2 rounded-lg text-sm font-medium">제안 있음</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<select id="sortBy" class="border border-gray-300 rounded-lg px-3 py-2 text-sm">
|
||||
<option value="newest">최신순</option>
|
||||
<option value="oldest">오래된순</option>
|
||||
<option value="suggested">제안순</option>
|
||||
</select>
|
||||
|
||||
<button onclick="batchClassify()" class="btn-primary px-4 py-2 rounded-lg text-sm" disabled id="batchBtn">
|
||||
<i class="fas fa-layer-group mr-1"></i>일괄 분류
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 분류 대기 항목들 -->
|
||||
<div class="space-y-4" id="classifyItems">
|
||||
<!-- 항목들이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
|
||||
<!-- 빈 상태 -->
|
||||
<div id="emptyState" class="hidden text-center py-16">
|
||||
<i class="fas fa-inbox text-6xl text-gray-300 mb-4"></i>
|
||||
<h3 class="text-xl font-semibold text-gray-600 mb-2">분류할 항목이 없습니다</h3>
|
||||
<p class="text-gray-500 mb-6">새로운 항목을 업로드하거나 메일을 받으면 여기에 표시됩니다.</p>
|
||||
<button onclick="goToDashboard()" class="btn-primary px-6 py-3 rounded-lg">
|
||||
<i class="fas fa-plus mr-2"></i>새 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="static/js/auth.js"></script>
|
||||
<script>
|
||||
let pendingItems = [];
|
||||
let selectedItems = [];
|
||||
let currentFilter = 'all';
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuthStatus();
|
||||
loadPendingItems();
|
||||
updateStats();
|
||||
});
|
||||
|
||||
// 분류 대기 항목 로드
|
||||
function loadPendingItems() {
|
||||
// 임시 데이터
|
||||
pendingItems = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'upload',
|
||||
content: '회의실 화이트보드 사진',
|
||||
photo: '/static/images/sample1.jpg',
|
||||
created_at: '2024-01-20T10:30:00Z',
|
||||
source: '직접 업로드',
|
||||
suggested: 'todo',
|
||||
confidence: 0.85,
|
||||
tags: ['업무', '회의', '계획']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'mail',
|
||||
content: '긴급: 내일까지 월말 보고서 제출 요청',
|
||||
sender: 'manager@company.com',
|
||||
created_at: '2024-01-20T14:15:00Z',
|
||||
source: '시놀로지 메일플러스',
|
||||
suggested: 'calendar',
|
||||
confidence: 0.95,
|
||||
tags: ['긴급', '업무', '마감']
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'upload',
|
||||
content: '마트에서 살 것들 메모',
|
||||
photo: '/static/images/sample2.jpg',
|
||||
created_at: '2024-01-20T16:45:00Z',
|
||||
source: '직접 업로드',
|
||||
suggested: 'checklist',
|
||||
confidence: 0.90,
|
||||
tags: ['개인', '쇼핑', '생활']
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'mail',
|
||||
content: '프로젝트 킥오프 미팅 일정 조율',
|
||||
sender: 'team@company.com',
|
||||
created_at: '2024-01-20T09:20:00Z',
|
||||
source: '시놀로지 메일플러스',
|
||||
suggested: 'todo',
|
||||
confidence: 0.75,
|
||||
tags: ['업무', '미팅', '프로젝트']
|
||||
}
|
||||
];
|
||||
|
||||
renderItems();
|
||||
}
|
||||
|
||||
// 항목들 렌더링
|
||||
function renderItems() {
|
||||
const container = document.getElementById('classifyItems');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
// 필터링
|
||||
let filteredItems = pendingItems;
|
||||
if (currentFilter !== 'all') {
|
||||
filteredItems = pendingItems.filter(item => {
|
||||
if (currentFilter === 'suggested') return item.suggested;
|
||||
return item.type === currentFilter;
|
||||
});
|
||||
}
|
||||
|
||||
if (filteredItems.length === 0) {
|
||||
container.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
container.innerHTML = filteredItems.map(item => `
|
||||
<div class="classify-card p-6 ${selectedItems.includes(item.id) ? 'selected' : ''}" data-id="${item.id}">
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- 선택 체크박스 -->
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<input type="checkbox" class="w-5 h-5 text-blue-600 rounded"
|
||||
${selectedItems.includes(item.id) ? 'checked' : ''}
|
||||
onchange="toggleSelection(${item.id})">
|
||||
</div>
|
||||
|
||||
<!-- 타입 아이콘 -->
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center ${item.type === 'upload' ? 'bg-blue-100' : 'bg-green-100'}">
|
||||
<i class="fas ${item.type === 'upload' ? 'fa-camera text-blue-600' : 'fa-envelope text-green-600'} text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사진 (있는 경우) -->
|
||||
${item.photo ? `
|
||||
<div class="flex-shrink-0">
|
||||
<img src="${item.photo}" class="w-20 h-20 object-cover rounded-lg" alt="첨부 사진">
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-2">${item.content}</h4>
|
||||
|
||||
<!-- 메타 정보 -->
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-3">
|
||||
<span>
|
||||
<i class="fas fa-clock mr-1"></i>${formatDate(item.created_at)}
|
||||
</span>
|
||||
<span>
|
||||
<i class="fas fa-source mr-1"></i>${item.source}
|
||||
</span>
|
||||
${item.sender ? `
|
||||
<span>
|
||||
<i class="fas fa-user mr-1"></i>${item.sender}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- 태그 -->
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
${item.tags.map(tag => `<span class="tag">#${tag}</span>`).join('')}
|
||||
</div>
|
||||
|
||||
<!-- 스마트 제안 -->
|
||||
${item.suggested ? `
|
||||
<div class="smart-suggestion">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-magic text-purple-600 mr-2"></i>
|
||||
<span class="text-sm font-medium text-purple-800">
|
||||
AI 제안: <strong>${getSuggestionText(item.suggested)}</strong>
|
||||
</span>
|
||||
<span class="ml-2 text-xs text-purple-600">(${Math.round(item.confidence * 100)}% 확신)</span>
|
||||
</div>
|
||||
<button onclick="acceptSuggestion(${item.id}, '${item.suggested}')"
|
||||
class="text-xs bg-purple-600 text-white px-3 py-1 rounded-full hover:bg-purple-700">
|
||||
적용
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 분류 버튼들 -->
|
||||
<div class="mt-6 flex flex-wrap gap-3 justify-center">
|
||||
<button onclick="classifyItem(${item.id}, 'todo')" class="classify-btn todo">
|
||||
<i class="fas fa-calendar-day mr-2"></i>Todo
|
||||
<div class="text-xs opacity-75">시작 날짜</div>
|
||||
</button>
|
||||
<button onclick="classifyItem(${item.id}, 'calendar')" class="classify-btn calendar">
|
||||
<i class="fas fa-calendar-times mr-2"></i>캘린더
|
||||
<div class="text-xs opacity-75">마감 기한</div>
|
||||
</button>
|
||||
<button onclick="classifyItem(${item.id}, 'checklist')" class="classify-btn checklist">
|
||||
<i class="fas fa-check-square mr-2"></i>체크리스트
|
||||
<div class="text-xs opacity-75">기한 없음</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 애니메이션 적용
|
||||
container.querySelectorAll('.classify-card').forEach((card, index) => {
|
||||
card.style.animationDelay = `${index * 0.1}s`;
|
||||
card.classList.add('fade-in');
|
||||
});
|
||||
}
|
||||
|
||||
// 항목 선택 토글
|
||||
function toggleSelection(id) {
|
||||
const index = selectedItems.indexOf(id);
|
||||
if (index > -1) {
|
||||
selectedItems.splice(index, 1);
|
||||
} else {
|
||||
selectedItems.push(id);
|
||||
}
|
||||
|
||||
updateBatchButton();
|
||||
renderItems();
|
||||
}
|
||||
|
||||
// 전체 선택
|
||||
function selectAll() {
|
||||
if (selectedItems.length === pendingItems.length) {
|
||||
selectedItems = [];
|
||||
} else {
|
||||
selectedItems = pendingItems.map(item => item.id);
|
||||
}
|
||||
|
||||
updateBatchButton();
|
||||
renderItems();
|
||||
}
|
||||
|
||||
// 일괄 분류 버튼 업데이트
|
||||
function updateBatchButton() {
|
||||
const batchBtn = document.getElementById('batchBtn');
|
||||
if (selectedItems.length > 0) {
|
||||
batchBtn.disabled = false;
|
||||
batchBtn.textContent = `${selectedItems.length}개 일괄 분류`;
|
||||
} else {
|
||||
batchBtn.disabled = true;
|
||||
batchBtn.innerHTML = '<i class="fas fa-layer-group mr-1"></i>일괄 분류';
|
||||
}
|
||||
}
|
||||
|
||||
// 개별 항목 분류
|
||||
function classifyItem(id, category) {
|
||||
const item = pendingItems.find(item => item.id === id);
|
||||
if (!item) return;
|
||||
|
||||
// 애니메이션 효과
|
||||
const card = document.querySelector(`[data-id="${id}"]`);
|
||||
card.style.transform = 'scale(0.95)';
|
||||
card.style.opacity = '0.7';
|
||||
|
||||
setTimeout(() => {
|
||||
// 항목 제거
|
||||
pendingItems = pendingItems.filter(item => item.id !== id);
|
||||
selectedItems = selectedItems.filter(itemId => itemId !== id);
|
||||
|
||||
// UI 업데이트
|
||||
renderItems();
|
||||
updateStats();
|
||||
updateBatchButton();
|
||||
|
||||
// 성공 메시지
|
||||
showToast(`"${item.content}"이(가) ${getSuggestionText(category)}(으)로 이동되었습니다.`, 'success');
|
||||
|
||||
// TODO: API 호출하여 실제 분류 처리
|
||||
console.log(`항목 ${id}을(를) ${category}로 분류`);
|
||||
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 제안 수락
|
||||
function acceptSuggestion(id, category) {
|
||||
classifyItem(id, category);
|
||||
}
|
||||
|
||||
// 일괄 분류
|
||||
function batchClassify() {
|
||||
if (selectedItems.length === 0) return;
|
||||
|
||||
// 일괄 분류 모달 또는 드롭다운 표시
|
||||
const category = prompt(`선택된 ${selectedItems.length}개 항목을 어디로 분류하시겠습니까?\n1. Todo\n2. 캘린더\n3. 체크리스트\n\n번호를 입력하세요:`);
|
||||
|
||||
const categories = { '1': 'todo', '2': 'calendar', '3': 'checklist' };
|
||||
const selectedCategory = categories[category];
|
||||
|
||||
if (selectedCategory) {
|
||||
selectedItems.forEach(id => {
|
||||
setTimeout(() => classifyItem(id, selectedCategory), Math.random() * 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 필터링
|
||||
function filterItems(filter) {
|
||||
currentFilter = filter;
|
||||
|
||||
// 필터 버튼 활성화 상태 업데이트
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.classList.remove('active', 'bg-blue-600', 'text-white');
|
||||
btn.classList.add('text-gray-600', 'bg-gray-100');
|
||||
});
|
||||
|
||||
event.target.classList.add('active', 'bg-blue-600', 'text-white');
|
||||
event.target.classList.remove('text-gray-600', 'bg-gray-100');
|
||||
|
||||
renderItems();
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
function updateStats() {
|
||||
document.getElementById('totalPending').textContent = pendingItems.length;
|
||||
document.getElementById('pendingCount').textContent = pendingItems.length;
|
||||
|
||||
// TODO: 실제 이동된 항목 수 계산
|
||||
document.getElementById('todoMoved').textContent = '5';
|
||||
document.getElementById('calendarMoved').textContent = '3';
|
||||
document.getElementById('checklistMoved').textContent = '7';
|
||||
}
|
||||
|
||||
// 유틸리티 함수들
|
||||
function getSuggestionText(category) {
|
||||
const texts = {
|
||||
'todo': 'Todo',
|
||||
'calendar': '캘린더',
|
||||
'checklist': '체크리스트'
|
||||
};
|
||||
return texts[category] || '미분류';
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
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 showToast(message, type = 'info') {
|
||||
// 간단한 토스트 메시지 (실제로는 더 예쁜 토스트 UI 구현)
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
|
||||
// 임시 알림
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
|
||||
type === 'success' ? 'bg-green-500' : 'bg-blue-500'
|
||||
}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 네비게이션 함수들
|
||||
function goBack() {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
function goToDashboard() {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
|
||||
// 전역 함수 등록
|
||||
window.toggleSelection = toggleSelection;
|
||||
window.selectAll = selectAll;
|
||||
window.classifyItem = classifyItem;
|
||||
window.acceptSuggestion = acceptSuggestion;
|
||||
window.batchClassify = batchClassify;
|
||||
window.filterItems = filterItems;
|
||||
window.goBack = goBack;
|
||||
window.goToDashboard = goToDashboard;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
937
frontend/dashboard.html
Normal file
@@ -0,0 +1,937 @@
|
||||
<!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: 120px;
|
||||
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);
|
||||
}
|
||||
|
||||
/* 모바일 일일 뷰 */
|
||||
.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="openUploadModal()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||||
<i class="fas fa-plus mr-1"></i>새 항목
|
||||
</button>
|
||||
<button onclick="goToClassify()" class="text-purple-600 hover:text-purple-800 font-medium text-sm">
|
||||
<i class="fas fa-inbox mr-1"></i>분류 센터
|
||||
<span class="ml-1 px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">3</span>
|
||||
</button>
|
||||
<button onclick="goToToday()" class="text-sm text-blue-600 hover:text-blue-800">
|
||||
<i class="fas fa-calendar-day mr-1"></i>오늘
|
||||
</button>
|
||||
<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-8">
|
||||
<!-- 데스크톱 뷰 -->
|
||||
<div class="desktop-view">
|
||||
<!-- 캘린더 네비게이션 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="previousMonth()" class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<h2 class="text-2xl font-bold text-gray-800" id="currentMonth"></h2>
|
||||
<button onclick="nextMonth()" class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<div class="w-3 h-3 bg-blue-200 border-l-4 border-blue-500 rounded-sm"></div>
|
||||
<span class="text-gray-600">Todo</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<div class="w-3 h-3 bg-yellow-200 border-l-4 border-yellow-500 rounded-sm"></div>
|
||||
<span class="text-gray-600">캘린더</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 캘린더 그리드 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-8">
|
||||
<div class="calendar-grid">
|
||||
<!-- 요일 헤더 -->
|
||||
<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 id="calendarDays"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 체크리스트 섹션 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<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="checklistProgress">0/0 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="checklistItems" class="max-h-96 overflow-y-auto">
|
||||
<!-- 체크리스트 항목들이 여기에 추가됩니다 -->
|
||||
</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/*" 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" class="hidden">
|
||||
<input type="file" id="galleryInput" accept="image/*" 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/auth.js"></script>
|
||||
<script src="static/js/image-utils.js"></script>
|
||||
<script>
|
||||
let currentDate = new Date();
|
||||
let calendarData = {};
|
||||
let checklistData = [];
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuthStatus();
|
||||
initializeDashboard();
|
||||
});
|
||||
|
||||
// 대시보드 초기화
|
||||
function initializeDashboard() {
|
||||
updateCurrentDate();
|
||||
loadCalendarData();
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// 캘린더 데이터 로드
|
||||
function loadCalendarData() {
|
||||
// 임시 데이터
|
||||
calendarData = {
|
||||
'2024-01-20': [
|
||||
{ type: 'todo', title: '프로젝트 시작', time: '09:00' },
|
||||
{ type: 'calendar', title: '회의 준비', time: '14:00' }
|
||||
],
|
||||
'2024-01-22': [
|
||||
{ type: 'calendar', title: '보고서 제출', time: '17:00' }
|
||||
],
|
||||
'2024-01-25': [
|
||||
{ type: 'todo', title: '문서 검토', time: '10:00' },
|
||||
{ type: 'todo', title: '팀 미팅', time: '15:00' },
|
||||
{ type: 'calendar', title: '월말 마감', time: '18:00' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 체크리스트 데이터 로드
|
||||
function loadChecklistData() {
|
||||
checklistData = [
|
||||
{ id: 1, title: '책상 정리하기', completed: false },
|
||||
{ id: 2, title: '운동 계획 세우기', completed: true },
|
||||
{ id: 3, title: '독서 목록 만들기', completed: false },
|
||||
{ id: 4, title: '이메일 정리', completed: false },
|
||||
{ id: 5, title: '비타민 구매', completed: true }
|
||||
];
|
||||
}
|
||||
|
||||
// 캘린더 렌더링
|
||||
function renderCalendar() {
|
||||
const calendarDays = document.getElementById('calendarDays');
|
||||
if (!calendarDays) return;
|
||||
|
||||
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()));
|
||||
|
||||
let html = '';
|
||||
const today = new Date();
|
||||
|
||||
for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) {
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
const isCurrentMonth = date.getMonth() === month;
|
||||
const isToday = date.toDateString() === today.toDateString();
|
||||
const dayData = calendarData[dateStr] || [];
|
||||
|
||||
html += `
|
||||
<div class="calendar-day ${!isCurrentMonth ? 'other-month' : ''} ${isToday ? 'today' : ''}"
|
||||
onclick="selectDate('${dateStr}')">
|
||||
<div class="day-number">${date.getDate()}</div>
|
||||
<div class="day-items">
|
||||
${dayData.map(item => `
|
||||
<div class="day-item ${item.type}" title="${item.title} (${item.time})">
|
||||
${item.title}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
calendarDays.innerHTML = html;
|
||||
}
|
||||
|
||||
// 일일 뷰 렌더링 (모바일)
|
||||
function renderDailyView() {
|
||||
const todayItems = document.getElementById('todayItems');
|
||||
if (!todayItems) return;
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayData = calendarData[today] || [];
|
||||
|
||||
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-clock mr-1"></i>${item.time}
|
||||
</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() {
|
||||
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 => `
|
||||
<div class="checklist-item ${item.completed ? 'completed' : ''}">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="checkbox-custom ${item.completed ? 'checked' : ''}"
|
||||
onclick="toggleChecklistItem(${item.id})">
|
||||
${item.completed ? '<i class="fas fa-check text-xs"></i>' : ''}
|
||||
</div>
|
||||
<span class="flex-1 ${item.completed ? 'line-through text-gray-500' : 'text-gray-900'}">${item.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
if (checklistItems) checklistItems.innerHTML = html;
|
||||
if (mobileChecklistItems) mobileChecklistItems.innerHTML = html;
|
||||
}
|
||||
|
||||
// 체크리스트 항목 토글
|
||||
function toggleChecklistItem(id) {
|
||||
const item = checklistData.find(item => item.id === id);
|
||||
if (item) {
|
||||
item.completed = !item.completed;
|
||||
renderChecklist();
|
||||
// TODO: API 호출
|
||||
}
|
||||
}
|
||||
|
||||
// 이전 달
|
||||
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 currentPhoto = null;
|
||||
|
||||
// 업로드 모달 열기
|
||||
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();
|
||||
removePhoto();
|
||||
}
|
||||
|
||||
// 파일 선택 (데스크톱)
|
||||
function selectFile() {
|
||||
document.getElementById('desktopFileInput').click();
|
||||
}
|
||||
|
||||
// 카메라 열기 (모바일)
|
||||
function openCamera() {
|
||||
document.getElementById('cameraInput').click();
|
||||
}
|
||||
|
||||
// 갤러리 열기 (모바일)
|
||||
function openGallery() {
|
||||
document.getElementById('galleryInput').click();
|
||||
}
|
||||
|
||||
// 사진 제거
|
||||
function removePhoto() {
|
||||
currentPhoto = null;
|
||||
const previewContainer = document.getElementById('photoPreview');
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
|
||||
if (previewContainer) {
|
||||
previewContainer.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (previewImage) {
|
||||
previewImage.src = '';
|
||||
}
|
||||
|
||||
// 파일 입력 초기화
|
||||
const inputs = ['desktopFileInput', 'cameraInput', 'galleryInput'];
|
||||
inputs.forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) input.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// 사진 업로드 처리
|
||||
async function handlePhotoUpload(event) {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
// 이미지 압축 (ImageUtils가 있는 경우)
|
||||
let processedImage;
|
||||
if (window.ImageUtils) {
|
||||
processedImage = await ImageUtils.compressImage(file, {
|
||||
maxWidth: 800,
|
||||
maxHeight: 600,
|
||||
quality: 0.8
|
||||
});
|
||||
} else {
|
||||
// 기본 처리
|
||||
processedImage = await fileToBase64(file);
|
||||
}
|
||||
|
||||
currentPhoto = processedImage;
|
||||
|
||||
// 미리보기 표시
|
||||
const previewContainer = document.getElementById('photoPreview');
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
const photoInfo = document.getElementById('photoInfo');
|
||||
|
||||
if (previewContainer && previewImage) {
|
||||
previewImage.src = processedImage;
|
||||
previewContainer.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (photoInfo) {
|
||||
const fileSize = Math.round(file.size / 1024);
|
||||
photoInfo.textContent = `${file.name} (${fileSize}KB)`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('이미지 처리 실패:', error);
|
||||
alert('이미지 처리에 실패했습니다.');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 파일을 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();
|
||||
|
||||
const content = document.getElementById('uploadContent').value.trim();
|
||||
if (!content) {
|
||||
alert('메모를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const itemData = {
|
||||
content: content,
|
||||
photo: currentPhoto,
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
// TODO: API 호출하여 항목 저장
|
||||
console.log('새 항목 등록:', itemData);
|
||||
|
||||
// 성공 메시지
|
||||
alert('항목이 등록되었습니다!');
|
||||
|
||||
// 모달 닫기 및 데이터 새로고침
|
||||
closeUploadModal();
|
||||
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.toggleChecklistItem = toggleChecklistItem;
|
||||
window.goBack = goBack;
|
||||
window.openUploadModal = openUploadModal;
|
||||
window.closeUploadModal = closeUploadModal;
|
||||
window.selectFile = selectFile;
|
||||
window.openCamera = openCamera;
|
||||
window.openGallery = openGallery;
|
||||
window.removePhoto = removePhoto;
|
||||
window.goToClassify = goToClassify;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
frontend/favicon.ico
Normal file
|
After Width: | Height: | Size: 723 B |
282
frontend/index.html
Normal file
@@ -0,0 +1,282 @@
|
||||
<!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>
|
||||
|
||||
<!-- PWA 설정 -->
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="theme-color" content="#6366f1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="Todo Project">
|
||||
|
||||
<!-- 파비콘 -->
|
||||
<link rel="icon" href="favicon.ico" sizes="any">
|
||||
<link rel="icon" href="static/icons/icon-192x192.png" type="image/png">
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" href="static/icons/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="static/icons/apple-touch-icon-ipad.png">
|
||||
|
||||
<!-- 추가 아이콘 크기들 -->
|
||||
<link rel="icon" sizes="72x72" href="static/icons/icon-72x72.png">
|
||||
<link rel="icon" sizes="96x96" href="static/icons/icon-96x96.png">
|
||||
<link rel="icon" sizes="128x128" href="static/icons/icon-128x128.png">
|
||||
<link rel="icon" sizes="144x144" href="static/icons/icon-144x144.png">
|
||||
<link rel="icon" sizes="152x152" href="static/icons/icon-152x152.png">
|
||||
<link rel="icon" sizes="192x192" href="static/icons/icon-192x192.png">
|
||||
<link rel="icon" sizes="384x384" href="static/icons/icon-384x384.png">
|
||||
<link rel="icon" sizes="512x512" href="static/icons/icon-512x512.png">
|
||||
|
||||
<!-- 외부 라이브러리 -->
|
||||
<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);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: var(--warning);
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #d97706;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: 1px solid var(--gray-300);
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.todo-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.status-draft { color: #6b7280; }
|
||||
.status-scheduled { color: var(--primary); }
|
||||
.status-active { color: var(--warning); }
|
||||
.status-completed { color: var(--success); }
|
||||
.status-delayed { color: var(--danger); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 로그인 화면 -->
|
||||
<div id="loginScreen" class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8 w-full max-w-sm">
|
||||
<div class="text-center mb-6">
|
||||
<i class="fas fa-tasks text-4xl text-blue-500 mb-4"></i>
|
||||
<h1 class="text-2xl font-bold text-gray-800">Todo Project</h1>
|
||||
<p class="text-gray-500 text-sm">간결한 할일 관리</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">사용자명</label>
|
||||
<input type="text" id="username" class="input-field w-full px-3 py-2 rounded-lg" placeholder="사용자명을 입력하세요" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">비밀번호</label>
|
||||
<input type="password" id="password" class="input-field w-full px-3 py-2 rounded-lg" placeholder="비밀번호를 입력하세요" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full py-2 px-4 rounded-lg font-medium">
|
||||
로그인
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-xs text-gray-500 text-center">
|
||||
<p>테스트 계정: user1 / password123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메인 애플리케이션 -->
|
||||
<div id="mainApp" class="hidden 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">
|
||||
<i class="fas fa-tasks text-2xl text-blue-500 mr-3"></i>
|
||||
<h1 class="text-xl font-semibold text-gray-800">Todo Project</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="goToClassify()" class="text-purple-600 hover:text-purple-800 font-medium">
|
||||
<i class="fas fa-inbox mr-1"></i>분류 센터
|
||||
<span class="ml-1 px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">3</span>
|
||||
</button>
|
||||
<button onclick="goToDashboard()" class="text-blue-600 hover:text-blue-800 font-medium">
|
||||
<i class="fas fa-chart-line mr-1"></i>대시보드
|
||||
</button>
|
||||
<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-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 빠른 등록 안내 -->
|
||||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-6 mb-8 border border-blue-100">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-plus-circle text-3xl text-blue-500 mb-3"></i>
|
||||
<h2 class="text-xl font-semibold text-gray-800 mb-2">새 항목을 등록하시겠어요?</h2>
|
||||
<p class="text-gray-600 mb-4">대시보드에서 사진과 메모를 함께 등록할 수 있습니다.</p>
|
||||
<button onclick="goToDashboard()" class="btn-primary px-6 py-3 rounded-lg font-medium">
|
||||
<i class="fas fa-chart-line mr-2"></i>대시보드에서 등록하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록된 항목들 -->
|
||||
<div class="bg-white rounded-xl shadow-sm">
|
||||
<div class="p-6 border-b">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-list text-blue-500 mr-2"></i>등록된 항목들
|
||||
</h2>
|
||||
|
||||
<!-- 분류 안내 -->
|
||||
<div class="bg-blue-50 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-blue-800 mb-2">
|
||||
<i class="fas fa-info-circle mr-2"></i>등록된 항목을 클릭하여 3가지 방법으로 분류하세요:
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
|
||||
<div class="flex items-center text-blue-700">
|
||||
<i class="fas fa-calendar-day mr-2"></i>
|
||||
<span><strong>Todo:</strong> 시작 날짜가 있는 일</span>
|
||||
</div>
|
||||
<div class="flex items-center text-blue-700">
|
||||
<i class="fas fa-calendar-times mr-2 text-orange-500"></i>
|
||||
<span><strong>캘린더:</strong> 마감 기한이 있는 일</span>
|
||||
</div>
|
||||
<div class="flex items-center text-blue-700">
|
||||
<i class="fas fa-check-square mr-2"></i>
|
||||
<span><strong>체크리스트:</strong> 기한 없는 일</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="itemsList" class="divide-y divide-gray-100">
|
||||
<!-- 등록된 항목들이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="p-12 text-center text-gray-500">
|
||||
<i class="fas fa-inbox text-4xl mb-4 opacity-50"></i>
|
||||
<p>아직 등록된 항목이 없습니다.</p>
|
||||
<p class="text-sm">위에서 새로운 항목을 등록해보세요!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 분류 페이지 링크 -->
|
||||
<div class="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Todo 페이지 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 hover:shadow-md transition-shadow cursor-pointer" onclick="goToPage('todo')">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-calendar-day text-2xl text-blue-600"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">Todo</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">시작 날짜가 있는 일들</p>
|
||||
<div class="bg-blue-50 rounded-lg p-3">
|
||||
<span class="text-blue-800 font-medium" id="todoCount">0개</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 캘린더 페이지 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 hover:shadow-md transition-shadow cursor-pointer" onclick="goToPage('calendar')">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-calendar-times text-2xl text-orange-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">캘린더</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">마감 기한이 있는 일들</p>
|
||||
<div class="bg-orange-50 rounded-lg p-3">
|
||||
<span class="text-orange-700 font-medium" id="calendarCount">0개</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 체크리스트 페이지 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 hover:shadow-md transition-shadow cursor-pointer" onclick="goToPage('checklist')">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-check-square text-2xl text-green-600"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">체크리스트</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">기한 없는 일들</p>
|
||||
<div class="bg-green-50 rounded-lg p-3">
|
||||
<span class="text-green-800 font-medium" id="checklistCount">0개</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 로딩 오버레이 -->
|
||||
<div id="loadingOverlay" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 text-center">
|
||||
<i class="fas fa-spinner fa-spin text-2xl text-indigo-600 mb-3"></i>
|
||||
<p class="text-gray-700">처리 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="static/js/image-utils.js"></script>
|
||||
<script src="static/js/api.js"></script>
|
||||
<script src="static/js/todos.js"></script>
|
||||
<script src="static/js/auth.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
104
frontend/manifest.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"name": "Todo Project - 간결한 할일 관리",
|
||||
"short_name": "Todo Project",
|
||||
"description": "사진과 메모를 기반으로 한 간단한 일정관리 시스템",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f9fafb",
|
||||
"theme_color": "#6366f1",
|
||||
"orientation": "portrait-primary",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"lang": "ko",
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "static/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "static/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "static/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "static/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "static/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "static/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "static/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "빠른 할일 추가",
|
||||
"short_name": "할일 추가",
|
||||
"description": "새로운 할일을 빠르게 추가합니다",
|
||||
"url": "/?action=add",
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/icons/shortcut-add.png",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "진행중인 할일",
|
||||
"short_name": "진행중",
|
||||
"description": "현재 진행중인 할일을 확인합니다",
|
||||
"url": "/?filter=active",
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/icons/shortcut-active.png",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "static/screenshots/desktop-1.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "데스크톱 메인 화면"
|
||||
},
|
||||
{
|
||||
"src": "static/screenshots/mobile-1.png",
|
||||
"sizes": "375x812",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow",
|
||||
"label": "모바일 메인 화면"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
frontend/static/icons/apple-touch-icon-ipad.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/static/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
frontend/static/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/static/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
frontend/static/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
frontend/static/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
frontend/static/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 261 KiB |
BIN
frontend/static/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 429 KiB |
BIN
frontend/static/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/static/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
165
frontend/static/js/api.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* API 통신 유틸리티
|
||||
*/
|
||||
|
||||
const API_BASE_URL = 'http://localhost:9000/api';
|
||||
|
||||
class ApiClient {
|
||||
constructor() {
|
||||
this.token = localStorage.getItem('authToken');
|
||||
}
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
// 인증 토큰 추가
|
||||
if (this.token) {
|
||||
config.headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// 토큰 만료 시 로그아웃
|
||||
this.logout();
|
||||
throw new Error('인증이 만료되었습니다. 다시 로그인해주세요.');
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return await response.json();
|
||||
}
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
console.error('API 요청 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// GET 요청
|
||||
async get(endpoint) {
|
||||
return this.request(endpoint, { method: 'GET' });
|
||||
}
|
||||
|
||||
// POST 요청
|
||||
async post(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
// PUT 요청
|
||||
async put(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE 요청
|
||||
async delete(endpoint) {
|
||||
return this.request(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// 파일 업로드
|
||||
async uploadFile(endpoint, formData) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
// Content-Type을 설정하지 않음 (FormData가 자동으로 설정)
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
||||
// 토큰 설정
|
||||
setToken(token) {
|
||||
this.token = token;
|
||||
localStorage.setItem('authToken', token);
|
||||
}
|
||||
|
||||
// 로그아웃
|
||||
logout() {
|
||||
this.token = null;
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('currentUser');
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 API 클라이언트 인스턴스
|
||||
const api = new ApiClient();
|
||||
|
||||
// 인증 관련 API
|
||||
const AuthAPI = {
|
||||
async login(username, password) {
|
||||
const response = await api.post('/auth/login', {
|
||||
username,
|
||||
password
|
||||
});
|
||||
|
||||
if (response.access_token) {
|
||||
api.setToken(response.access_token);
|
||||
localStorage.setItem('currentUser', JSON.stringify(response.user));
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await api.post('/auth/logout');
|
||||
} catch (error) {
|
||||
console.error('로그아웃 API 호출 실패:', error);
|
||||
} finally {
|
||||
api.logout();
|
||||
}
|
||||
},
|
||||
|
||||
async getCurrentUser() {
|
||||
return api.get('/auth/me');
|
||||
}
|
||||
};
|
||||
|
||||
// Todo 관련 API
|
||||
const TodoAPI = {
|
||||
async getTodos(filter = 'all') {
|
||||
const params = filter !== 'all' ? `?status=${filter}` : '';
|
||||
return api.get(`/todos${params}`);
|
||||
},
|
||||
|
||||
async createTodo(todoData) {
|
||||
return api.post('/todos', todoData);
|
||||
},
|
||||
|
||||
async updateTodo(id, todoData) {
|
||||
return api.put(`/todos/${id}`, todoData);
|
||||
},
|
||||
|
||||
async deleteTodo(id) {
|
||||
return api.delete(`/todos/${id}`);
|
||||
},
|
||||
|
||||
async uploadImage(imageFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
return api.uploadFile('/todos/upload-image', formData);
|
||||
}
|
||||
};
|
||||
|
||||
// 전역으로 사용 가능하도록 export
|
||||
window.api = api;
|
||||
window.AuthAPI = AuthAPI;
|
||||
window.TodoAPI = TodoAPI;
|
||||
139
frontend/static/js/auth.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 인증 관리
|
||||
*/
|
||||
|
||||
let currentUser = null;
|
||||
|
||||
// 페이지 로드 시 인증 상태 확인
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuthStatus();
|
||||
setupLoginForm();
|
||||
});
|
||||
|
||||
// 인증 상태 확인
|
||||
function checkAuthStatus() {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const userData = localStorage.getItem('currentUser');
|
||||
|
||||
if (token && userData) {
|
||||
try {
|
||||
currentUser = JSON.parse(userData);
|
||||
showMainApp();
|
||||
} catch (error) {
|
||||
console.error('사용자 데이터 파싱 실패:', error);
|
||||
logout();
|
||||
}
|
||||
} else {
|
||||
showLoginScreen();
|
||||
}
|
||||
}
|
||||
|
||||
// 로그인 폼 설정
|
||||
function setupLoginForm() {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', handleLogin);
|
||||
}
|
||||
}
|
||||
|
||||
// 로그인 처리
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!username || !password) {
|
||||
alert('사용자명과 비밀번호를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
// 임시 로그인 (백엔드 구현 전까지)
|
||||
if (username === 'user1' && password === 'password123') {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
email: 'user1@todo-project.local',
|
||||
full_name: '사용자1'
|
||||
};
|
||||
|
||||
currentUser = mockUser;
|
||||
localStorage.setItem('authToken', 'mock-token-' + Date.now());
|
||||
localStorage.setItem('currentUser', JSON.stringify(mockUser));
|
||||
|
||||
showMainApp();
|
||||
} else {
|
||||
throw new Error('잘못된 사용자명 또는 비밀번호입니다.');
|
||||
}
|
||||
|
||||
// 실제 API 호출 (백엔드 구현 후 사용)
|
||||
/*
|
||||
const response = await AuthAPI.login(username, password);
|
||||
currentUser = response.user;
|
||||
showMainApp();
|
||||
*/
|
||||
|
||||
} catch (error) {
|
||||
console.error('로그인 실패:', error);
|
||||
alert(error.message || '로그인에 실패했습니다.');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 로그아웃
|
||||
function logout() {
|
||||
currentUser = null;
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('currentUser');
|
||||
showLoginScreen();
|
||||
}
|
||||
|
||||
// 로그인 화면 표시
|
||||
function showLoginScreen() {
|
||||
document.getElementById('loginScreen').classList.remove('hidden');
|
||||
document.getElementById('mainApp').classList.add('hidden');
|
||||
|
||||
// 폼 초기화
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
if (loginForm) {
|
||||
loginForm.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// 메인 앱 표시
|
||||
function showMainApp() {
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
document.getElementById('mainApp').classList.remove('hidden');
|
||||
|
||||
// 사용자 정보 표시
|
||||
const currentUserElement = document.getElementById('currentUser');
|
||||
if (currentUserElement && currentUser) {
|
||||
currentUserElement.textContent = currentUser.full_name || currentUser.username;
|
||||
}
|
||||
|
||||
// Todo 목록 로드
|
||||
if (typeof loadTodos === 'function') {
|
||||
loadTodos();
|
||||
}
|
||||
}
|
||||
|
||||
// 로딩 상태 표시
|
||||
function showLoading(show) {
|
||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||
if (loadingOverlay) {
|
||||
if (show) {
|
||||
loadingOverlay.classList.remove('hidden');
|
||||
} else {
|
||||
loadingOverlay.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 사용 가능하도록 export
|
||||
window.currentUser = currentUser;
|
||||
window.logout = logout;
|
||||
window.showLoading = showLoading;
|
||||
134
frontend/static/js/image-utils.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 이미지 압축 및 최적화 유틸리티
|
||||
*/
|
||||
|
||||
const ImageUtils = {
|
||||
/**
|
||||
* 이미지를 압축하고 리사이즈
|
||||
* @param {File|Blob|String} source - 이미지 파일, Blob 또는 base64 문자열
|
||||
* @param {Object} options - 압축 옵션
|
||||
* @returns {Promise<String>} - 압축된 base64 이미지
|
||||
*/
|
||||
async compressImage(source, options = {}) {
|
||||
const {
|
||||
maxWidth = 1024, // 최대 너비
|
||||
maxHeight = 1024, // 최대 높이
|
||||
quality = 0.7, // JPEG 품질 (0-1)
|
||||
format = 'jpeg' // 출력 형식
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let img = new Image();
|
||||
|
||||
// 이미지 로드 완료 시
|
||||
img.onload = () => {
|
||||
// Canvas 생성
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 리사이즈 계산
|
||||
let { width, height } = this.calculateDimensions(
|
||||
img.width,
|
||||
img.height,
|
||||
maxWidth,
|
||||
maxHeight
|
||||
);
|
||||
|
||||
// Canvas 크기 설정
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// 이미지 그리기
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// 압축된 이미지를 base64로 변환
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
reject(new Error('이미지 압축 실패'));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
}, `image/${format}`, quality);
|
||||
};
|
||||
|
||||
img.onerror = () => reject(new Error('이미지 로드 실패'));
|
||||
|
||||
// 소스 타입에 따라 처리
|
||||
if (typeof source === 'string') {
|
||||
// Base64 문자열인 경우
|
||||
img.src = source;
|
||||
} else if (source instanceof File || source instanceof Blob) {
|
||||
// File 또는 Blob인 경우
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
img.src = reader.result;
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(source);
|
||||
} else {
|
||||
reject(new Error('지원하지 않는 이미지 형식'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 이미지 크기 계산 (비율 유지)
|
||||
*/
|
||||
calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
|
||||
// 원본 크기가 제한 내에 있으면 그대로 반환
|
||||
if (originalWidth <= maxWidth && originalHeight <= maxHeight) {
|
||||
return { width: originalWidth, height: originalHeight };
|
||||
}
|
||||
|
||||
// 비율 계산
|
||||
const widthRatio = maxWidth / originalWidth;
|
||||
const heightRatio = maxHeight / originalHeight;
|
||||
const ratio = Math.min(widthRatio, heightRatio);
|
||||
|
||||
return {
|
||||
width: Math.round(originalWidth * ratio),
|
||||
height: Math.round(originalHeight * ratio)
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 파일 크기를 사람이 읽을 수 있는 형식으로 변환
|
||||
*/
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
/**
|
||||
* Base64 문자열의 크기 계산
|
||||
*/
|
||||
getBase64Size(base64String) {
|
||||
const base64Length = base64String.length - (base64String.indexOf(',') + 1);
|
||||
const padding = (base64String.charAt(base64String.length - 2) === '=') ? 2 :
|
||||
((base64String.charAt(base64String.length - 1) === '=') ? 1 : 0);
|
||||
return (base64Length * 0.75) - padding;
|
||||
},
|
||||
|
||||
/**
|
||||
* 이미지 미리보기 생성 (썸네일)
|
||||
*/
|
||||
async createThumbnail(source, size = 150) {
|
||||
return this.compressImage(source, {
|
||||
maxWidth: size,
|
||||
maxHeight: size,
|
||||
quality: 0.8
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 전역으로 사용 가능하도록 export
|
||||
window.ImageUtils = ImageUtils;
|
||||
589
frontend/static/js/todos.js
Normal file
@@ -0,0 +1,589 @@
|
||||
/**
|
||||
* Todo 관리 기능
|
||||
*/
|
||||
|
||||
let todos = [];
|
||||
let currentPhoto = null;
|
||||
let currentFilter = 'all';
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setupTodoForm();
|
||||
setupPhotoUpload();
|
||||
setupFilters();
|
||||
updateItemCounts();
|
||||
loadRegisteredItems();
|
||||
});
|
||||
|
||||
// Todo 폼 설정
|
||||
function setupTodoForm() {
|
||||
const todoForm = document.getElementById('todoForm');
|
||||
if (todoForm) {
|
||||
todoForm.addEventListener('submit', handleTodoSubmit);
|
||||
}
|
||||
}
|
||||
|
||||
// 사진 업로드 설정
|
||||
function setupPhotoUpload() {
|
||||
const cameraInput = document.getElementById('cameraInput');
|
||||
const galleryInput = document.getElementById('galleryInput');
|
||||
|
||||
if (cameraInput) {
|
||||
cameraInput.addEventListener('change', handlePhotoUpload);
|
||||
}
|
||||
|
||||
if (galleryInput) {
|
||||
galleryInput.addEventListener('change', handlePhotoUpload);
|
||||
}
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
function setupFilters() {
|
||||
// 필터 탭 클릭 이벤트는 HTML에서 onclick으로 처리
|
||||
}
|
||||
|
||||
// Todo 제출 처리
|
||||
async function handleTodoSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const content = document.getElementById('todoContent').value.trim();
|
||||
if (!content) {
|
||||
alert('할일 내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const todoData = {
|
||||
content: content,
|
||||
photo: currentPhoto,
|
||||
status: 'draft',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 임시 저장 (백엔드 구현 전까지)
|
||||
const newTodo = {
|
||||
id: Date.now(),
|
||||
...todoData,
|
||||
user_id: currentUser?.id || 1
|
||||
};
|
||||
|
||||
todos.unshift(newTodo);
|
||||
|
||||
// 실제 API 호출 (백엔드 구현 후 사용)
|
||||
/*
|
||||
const newTodo = await TodoAPI.createTodo(todoData);
|
||||
todos.unshift(newTodo);
|
||||
*/
|
||||
|
||||
// 폼 초기화 및 목록 업데이트
|
||||
clearForm();
|
||||
loadRegisteredItems();
|
||||
updateItemCounts();
|
||||
|
||||
// 성공 메시지
|
||||
showToast('항목이 등록되었습니다!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('할일 추가 실패:', error);
|
||||
alert(error.message || '할일 추가에 실패했습니다.');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 사진 업로드 처리
|
||||
async function handlePhotoUpload(event) {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
// 이미지 압축
|
||||
const compressedImage = await ImageUtils.compressImage(file, {
|
||||
maxWidth: 800,
|
||||
maxHeight: 600,
|
||||
quality: 0.8
|
||||
});
|
||||
|
||||
currentPhoto = compressedImage;
|
||||
|
||||
// 미리보기 표시
|
||||
const previewContainer = document.getElementById('photoPreview');
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
|
||||
if (previewContainer && previewImage) {
|
||||
previewImage.src = compressedImage;
|
||||
previewContainer.classList.remove('hidden');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('이미지 처리 실패:', error);
|
||||
alert('이미지 처리에 실패했습니다.');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 카메라 열기
|
||||
function openCamera() {
|
||||
const cameraInput = document.getElementById('cameraInput');
|
||||
if (cameraInput) {
|
||||
cameraInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
// 갤러리 열기
|
||||
function openGallery() {
|
||||
const galleryInput = document.getElementById('galleryInput');
|
||||
if (galleryInput) {
|
||||
galleryInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
// 사진 제거
|
||||
function removePhoto() {
|
||||
currentPhoto = null;
|
||||
|
||||
const previewContainer = document.getElementById('photoPreview');
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
|
||||
if (previewContainer) {
|
||||
previewContainer.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (previewImage) {
|
||||
previewImage.src = '';
|
||||
}
|
||||
|
||||
// 파일 입력 초기화
|
||||
const cameraInput = document.getElementById('cameraInput');
|
||||
const galleryInput = document.getElementById('galleryInput');
|
||||
|
||||
if (cameraInput) cameraInput.value = '';
|
||||
if (galleryInput) galleryInput.value = '';
|
||||
}
|
||||
|
||||
// 폼 초기화
|
||||
function clearForm() {
|
||||
const todoForm = document.getElementById('todoForm');
|
||||
if (todoForm) {
|
||||
todoForm.reset();
|
||||
}
|
||||
|
||||
removePhoto();
|
||||
}
|
||||
|
||||
// Todo 목록 로드
|
||||
async function loadTodos() {
|
||||
try {
|
||||
// 임시 데이터 (백엔드 구현 전까지)
|
||||
if (todos.length === 0) {
|
||||
todos = [
|
||||
{
|
||||
id: 1,
|
||||
content: '프로젝트 문서 검토',
|
||||
status: 'active',
|
||||
photo: null,
|
||||
created_at: new Date(Date.now() - 86400000).toISOString(),
|
||||
user_id: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '회의 준비',
|
||||
status: 'completed',
|
||||
photo: null,
|
||||
created_at: new Date(Date.now() - 172800000).toISOString(),
|
||||
user_id: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// 실제 API 호출 (백엔드 구현 후 사용)
|
||||
/*
|
||||
todos = await TodoAPI.getTodos(currentFilter);
|
||||
*/
|
||||
|
||||
renderTodos();
|
||||
|
||||
} catch (error) {
|
||||
console.error('할일 목록 로드 실패:', error);
|
||||
showToast('할일 목록을 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Todo 목록 렌더링
|
||||
function renderTodos() {
|
||||
const todoList = document.getElementById('todoList');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (!todoList || !emptyState) return;
|
||||
|
||||
// 필터링
|
||||
const filteredTodos = todos.filter(todo => {
|
||||
if (currentFilter === 'all') return true;
|
||||
if (currentFilter === 'active') return ['draft', 'scheduled', 'active', 'delayed'].includes(todo.status);
|
||||
if (currentFilter === 'completed') return todo.status === 'completed';
|
||||
return todo.status === currentFilter;
|
||||
});
|
||||
|
||||
// 빈 상태 처리
|
||||
if (filteredTodos.length === 0) {
|
||||
todoList.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
// Todo 항목 렌더링
|
||||
todoList.innerHTML = filteredTodos.map(todo => `
|
||||
<div class="todo-item p-4 hover:bg-gray-50 transition-colors">
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- 체크박스 -->
|
||||
<button onclick="toggleTodo(${todo.id})" class="mt-1 flex-shrink-0">
|
||||
<i class="fas ${todo.status === 'completed' ? 'fa-check-circle text-green-500' : 'fa-circle text-gray-300'} text-lg"></i>
|
||||
</button>
|
||||
|
||||
<!-- 사진 (있는 경우) -->
|
||||
${todo.photo ? `
|
||||
<div class="flex-shrink-0">
|
||||
<img src="${todo.photo}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-gray-900 ${todo.status === 'completed' ? 'line-through text-gray-500' : ''}">${todo.content}</p>
|
||||
<div class="flex items-center space-x-3 mt-2 text-sm text-gray-500">
|
||||
<span class="status-${todo.status}">
|
||||
<i class="fas ${getStatusIcon(todo.status)} mr-1"></i>${getStatusText(todo.status)}
|
||||
</span>
|
||||
<span>${formatDate(todo.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="flex-shrink-0 flex space-x-2">
|
||||
${todo.status !== 'completed' ? `
|
||||
<button onclick="editTodo(${todo.id})" class="text-gray-400 hover:text-blue-500">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="deleteTodo(${todo.id})" class="text-gray-400 hover:text-red-500">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Todo 상태 토글
|
||||
async function toggleTodo(id) {
|
||||
try {
|
||||
const todo = todos.find(t => t.id === id);
|
||||
if (!todo) return;
|
||||
|
||||
const newStatus = todo.status === 'completed' ? 'active' : 'completed';
|
||||
|
||||
// 임시 업데이트
|
||||
todo.status = newStatus;
|
||||
|
||||
// 실제 API 호출 (백엔드 구현 후 사용)
|
||||
/*
|
||||
await TodoAPI.updateTodo(id, { status: newStatus });
|
||||
*/
|
||||
|
||||
renderTodos();
|
||||
showToast(newStatus === 'completed' ? '할일을 완료했습니다!' : '할일을 다시 활성화했습니다!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('할일 상태 변경 실패:', error);
|
||||
showToast('상태 변경에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Todo 삭제
|
||||
async function deleteTodo(id) {
|
||||
if (!confirm('정말로 이 할일을 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
// 임시 삭제
|
||||
todos = todos.filter(t => t.id !== id);
|
||||
|
||||
// 실제 API 호출 (백엔드 구현 후 사용)
|
||||
/*
|
||||
await TodoAPI.deleteTodo(id);
|
||||
*/
|
||||
|
||||
renderTodos();
|
||||
showToast('할일이 삭제되었습니다.', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('할일 삭제 실패:', error);
|
||||
showToast('삭제에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Todo 편집 (향후 구현)
|
||||
function editTodo(id) {
|
||||
// TODO: 편집 모달 또는 인라인 편집 구현
|
||||
console.log('편집 기능 구현 예정:', id);
|
||||
}
|
||||
|
||||
// 필터 변경
|
||||
function filterTodos(filter) {
|
||||
currentFilter = filter;
|
||||
|
||||
// 탭 활성화 상태 변경
|
||||
document.querySelectorAll('.filter-tab').forEach(tab => {
|
||||
tab.classList.remove('active', 'bg-white', 'text-blue-600');
|
||||
tab.classList.add('text-gray-600');
|
||||
});
|
||||
|
||||
event.target.classList.add('active', 'bg-white', 'text-blue-600');
|
||||
event.target.classList.remove('text-gray-600');
|
||||
|
||||
renderTodos();
|
||||
}
|
||||
|
||||
// 상태 아이콘 반환
|
||||
function getStatusIcon(status) {
|
||||
const icons = {
|
||||
draft: 'fa-edit',
|
||||
scheduled: 'fa-calendar',
|
||||
active: 'fa-play',
|
||||
completed: 'fa-check',
|
||||
delayed: 'fa-clock'
|
||||
};
|
||||
return icons[status] || 'fa-circle';
|
||||
}
|
||||
|
||||
// 상태 텍스트 반환
|
||||
function getStatusText(status) {
|
||||
const texts = {
|
||||
draft: '검토 필요',
|
||||
scheduled: '예정됨',
|
||||
active: '진행중',
|
||||
completed: '완료됨',
|
||||
delayed: '지연됨'
|
||||
};
|
||||
return texts[status] || '알 수 없음';
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffTime = now - date;
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) return '오늘';
|
||||
if (diffDays === 1) return '어제';
|
||||
if (diffDays < 7) return `${diffDays}일 전`;
|
||||
|
||||
return date.toLocaleDateString('ko-KR');
|
||||
}
|
||||
|
||||
// 토스트 메시지 표시
|
||||
function showToast(message, type = 'info') {
|
||||
// 간단한 alert으로 대체 (향후 토스트 UI 구현)
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
|
||||
if (type === 'error') {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 이동 함수
|
||||
function goToPage(pageType) {
|
||||
const pages = {
|
||||
'todo': 'todo.html',
|
||||
'calendar': 'calendar.html',
|
||||
'checklist': 'checklist.html'
|
||||
};
|
||||
|
||||
if (pages[pageType]) {
|
||||
window.location.href = pages[pageType];
|
||||
} else {
|
||||
console.error('Unknown page type:', pageType);
|
||||
}
|
||||
}
|
||||
|
||||
// 대시보드로 이동
|
||||
function goToDashboard() {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
|
||||
// 분류 센터로 이동
|
||||
function goToClassify() {
|
||||
window.location.href = 'classify.html';
|
||||
}
|
||||
|
||||
// 항목 등록 후 인덱스 업데이트
|
||||
function updateItemCounts() {
|
||||
// TODO: API에서 각 분류별 항목 수를 가져와서 업데이트
|
||||
// 임시로 하드코딩된 값 사용
|
||||
const todoCount = document.getElementById('todoCount');
|
||||
const calendarCount = document.getElementById('calendarCount');
|
||||
const checklistCount = document.getElementById('checklistCount');
|
||||
|
||||
if (todoCount) todoCount.textContent = '2개';
|
||||
if (calendarCount) calendarCount.textContent = '3개';
|
||||
if (checklistCount) checklistCount.textContent = '5개';
|
||||
}
|
||||
|
||||
// 등록된 항목들 로드
|
||||
function loadRegisteredItems() {
|
||||
// 임시 데이터 (실제로는 API에서 가져옴)
|
||||
const sampleItems = [
|
||||
{
|
||||
id: 1,
|
||||
content: '프로젝트 문서 정리',
|
||||
photo_url: null,
|
||||
category: null,
|
||||
created_at: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '회의 자료 준비',
|
||||
photo_url: null,
|
||||
category: 'todo',
|
||||
created_at: '2024-01-16'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: '월말 보고서 작성',
|
||||
photo_url: null,
|
||||
category: 'calendar',
|
||||
created_at: '2024-01-17'
|
||||
}
|
||||
];
|
||||
|
||||
renderRegisteredItems(sampleItems);
|
||||
}
|
||||
|
||||
// 등록된 항목들 렌더링
|
||||
function renderRegisteredItems(items) {
|
||||
const itemsList = document.getElementById('itemsList');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (!itemsList || !emptyState) return;
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
itemsList.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
itemsList.innerHTML = items.map(item => `
|
||||
<div class="p-6 hover:bg-gray-50 cursor-pointer transition-colors" onclick="showClassificationModal(${item.id})">
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- 사진 (있는 경우) -->
|
||||
${item.photo_url ? `
|
||||
<div class="flex-shrink-0">
|
||||
<img src="${item.photo_url}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-gray-900 font-medium mb-2">${item.content}</h4>
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<span>
|
||||
<i class="fas fa-clock mr-1"></i>등록: ${formatDate(item.created_at)}
|
||||
</span>
|
||||
${item.category ? `
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getCategoryColor(item.category)}">
|
||||
${getCategoryText(item.category)}
|
||||
</span>
|
||||
` : `
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
미분류
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 분류 아이콘 -->
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 분류 모달 표시
|
||||
function showClassificationModal(itemId) {
|
||||
// TODO: 분류 선택 모달 구현
|
||||
console.log('분류 모달 표시:', itemId);
|
||||
|
||||
// 임시로 confirm으로 분류 선택
|
||||
const choice = prompt('분류를 선택하세요:\n1. Todo (시작 날짜)\n2. 캘린더 (마감 기한)\n3. 체크리스트 (기한 없음)\n\n번호를 입력하세요:');
|
||||
|
||||
if (choice) {
|
||||
const categories = {
|
||||
'1': 'todo',
|
||||
'2': 'calendar',
|
||||
'3': 'checklist'
|
||||
};
|
||||
|
||||
const category = categories[choice];
|
||||
if (category) {
|
||||
classifyItem(itemId, category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 항목 분류
|
||||
function classifyItem(itemId, category) {
|
||||
// TODO: API 호출하여 항목 분류 업데이트
|
||||
console.log('항목 분류:', itemId, category);
|
||||
|
||||
// 분류 후 해당 페이지로 이동
|
||||
goToPage(category);
|
||||
}
|
||||
|
||||
// 분류별 색상
|
||||
function getCategoryColor(category) {
|
||||
const colors = {
|
||||
'todo': 'bg-blue-100 text-blue-800',
|
||||
'calendar': 'bg-orange-100 text-orange-800',
|
||||
'checklist': 'bg-green-100 text-green-800'
|
||||
};
|
||||
return colors[category] || 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
// 분류별 텍스트
|
||||
function getCategoryText(category) {
|
||||
const texts = {
|
||||
'todo': 'Todo',
|
||||
'calendar': '캘린더',
|
||||
'checklist': '체크리스트'
|
||||
};
|
||||
return texts[category] || '미분류';
|
||||
}
|
||||
|
||||
// 전역으로 사용 가능하도록 export
|
||||
window.loadTodos = loadTodos;
|
||||
window.openCamera = openCamera;
|
||||
window.openGallery = openGallery;
|
||||
window.removePhoto = removePhoto;
|
||||
window.clearForm = clearForm;
|
||||
window.toggleTodo = toggleTodo;
|
||||
window.deleteTodo = deleteTodo;
|
||||
window.editTodo = editTodo;
|
||||
window.filterTodos = filterTodos;
|
||||
window.goToPage = goToPage;
|
||||
window.goToDashboard = goToDashboard;
|
||||
window.goToClassify = goToClassify;
|
||||
window.showClassificationModal = showClassificationModal;
|
||||
window.updateItemCounts = updateItemCounts;
|
||||
310
frontend/todo.html
Normal file
@@ -0,0 +1,310 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Todo - 시작 날짜가 있는 일들</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);
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.todo-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
</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-calendar-day text-2xl text-blue-500 mr-3"></i>
|
||||
<h1 class="text-xl font-semibold text-gray-800">Todo</h1>
|
||||
<span class="ml-3 text-sm text-gray-500">시작 날짜가 있는 일들</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="goToDashboard()" class="text-blue-600 hover:text-blue-800 font-medium">
|
||||
<i class="fas fa-chart-line mr-1"></i>대시보드
|
||||
</button>
|
||||
<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-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 페이지 설명 -->
|
||||
<div class="bg-blue-50 rounded-xl p-6 mb-8">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-calendar-day text-2xl text-blue-600 mr-3"></i>
|
||||
<h2 class="text-xl font-semibold text-blue-900">Todo 관리</h2>
|
||||
</div>
|
||||
<p class="text-blue-800 mb-4">
|
||||
시작 날짜가 정해진 일들을 관리합니다. 언제 시작할지 계획을 세우고 실행해보세요.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-blue-900 mb-1">📅 시작 예정</div>
|
||||
<div class="text-blue-700">아직 시작하지 않은 일들</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-blue-900 mb-1">🔥 진행 중</div>
|
||||
<div class="text-blue-700">현재 작업 중인 일들</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3">
|
||||
<div class="font-medium text-blue-900 mb-1">✅ 완료</div>
|
||||
<div class="text-blue-700">완료된 일들</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 및 정렬 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div class="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
||||
<button onclick="filterTodos('all')" class="filter-tab active px-4 py-2 rounded text-sm font-medium">전체</button>
|
||||
<button onclick="filterTodos('scheduled')" class="filter-tab px-4 py-2 rounded text-sm font-medium">시작 예정</button>
|
||||
<button onclick="filterTodos('active')" class="filter-tab px-4 py-2 rounded text-sm font-medium">진행 중</button>
|
||||
<button onclick="filterTodos('completed')" class="filter-tab px-4 py-2 rounded text-sm font-medium">완료</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<label class="text-sm text-gray-600">정렬:</label>
|
||||
<select id="sortBy" class="border border-gray-300 rounded-lg px-3 py-1 text-sm">
|
||||
<option value="start_date">시작일 순</option>
|
||||
<option value="created_at">등록일 순</option>
|
||||
<option value="priority">우선순위 순</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Todo 목록 -->
|
||||
<div class="bg-white rounded-xl shadow-sm">
|
||||
<div class="p-6 border-b">
|
||||
<h3 class="text-lg font-semibold text-gray-800">
|
||||
<i class="fas fa-list text-blue-500 mr-2"></i>Todo 목록
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div id="todoList" class="divide-y divide-gray-100">
|
||||
<!-- Todo 항목들이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="p-12 text-center text-gray-500">
|
||||
<i class="fas fa-calendar-day text-4xl mb-4 opacity-50"></i>
|
||||
<p>아직 시작 날짜가 설정된 일이 없습니다.</p>
|
||||
<p class="text-sm">메인 페이지에서 항목을 등록하고 시작 날짜를 설정해보세요!</p>
|
||||
<button onclick="goBack()" class="mt-4 btn-primary px-6 py-2 rounded-lg">
|
||||
<i class="fas fa-arrow-left mr-2"></i>메인으로 돌아가기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="static/js/auth.js"></script>
|
||||
<script>
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuthStatus();
|
||||
loadTodoItems();
|
||||
});
|
||||
|
||||
// 뒤로 가기
|
||||
function goBack() {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// Todo 항목 로드
|
||||
function loadTodoItems() {
|
||||
// 임시 데이터 (실제로는 API에서 가져옴)
|
||||
const todoItems = [
|
||||
{
|
||||
id: 1,
|
||||
content: '프로젝트 기획서 작성',
|
||||
photo: null,
|
||||
start_date: '2024-01-20',
|
||||
status: 'scheduled',
|
||||
created_at: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '팀 미팅 준비',
|
||||
photo: null,
|
||||
start_date: '2024-01-18',
|
||||
status: 'active',
|
||||
created_at: '2024-01-16'
|
||||
}
|
||||
];
|
||||
|
||||
renderTodoItems(todoItems);
|
||||
}
|
||||
|
||||
// Todo 항목 렌더링
|
||||
function renderTodoItems(items) {
|
||||
const todoList = document.getElementById('todoList');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (items.length === 0) {
|
||||
todoList.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
todoList.innerHTML = items.map(item => `
|
||||
<div class="todo-item p-6">
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- 상태 아이콘 -->
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center ${getStatusColor(item.status)}">
|
||||
<i class="fas ${getStatusIcon(item.status)} text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사진 (있는 경우) -->
|
||||
${item.photo ? `
|
||||
<div class="flex-shrink-0">
|
||||
<img src="${item.photo}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- 내용 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-gray-900 font-medium mb-2">${item.content}</h4>
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<span>
|
||||
<i class="fas fa-calendar mr-1"></i>시작일: ${formatDate(item.start_date)}
|
||||
</span>
|
||||
<span>
|
||||
<i class="fas fa-clock mr-1"></i>등록: ${formatDate(item.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="flex-shrink-0 flex space-x-2">
|
||||
${item.status !== 'completed' ? `
|
||||
<button onclick="startTodo(${item.id})" class="text-blue-500 hover:text-blue-700" title="시작하기">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
<button onclick="completeTodo(${item.id})" class="text-green-500 hover:text-green-700" title="완료하기">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="editTodo(${item.id})" class="text-gray-400 hover:text-blue-500" title="수정하기">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 상태별 색상
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
scheduled: 'bg-blue-100 text-blue-600',
|
||||
active: 'bg-orange-100 text-orange-600',
|
||||
completed: 'bg-green-100 text-green-600'
|
||||
};
|
||||
return colors[status] || 'bg-gray-100 text-gray-600';
|
||||
}
|
||||
|
||||
// 상태별 아이콘
|
||||
function getStatusIcon(status) {
|
||||
const icons = {
|
||||
scheduled: 'fa-calendar',
|
||||
active: 'fa-play',
|
||||
completed: 'fa-check'
|
||||
};
|
||||
return icons[status] || 'fa-circle';
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR');
|
||||
}
|
||||
|
||||
// Todo 시작
|
||||
function startTodo(id) {
|
||||
console.log('Todo 시작:', id);
|
||||
// TODO: API 호출하여 상태를 'active'로 변경
|
||||
}
|
||||
|
||||
// Todo 완료
|
||||
function completeTodo(id) {
|
||||
console.log('Todo 완료:', id);
|
||||
// TODO: API 호출하여 상태를 'completed'로 변경
|
||||
}
|
||||
|
||||
// Todo 편집
|
||||
function editTodo(id) {
|
||||
console.log('Todo 편집:', id);
|
||||
// TODO: 편집 모달 또는 페이지로 이동
|
||||
}
|
||||
|
||||
// 필터링
|
||||
function filterTodos(filter) {
|
||||
console.log('필터:', filter);
|
||||
// TODO: 필터에 따라 목록 재로드
|
||||
}
|
||||
|
||||
// 대시보드로 이동
|
||||
function goToDashboard() {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
|
||||
// 전역 함수 등록
|
||||
window.goToDashboard = goToDashboard;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||