/** * 할일관리 애플리케이션 */ 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: this.formatDateLocal(new Date()), 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 startDate = new Date(this.scheduleForm.start_date + 'T00:00:00'); const response = await window.api.post(`/todos/${this.currentTodo.id}/schedule`, { start_date: startDate.toISOString(), estimated_minutes: parseInt(this.scheduleForm.estimated_minutes) }); // 할일 업데이트 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.currentTodo = todo; this.delayForm = { delayed_until: this.formatDateTimeLocal(new Date(Date.now() + 24 * 60 * 60 * 1000)) // 내일 }; this.showDelayModal = true; }, // 지연 모달 닫기 closeDelayModal() { this.showDelayModal = false; this.currentTodo = null; }, // 할일 지연 async delayTodo() { if (!this.currentTodo || !this.delayForm.delayed_until) return; try { const response = await window.api.put(`/todos/${this.currentTodo.id}/delay`, { delayed_until: new Date(this.delayForm.delayed_until).toISOString() }); // 할일 업데이트 const index = this.todos.findIndex(t => t.id === this.currentTodo.id); if (index !== -1) { this.todos[index] = response; } await this.loadStats(); this.closeDelayModal(); console.log('✅ 할일 지연 설정 완료'); } catch (error) { console.error('❌ 할일 지연 실패:', error); alert('할일 지연 설정 중 오류가 발생했습니다.'); } }, // 댓글 모달 열기 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); }, // 햅틱 피드백 (모바일) 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] || []; } }; } // 모바일 감지 및 초기화 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'; } }); // 캘린더 컴포넌트 (메인 컴포넌트에 통합) function calendarComponent() { return { currentDate: new Date(), init() { // 부모 컴포넌트 참조 설정 this.parentApp = this.$el.closest('[x-data*="todosApp"]').__x.$data; }, get calendarDays() { if (!this.parentApp) return []; const year = this.currentDate.getFullYear(); const month = this.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 days = []; const today = new Date(); const todayString = today.toISOString().slice(0, 10); for (let i = 0; i < 42; i++) { const currentDay = new Date(startDate); currentDay.setDate(startDate.getDate() + i); const dateString = currentDay.toISOString().slice(0, 10); const isCurrentMonth = currentDay.getMonth() === month; const isToday = dateString === todayString; // 해당 날짜의 할일들 찾기 const dayTodos = this.parentApp.todos.filter(todo => { if (!todo.start_date) return false; const todoDate = new Date(todo.start_date).toISOString().slice(0, 10); return todoDate === dateString && (todo.status === 'scheduled' || todo.status === 'active'); }); days.push({ day: currentDay.getDate(), dateString: dateString, isCurrentMonth: isCurrentMonth, isToday: isToday, todos: dayTodos }); } return days; }, formatMonthYear(date) { return date.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long' }); }, formatSelectedDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('ko-KR', { month: 'long', day: 'numeric', weekday: 'short' }); }, previousMonth() { this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, 1); }, nextMonth() { this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, 1); }, selectDate(dateString) { if (this.parentApp) { this.parentApp.scheduleForm.start_date = dateString; } }, selectDelayDate(dateString) { if (this.parentApp) { this.parentApp.delayForm.delayed_until = dateString; } }, getSelectedDateTodos() { if (!this.parentApp || !this.parentApp.scheduleForm.start_date) return []; return this.parentApp.todos.filter(todo => { if (!todo.start_date) return false; const todoDate = new Date(todo.start_date).toISOString().slice(0, 10); return todoDate === this.parentApp.scheduleForm.start_date && (todo.status === 'scheduled' || todo.status === 'active'); }); }, getDelayDateTodos() { if (!this.parentApp || !this.parentApp.delayForm.delayed_until) return []; return this.parentApp.todos.filter(todo => { if (!todo.start_date) return false; const todoDate = new Date(todo.start_date).toISOString().slice(0, 10); return todoDate === this.parentApp.delayForm.delayed_until && (todo.status === 'scheduled' || todo.status === 'active'); }); } }; } console.log('📋 할일관리 컴포넌트 등록 완료');