/** * 할일관리 애플리케이션 */ console.log('📋 할일관리 JavaScript 로드 완료'); function todosApp() { return { // 상태 관리 loading: false, activeTab: 'todo', // draft, todo, completed // 할일 데이터 todos: [], stats: { total_count: 0, draft_count: 0, scheduled_count: 0, active_count: 0, completed_count: 0, delayed_count: 0, completion_rate: 0 }, // 입력 폼 newTodoContent: '', // 모달 상태 showScheduleModal: false, showDelayModal: false, showCommentModal: false, showSplitModal: false, // 현재 선택된 할일 currentTodo: null, currentTodoComments: [], // 메모 상태 (각 할일별) todoMemos: {}, showMemoForTodo: {}, // 폼 데이터 scheduleForm: { start_date: '', estimated_minutes: 30 }, delayForm: { delayed_until: '' }, commentForm: { content: '' }, splitForm: { subtasks: ['', ''], estimated_minutes_per_task: [30, 30] }, // 계산된 속성들 get draftTodos() { return this.todos.filter(todo => todo.status === 'draft'); }, get activeTodos() { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); return this.todos.filter(todo => { // active 상태이거나, scheduled인데 날짜가 오늘이거나 지난 경우 if (todo.status === 'active') return true; if (todo.status === 'scheduled' && todo.start_date) { const startDate = new Date(todo.start_date); const startDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); return startDay <= today; } return false; }); }, get scheduledTodos() { return this.todos.filter(todo => todo.status === 'scheduled'); }, get completedTodos() { return this.todos.filter(todo => todo.status === 'completed'); }, // 초기화 async init() { console.log('📋 할일관리 초기화 중...'); // API 로드 대기 let retryCount = 0; while (!window.api && retryCount < 50) { await new Promise(resolve => setTimeout(resolve, 100)); retryCount++; } if (!window.api) { console.error('❌ API가 로드되지 않았습니다.'); return; } await this.loadTodos(); await this.loadStats(); await this.loadAllTodoMemos(); // 주기적으로 활성 할일 업데이트 (1분마다) setInterval(() => { this.loadActiveTodos(); }, 60000); }, // 할일 목록 로드 async loadTodos() { try { this.loading = true; const response = await window.api.get('/todos/'); this.todos = response || []; console.log(`✅ ${this.todos.length}개 할일 로드 완료`); } catch (error) { console.error('❌ 할일 목록 로드 실패:', error); alert('할일 목록을 불러오는 중 오류가 발생했습니다.'); } finally { this.loading = false; } }, // 활성 할일 로드 (시간 체크 포함) async loadActiveTodos() { try { const response = await window.api.get('/todos/active'); const activeTodos = response || []; // 기존 todos에서 active 상태 업데이트 this.todos = this.todos.map(todo => { const activeVersion = activeTodos.find(active => active.id === todo.id); return activeVersion || todo; }); // 새로 활성화된 할일들 추가 activeTodos.forEach(activeTodo => { if (!this.todos.find(todo => todo.id === activeTodo.id)) { this.todos.push(activeTodo); } }); await this.loadStats(); } catch (error) { console.error('❌ 활성 할일 로드 실패:', error); } }, // 통계 로드 async loadStats() { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const stats = { total_count: this.todos.length, draft_count: this.todos.filter(t => t.status === 'draft').length, todo_count: this.todos.filter(t => { // active 상태이거나, scheduled인데 날짜가 오늘이거나 지난 경우 if (t.status === 'active') return true; if (t.status === 'scheduled' && t.start_date) { const startDate = new Date(t.start_date); const startDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); return startDay <= today; } return false; }).length, completed_count: this.todos.filter(t => t.status === 'completed').length }; stats.completion_rate = stats.total_count > 0 ? Math.round((stats.completed_count / stats.total_count) * 100) : 0; this.stats = stats; }, // 새 할일 생성 async createTodo() { if (!this.newTodoContent.trim()) return; try { this.loading = true; const response = await window.api.post('/todos/', { content: this.newTodoContent.trim() }); this.todos.unshift(response); // 새 할일의 메모 상태 초기화 this.todoMemos[response.id] = []; this.showMemoForTodo[response.id] = false; this.newTodoContent = ''; await this.loadStats(); console.log('✅ 새 할일 생성 완료'); // 검토필요 탭으로 이동 this.activeTab = 'draft'; } catch (error) { console.error('❌ 할일 생성 실패:', error); alert('할일 생성 중 오류가 발생했습니다.'); } finally { this.loading = false; } }, // 일정 설정 모달 열기 openScheduleModal(todo) { this.currentTodo = todo; // 기존 값이 있으면 사용, 없으면 기본값 this.scheduleForm = { start_date: todo.start_date ? new Date(todo.start_date).toISOString().slice(0, 10) : this.formatDateLocal(new Date()), estimated_minutes: todo.estimated_minutes || 30 }; this.showScheduleModal = true; }, // 일정 설정 모달 닫기 closeScheduleModal() { this.showScheduleModal = false; this.currentTodo = null; }, // 할일 일정 설정 async scheduleTodo() { if (!this.currentTodo || !this.scheduleForm.start_date) return; try { // 선택한 날짜의 총 시간 체크 const selectedDate = this.scheduleForm.start_date; const newMinutes = parseInt(this.scheduleForm.estimated_minutes); // 해당 날짜의 기존 할일들 시간 합계 const existingMinutes = this.todos .filter(todo => { if (!todo.start_date || todo.id === this.currentTodo.id) return false; const todoDate = new Date(todo.start_date).toISOString().slice(0, 10); return todoDate === selectedDate && (todo.status === 'scheduled' || todo.status === 'active'); }) .reduce((sum, todo) => sum + (todo.estimated_minutes || 0), 0); const totalMinutes = existingMinutes + newMinutes; const totalHours = Math.round(totalMinutes / 60 * 10) / 10; // 8시간 초과 시 경고 if (totalMinutes > 480) { // 8시간 = 480분 const choice = await this.showOverworkWarning(selectedDate, totalHours); if (choice === 'cancel') return; if (choice === 'change') { // 모달을 닫지 않고 사용자가 다시 선택할 수 있도록 함 return; } // choice === 'continue'인 경우 계속 진행 } // 날짜만 사용하여 해당 날짜의 시작 시간으로 설정 const startDate = new Date(this.scheduleForm.start_date + 'T00:00:00'); let response; // 이미 일정이 설정된 할일인지 확인 if (this.currentTodo.status === 'draft') { // 새로 일정 설정 response = await window.api.post(`/todos/${this.currentTodo.id}/schedule`, { start_date: startDate.toISOString(), estimated_minutes: newMinutes }); } else { // 기존 일정 지연 (active 상태의 할일) response = await window.api.put(`/todos/${this.currentTodo.id}/delay`, { delayed_until: startDate.toISOString() }); } // 할일 업데이트 const index = this.todos.findIndex(t => t.id === this.currentTodo.id); if (index !== -1) { this.todos[index] = response; } await this.loadStats(); this.closeScheduleModal(); console.log('✅ 할일 일정 설정 완료'); } catch (error) { console.error('❌ 일정 설정 실패:', error); if (error.message.includes('split')) { alert('2시간 이상의 작업은 분할하는 것을 권장합니다.'); } else { alert('일정 설정 중 오류가 발생했습니다.'); } } }, // 할일 완료 async completeTodo(todoId) { try { const response = await window.api.put(`/todos/${todoId}/complete`); // 할일 업데이트 const index = this.todos.findIndex(t => t.id === todoId); if (index !== -1) { this.todos[index] = response; } await this.loadStats(); console.log('✅ 할일 완료'); } catch (error) { console.error('❌ 할일 완료 실패:', error); alert('할일 완료 처리 중 오류가 발생했습니다.'); } }, // 지연 모달 열기 (일정 설정 모달 재사용) - 더 이상 사용하지 않음 openDelayModal(todo) { // 일정변경과 동일하게 처리 this.openScheduleModal(todo); }, // 댓글 모달 열기 async openCommentModal(todo) { this.currentTodo = todo; this.commentForm = { content: '' }; try { const response = await window.api.get(`/todos/${todo.id}/comments`); this.currentTodoComments = response || []; } catch (error) { console.error('❌ 댓글 로드 실패:', error); this.currentTodoComments = []; } this.showCommentModal = true; }, // 댓글 모달 닫기 closeCommentModal() { this.showCommentModal = false; this.currentTodo = null; this.currentTodoComments = []; }, // 댓글 추가 async addComment() { if (!this.currentTodo || !this.commentForm.content.trim()) return; try { const response = await window.api.post(`/todos/${this.currentTodo.id}/comments`, { content: this.commentForm.content.trim() }); this.currentTodoComments.push(response); this.commentForm.content = ''; // 할일의 댓글 수 업데이트 const index = this.todos.findIndex(t => t.id === this.currentTodo.id); if (index !== -1) { this.todos[index].comment_count = this.currentTodoComments.length; } console.log('✅ 댓글 추가 완료'); } catch (error) { console.error('❌ 댓글 추가 실패:', error); alert('댓글 추가 중 오류가 발생했습니다.'); } }, // 분할 모달 열기 openSplitModal(todo) { this.currentTodo = todo; this.splitForm = { subtasks: ['', ''], estimated_minutes_per_task: [30, 30] }; this.showSplitModal = true; }, // 분할 모달 닫기 closeSplitModal() { this.showSplitModal = false; this.currentTodo = null; }, // 할일 분할 async splitTodo() { if (!this.currentTodo) return; const validSubtasks = this.splitForm.subtasks.filter(s => s.trim()); const validMinutes = this.splitForm.estimated_minutes_per_task.slice(0, validSubtasks.length); if (validSubtasks.length < 2) { alert('최소 2개의 하위 작업이 필요합니다.'); return; } try { const response = await window.api.post(`/todos/${this.currentTodo.id}/split`, { subtasks: validSubtasks, estimated_minutes_per_task: validMinutes }); // 원본 할일 제거하고 분할된 할일들 추가 this.todos = this.todos.filter(t => t.id !== this.currentTodo.id); this.todos.unshift(...response); await this.loadStats(); this.closeSplitModal(); console.log('✅ 할일 분할 완료'); } catch (error) { console.error('❌ 할일 분할 실패:', error); alert('할일 분할 중 오류가 발생했습니다.'); } }, // 댓글 토글 async toggleComments(todoId) { const todo = this.todos.find(t => t.id === todoId); if (todo) { await this.openCommentModal(todo); } }, // 유틸리티 함수들 formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return '방금 전'; if (diffMins < 60) return `${diffMins}분 전`; if (diffHours < 24) return `${diffHours}시간 전`; if (diffDays < 7) return `${diffDays}일 전`; return date.toLocaleDateString('ko-KR'); }, formatDateLocal(date) { const d = new Date(date); return d.toISOString().slice(0, 10); }, formatRelativeTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMinutes < 1) return '방금 전'; if (diffMinutes < 60) return `${diffMinutes}분 전`; if (diffHours < 24) return `${diffHours}시간 전`; if (diffDays < 7) return `${diffDays}일 전`; return date.toLocaleDateString('ko-KR'); }, // 햅틱 피드백 (모바일) hapticFeedback(element) { // 진동 API 지원 확인 if ('vibrate' in navigator) { navigator.vibrate(50); // 50ms 진동 } // 시각적 피드백 if (element) { element.classList.add('haptic-feedback'); setTimeout(() => { element.classList.remove('haptic-feedback'); }, 100); } }, // 풀 투 리프레시 (모바일) handlePullToRefresh() { let startY = 0; let currentY = 0; let pullDistance = 0; const threshold = 100; document.addEventListener('touchstart', (e) => { if (window.scrollY === 0) { startY = e.touches[0].clientY; } }); document.addEventListener('touchmove', (e) => { if (startY > 0) { currentY = e.touches[0].clientY; pullDistance = currentY - startY; if (pullDistance > 0 && pullDistance < threshold) { // 당기는 중 시각적 피드백 const opacity = pullDistance / threshold; document.body.style.background = `linear-gradient(to bottom, rgba(99, 102, 241, ${opacity * 0.1}) 0%, #f9fafb 100%)`; } } }); document.addEventListener('touchend', async () => { if (pullDistance > threshold) { // 새로고침 실행 await this.loadTodos(); await this.loadActiveTodos(); // 햅틱 피드백 if ('vibrate' in navigator) { navigator.vibrate([100, 50, 100]); } } // 리셋 startY = 0; pullDistance = 0; document.body.style.background = ''; }); }, // 모든 할일의 메모 로드 (초기화용) async loadAllTodoMemos() { try { console.log('📋 모든 할일 메모 로드 중...'); // 모든 할일에 대해 메모 로드 const memoPromises = this.todos.map(async (todo) => { try { const response = await window.api.get(`/todos/${todo.id}/comments`); this.todoMemos[todo.id] = response || []; if (this.todoMemos[todo.id].length > 0) { console.log(`✅ ${todo.content.slice(0, 20)}... - ${this.todoMemos[todo.id].length}개 메모 로드`); } } catch (error) { console.error(`❌ ${todo.id} 메모 로드 실패:`, error); this.todoMemos[todo.id] = []; } }); await Promise.all(memoPromises); console.log('✅ 모든 할일 메모 로드 완료'); } catch (error) { console.error('❌ 전체 메모 로드 실패:', error); } }, // 할일 메모 로드 (인라인용) async loadTodoMemos(todoId) { try { const response = await window.api.get(`/todos/${todoId}/comments`); this.todoMemos[todoId] = response || []; return this.todoMemos[todoId]; } catch (error) { console.error('❌ 메모 로드 실패:', error); this.todoMemos[todoId] = []; return []; } }, // 할일 메모 추가 (인라인용) async addTodoMemo(todoId, content) { try { const response = await window.api.post(`/todos/${todoId}/comments`, { content: content.trim() }); // 메모 목록 새로고침 await this.loadTodoMemos(todoId); console.log('✅ 메모 추가 완료'); return response; } catch (error) { console.error('❌ 메모 추가 실패:', error); alert('메모 추가 중 오류가 발생했습니다.'); throw error; } }, // 메모 토글 toggleMemo(todoId) { this.showMemoForTodo[todoId] = !this.showMemoForTodo[todoId]; // 메모가 처음 열릴 때만 로드 if (this.showMemoForTodo[todoId] && !this.todoMemos[todoId]) { this.loadTodoMemos(todoId); } }, // 특정 할일의 메모 개수 가져오기 getTodoMemoCount(todoId) { return this.todoMemos[todoId] ? this.todoMemos[todoId].length : 0; }, // 특정 할일의 메모 목록 가져오기 getTodoMemos(todoId) { return this.todoMemos[todoId] || []; }, // 8시간 초과 경고 모달 표시 showOverworkWarning(selectedDate, totalHours) { return new Promise((resolve) => { // 기존 경고 모달이 있으면 제거 const existingModal = document.getElementById('overwork-warning-modal'); if (existingModal) { existingModal.remove(); } // 모달 HTML 생성 const modalHTML = `

⚠️ 과로 경고

하루 권장 작업시간을 초과했습니다

${selectedDate}의 총 작업시간이 ${totalHours}시간이 됩니다.

건강한 작업을 위해 하루 8시간 이내로 계획하는 것을 권장합니다.

`; // 모달을 body에 추가 document.body.insertAdjacentHTML('beforeend', modalHTML); // 전역 함수로 resolve 설정 window.resolveOverworkWarning = (choice) => { const modal = document.getElementById('overwork-warning-modal'); if (modal) { modal.remove(); } delete window.resolveOverworkWarning; resolve(choice); }; }); } }; } // 모바일 감지 및 초기화 function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform)); } // 모바일 최적화 초기화 document.addEventListener('DOMContentLoaded', () => { if (isMobile()) { // 모바일 전용 스타일 추가 document.body.classList.add('mobile-optimized'); // iOS Safari 주소창 숨김 대응 const setVH = () => { const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); }; setVH(); window.addEventListener('resize', setVH); window.addEventListener('orientationchange', setVH); // 터치 스크롤 개선 document.body.style.webkitOverflowScrolling = 'touch'; } }); console.log('📋 할일관리 컴포넌트 등록 완료');