🔔
👤
오늘 할 일
할 일이 없습니다
할 일 추가
// ── 한국 공휴일 ── const FIXED_HOLIDAYS = { '01-01': '신정', '03-01': '삼일절', '05-05': '어린이날', '06-06': '현충일', '08-15': '광복절', '10-03': '개천절', '10-09': '한글날', '12-25': '크리스마스' }; const LUNAR_HOLIDAYS = { 2025: ['01-28','01-29','01-30','05-06','10-05','10-06','10-07'], 2026: ['02-17','02-18','02-19','05-24','09-24','09-25','09-26'], 2027: ['02-06','02-07','02-08','05-13','09-14','09-15','09-16'], }; function isHoliday(year, month, day) { const mmdd = String(month + 1).padStart(2, '0') + '-' + String(day).padStart(2, '0'); if (FIXED_HOLIDAYS[mmdd]) return true; const lunar = LUNAR_HOLIDAYS[year] || []; return lunar.includes(mmdd); } // ── 상태 ── const today = new Date(); today.setHours(0, 0, 0, 0); let selectedDate = new Date(today); let calYear = today.getFullYear(); let calMonth = today.getMonth(); let currentView = 'today'; let currentFilter = 'all'; let editingId = null; // 현재 편집 중인 태스크 ID function dateKey(d) { return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); } const todayKey = dateKey(today); function loadTasks() { try { return JSON.parse(localStorage.getItem('mytodo_tasks') || '[]'); } catch(e) { return []; } } function saveTasks(tasks) { localStorage.setItem('mytodo_tasks', JSON.stringify(tasks)); } // ── 달력 렌더링 ── function renderCalendar() { const firstDay = new Date(calYear, calMonth, 1).getDay(); const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate(); const daysInPrev = new Date(calYear, calMonth, 0).getDate(); document.getElementById('calMonthLabel').textContent = calYear + '년 ' + (calMonth + 1) + '월'; const tasks = loadTasks(); const datesWithTasks = new Set(tasks.map(t => t.date)); const totalNeeded = firstDay + daysInMonth; let totalCells = Math.ceil(totalNeeded / 7) * 7; let html = ''; let currentDay = 1; let nextMonthDay = 1; for (let i = 0; i < totalCells; i++) { if (i % 7 === 0) html += ''; const col = i % 7; let isThisMonth = true; let dayNum = 0; let cellDate = null; if (i < firstDay) { dayNum = daysInPrev - firstDay + i + 1; isThisMonth = false; cellDate = new Date(calYear, calMonth - 1, dayNum); } else if (currentDay > daysInMonth) { dayNum = nextMonthDay++; isThisMonth = false; cellDate = new Date(calYear, calMonth + 1, dayNum); } else { dayNum = currentDay++; cellDate = new Date(calYear, calMonth, dayNum); } if (!isThisMonth && cellDate.getMonth() !== calMonth) { const isPrevMonth = cellDate < new Date(calYear, calMonth, 1); if (!isPrevMonth) { html += ''; if (i % 7 === 6) html += ''; continue; } } let cls = 'cal-day'; if (!isThisMonth) cls += ' other-month'; if (col === 0) cls += ' sun'; if (col === 6) cls += ' sat'; if (isThisMonth && isHoliday(cellDate.getFullYear(), cellDate.getMonth(), cellDate.getDate())) cls += ' holiday'; const dKey = dateKey(cellDate); if (dKey === todayKey) cls += ' today'; else if (dKey === dateKey(selectedDate)) cls += ' selected'; const hasDot = datesWithTasks.has(dKey); const dot = hasDot ? '' : ''; html += `
${dayNum}${dot}
`; if (i % 7 === 6) html += ''; } document.getElementById('calBody').innerHTML = html; } function selectDate(y, m, d) { selectedDate = new Date(y, m, d); if (currentView !== 'work' && currentView !== 'personal') { currentView = 'today'; updateNavActive(); } editingId = null; renderAll(); } document.getElementById('calPrev').onclick = () => { calMonth--; if (calMonth < 0) { calMonth = 11; calYear--; } renderCalendar(); }; document.getElementById('calNext').onclick = () => { calMonth++; if (calMonth > 11) { calMonth = 0; calYear++; } renderCalendar(); }; function setView(v) { currentView = v; if (v === 'today') { selectedDate = new Date(today); calYear = today.getFullYear(); calMonth = today.getMonth(); } currentFilter = 'all'; editingId = null; document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active')); document.querySelector('.filter-tab[data-filter="all"]').classList.add('active'); updateNavActive(); renderAll(); } function setFilter(f) { currentFilter = f; document.querySelectorAll('.filter-tab').forEach(t => { t.classList.toggle('active', t.dataset.filter === f); }); renderTaskList(); } function updateNavActive() { document.querySelectorAll('.nav-item').forEach(el => { el.classList.toggle('active', el.dataset.view === currentView); }); } document.getElementById('taskInput').addEventListener('keydown', e => { if (e.key === 'Enter') addTask(); }); function addTask() { const input = document.getElementById('taskInput'); const text = input.value.trim(); if (!text) return; const cat = document.getElementById('catSelect').value; const time = document.getElementById('timeInput').value; const priority = document.getElementById('prioritySelect').value; const tasks = loadTasks(); tasks.push({ id: Date.now(), text, category: cat, time: time || '', done: false, date: dateKey(selectedDate), priority: priority || 'medium' }); saveTasks(tasks); input.value = ''; document.getElementById('timeInput').value = ''; document.getElementById('prioritySelect').value = 'medium'; renderAll(); } function deleteTask(id) { if (editingId === id) editingId = null; saveTasks(loadTasks().filter(t => t.id !== id)); renderAll(); } function toggleTask(id) { const tasks = loadTasks(); const t = tasks.find(t => t.id === id); if (t) t.done = !t.done; saveTasks(tasks); renderAll(); } // ── 인라인 편집 ── function startEdit(id) { editingId = id; renderTaskList(); // 편집 UI 표시 // 포커스 설정 const inp = document.getElementById('edit-text-' + id); if (inp) { inp.focus(); inp.select(); } } function cancelEdit() { editingId = null; renderTaskList(); } function saveEdit(id) { const newText = (document.getElementById('edit-text-' + id).value || '').trim(); if (!newText) return; const newCat = document.getElementById('edit-cat-' + id).value; const newTime = document.getElementById('edit-time-' + id).value; const newPriority = document.getElementById('edit-priority-' + id).value; const tasks = loadTasks(); const t = tasks.find(t => t.id === id); if (t) { t.text = newText; t.category = newCat; t.time = newTime || ''; t.priority = newPriority || 'medium'; } saveTasks(tasks); editingId = null; renderAll(); } function handleEditKeydown(e, id) { if (e.key === 'Enter') saveEdit(id); if (e.key === 'Escape') cancelEdit(); } function getFilteredTasks() { const all = loadTasks(); const selKey = dateKey(selectedDate); if (currentView === 'today') return all.filter(t => t.date === selKey); if (currentView === 'upcoming') return all.filter(t => t.date > todayKey && !t.done); if (currentView === 'done') return all.filter(t => t.done); if (currentView === 'work') { const base = all.filter(t => t.category === 'work'); return currentFilter === 'all' ? base : base.filter(t => t.category === currentFilter); } if (currentView === 'personal') { const base = all.filter(t => t.category === 'personal'); return currentFilter === 'all' ? base : base.filter(t => t.category === currentFilter); } return []; } function renderTaskList() { const tasks = getFilteredTasks(); const list = document.getElementById('taskList'); const filterTabs = document.getElementById('filterTabs'); const addRow = document.querySelector('.add-task-row'); filterTabs.style.display = (currentView === 'work' || currentView === 'personal') ? 'flex' : 'none'; addRow.style.display = currentView === 'today' ? 'flex' : 'none'; if (!tasks.length) { list.innerHTML = '
📋
할 일이 없습니다
'; updateProgress(0, 0); return; } updateProgress(tasks.filter(t => t.done).length, tasks.length); list.innerHTML = tasks.map(t => { const badgeCls = t.category === 'work' ? 'badge-work' : 'badge-personal'; const badgeLabel = t.category === 'work' ? '업무' : '개인'; const timeStr = t.time ? `🕐 ${t.time}` : ''; const doneLabel = t.done ? '· 완료' : ''; const dateStr = currentView !== 'today' ? `📅 ${formatDateLabel(t.date)}` : ''; const isEditing = editingId === t.id; // 중요도 배지 const prio = t.priority || 'medium'; const prioMap = { high: { cls: 'priority-high', label: '🔴 높음' }, medium: { cls: 'priority-medium', label: '🟡 보통' }, low: { cls: 'priority-low', label: '🟢 낮음' } }; const prioBadge = `${prioMap[prio].label}`; // 편집 모드이고 완료된 항목은 편집 불가 const canEdit = !t.done; // 편집 버튼 (완료된 항목은 숨김) const editBtn = canEdit ? `` : ''; // 편집 영역 (열린 경우에만) const editArea = isEditing ? `
` : ''; return `
${escHtml(t.text)} ${timeStr}${dateStr} ${editArea}
${prioBadge} ${badgeLabel} ${doneLabel} ${editBtn}
`; }).join(''); } function formatDateLabel(key) { const [y, m, d] = key.split('-').map(Number); return m + '월 ' + d + '일'; } function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function updateProgress(done, total) { const pct = total > 0 ? Math.round(done / total * 100) : 0; const remaining = total - done; document.getElementById('progressFill').style.width = pct + '%'; document.getElementById('progressFraction').textContent = total > 0 ? done + ' / ' + total : ''; document.getElementById('progressPct').textContent = total > 0 ? pct + '%' : ''; document.getElementById('progressText').textContent = total === 0 ? '할 일이 없습니다' : remaining === 0 ? '모든 할 일을 완료했습니다 🎉' : remaining + '개의 할 일이 남았습니다'; } function renderHeader() { const days = ['일','월','화','수','목','금','토']; const months = ['1월','2월','3월','4월','5월','6월','7월','8월','9월','10월','11월','12월']; const sd = selectedDate; const selKey = dateKey(sd); const labelDate = sd.getFullYear() + '년 ' + months[sd.getMonth()] + ' ' + sd.getDate() + '일 (' + days[sd.getDay()] + ')'; let title = '오늘 할 일', label = ''; if (currentView === 'today') { title = selKey === todayKey ? '오늘 할 일' : selKey < todayKey ? '지난 할 일' : '예정 할 일'; label = labelDate; } else if (currentView === 'upcoming') { title = '예정된 할 일'; } else if (currentView === 'done') { title = '완료된 할 일'; } else if (currentView === 'work') { title = '업무'; } else if (currentView === 'personal') { title = '개인'; } document.getElementById('mainTitle').textContent = title; document.getElementById('mainDateLabel').textContent = label; } function renderCounts() { const all = loadTasks(); document.getElementById('countToday').textContent = all.filter(t => t.date === dateKey(selectedDate) && !t.done).length; document.getElementById('countUpcoming').textContent = all.filter(t => t.date > todayKey && !t.done).length; document.getElementById('countDone').textContent = all.filter(t => t.done).length; document.getElementById('countWork').textContent = all.filter(t => t.category === 'work' && !t.done).length; document.getElementById('countPersonal').textContent = all.filter(t => t.category === 'personal' && !t.done).length; } function renderTopDate() { const days = ['일','월','화','수','목','금','토']; const months = ['1월','2월','3월','4월','5월','6월','7월','8월','9월','10월','11월','12월']; document.getElementById('topbarDate').textContent = '오늘, ' + today.getFullYear() + '년 ' + months[today.getMonth()] + ' ' + today.getDate() + '일 (' + days[today.getDay()] + ')'; } function renderAll() { renderCalendar(); renderHeader(); renderTaskList(); renderCounts(); updateMobileCounts(); } // ── 모바일 감지 ── function isMobile() { return window.innerWidth <= 640; } // ── 메뉴버튼 표시 ── function updateMenuBtn() { const btn = document.getElementById('menuBtn'); if (btn) btn.style.display = isMobile() ? 'flex' : 'none'; } window.addEventListener('resize', updateMenuBtn); updateMenuBtn(); // ── 사이드바 토글 ── function toggleSidebar() { const sb = document.querySelector('.sidebar'); const ov = document.getElementById('sidebarOverlay'); sb.classList.toggle('open'); ov.classList.toggle('open'); } function closeSidebar() { document.querySelector('.sidebar').classList.remove('open'); document.getElementById('sidebarOverlay').classList.remove('open'); } // ── 모바일 뷰 전환 ── function mobileSetView(v) { setView(v); closeSidebar(); document.querySelectorAll('.mobile-nav-btn').forEach(b => { b.classList.toggle('active', b.dataset.view === v); }); } // ── 모달 열기/닫기 ── function openAddModal() { document.getElementById('addModalOverlay').classList.add('open'); setTimeout(() => document.getElementById('modalTaskInput').focus(), 100); } function closeAddModal() { document.getElementById('addModalOverlay').classList.remove('open'); document.getElementById('modalTaskInput').value = ''; document.getElementById('modalTimeInput').value = ''; document.getElementById('modalPrioritySelect').value = 'medium'; } function closeAddModalOutside(e) { if (e.target === document.getElementById('addModalOverlay')) closeAddModal(); } // ── 모달에서 할 일 추가 ── function addTaskModal() { const text = document.getElementById('modalTaskInput').value.trim(); if (!text) return; const cat = document.getElementById('modalCatSelect').value; const time = document.getElementById('modalTimeInput').value; const priority = document.getElementById('modalPrioritySelect').value; const tasks = loadTasks(); tasks.push({ id: Date.now(), text, category: cat, time: time || '', done: false, date: dateKey(selectedDate), priority: priority || 'medium' }); saveTasks(tasks); closeAddModal(); renderAll(); } // ── 모달에서 Enter 키 ── document.addEventListener('DOMContentLoaded', () => { const mi = document.getElementById('modalTaskInput'); if (mi) mi.addEventListener('keydown', e => { if (e.key === 'Enter') addTaskModal(); }); }); // ── 모바일 하단 네비 카운트 업데이트 ── function updateMobileCounts() { const all = loadTasks(); const todayC = all.filter(t => t.date === dateKey(selectedDate) && !t.done).length; const upcomingC = all.filter(t => t.date > todayKey && !t.done).length; const workC = all.filter(t => t.category === 'work' && !t.done).length; const personalC = all.filter(t => t.category === 'personal' && !t.done).length; const setB = (id, n) => { const el = document.getElementById(id); if (!el) return; el.textContent = n; el.style.display = n > 0 ? 'block' : 'none'; }; setB('mCountToday', todayC); setB('mCountUpcoming', upcomingC); setB('mCountWork', workC); setB('mCountPersonal', personalC); } renderTopDate(); renderAll();