Initial commit: Todo Project with dashboard, classification center, and upload functionality

- 📱 PWA 지원: 홈화면 추가 가능한 Progressive Web App
- 🎨 M-Project 색상 스키마: 하늘색, 주황색, 회색, 흰색 일관된 디자인
- 📊 대시보드: 데스크톱 캘린더 뷰 + 모바일 일일 뷰 반응형 디자인
- 📥 분류 센터: Gmail 스타일 받은편지함으로 스마트 분류 시스템
- 🤖 AI 분류 제안: 키워드 기반 자동 분류 제안 및 일괄 처리
- 📷 업로드 모달: 데스크톱(파일 선택) + 모바일(카메라/갤러리) 최적화
- 🏷️ 3가지 분류: Todo(시작일), 캘린더(마감일), 체크리스트(무기한)
- 📋 체크리스트: 진행률 표시 및 완료 토글 기능
- 🔄 시놀로지 연동 준비: 메일플러스 연동을 위한 구조 설계
- 📱 반응형 UI: 모든 페이지 모바일 최적화 완료
This commit is contained in:
Hyungi Ahn
2025-09-19 08:52:49 +09:00
commit 761757c12e
60 changed files with 10281 additions and 0 deletions

400
frontend/calendar.html Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

282
frontend/index.html Normal file
View 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
View 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": "모바일 메인 화면"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

165
frontend/static/js/api.js Normal file
View 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
View 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;

View 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
View 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
View 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>