diff --git a/app.js b/app.js new file mode 100644 index 0000000..aac0b59 --- /dev/null +++ b/app.js @@ -0,0 +1,232 @@ +const DAY_NAMES = ["일", "월", "화", "수", "목", "금", "토"]; + +/* ── State ── */ +let selectedDate = new Date(); +let weekOffset = 0; +let todos = loadTodos(); + +/* ── DOM Elements ── */ +const currentDateEl = document.getElementById("currentDate"); +const weekDaysEl = document.getElementById("weekDays"); +const prevWeekBtn = document.getElementById("prevWeek"); +const nextWeekBtn = document.getElementById("nextWeek"); +const todoForm = document.getElementById("todoForm"); +const todoInput = document.getElementById("todoInput"); +const todoList = document.getElementById("todoList"); +const todoCountEl = document.getElementById("todoCount"); +const themeToggleBtn = document.getElementById("themeToggle"); + +/* ── LocalStorage ── */ +function loadTodos() { + try { + return JSON.parse(localStorage.getItem("todos")) || {}; + } catch { + return {}; + } +} + +function saveTodos() { + localStorage.setItem("todos", JSON.stringify(todos)); +} + +/* ── Date Helpers ── */ +function formatDateKey(date) { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; +} + +function isSameDay(a, b) { + return formatDateKey(a) === formatDateKey(b); +} + +function getWeekDates(offset) { + const today = new Date(); + const dayOfWeek = today.getDay(); + + // 선택된 주의 일요일 날짜 계산 + const sunday = new Date(today); + sunday.setDate(today.getDate() - dayOfWeek + offset * 7); + + // 선택된 주의 날짜 설정 + const dates = []; + for (let i = 0; i < 7; i++) { + const date = new Date(sunday); + date.setDate(sunday.getDate() + i); + dates.push(date); + } + return dates; +} + +function formatDisplayDate(date) { + const y = date.getFullYear(); + const m = date.getMonth() + 1; + const d = date.getDate(); + const day = DAY_NAMES[date.getDay()]; + return `${y}년 ${m}월 ${d}일 ${day}요일`; +} + +/* ── Render: Week Navigation ── */ +function renderWeek() { + const weekDates = getWeekDates(weekOffset); + const today = new Date(); + + weekDaysEl.innerHTML = ""; + + weekDates.forEach((date) => { + const dateKey = formatDateKey(date); + const todoCount = (todos[dateKey] || []).length; + const isSelected = isSameDay(date, selectedDate); + const isToday = isSameDay(date, today); + + const li = document.createElement("li"); + li.className = "week-nav__day"; + if (isSelected) li.classList.add("week-nav__day--selected"); + if (isToday) li.classList.add("week-nav__day--today"); + + const dayName = document.createElement("span"); + dayName.className = "week-nav__day-name"; + dayName.textContent = DAY_NAMES[date.getDay()]; + + const dayNumber = document.createElement("span"); + dayNumber.className = "week-nav__day-number"; + dayNumber.textContent = date.getDate(); + + const dayCount = document.createElement("span"); + dayCount.className = "week-nav__day-count"; + dayCount.textContent = todoCount > 0 ? `${todoCount}개` : ""; + + li.appendChild(dayName); + li.appendChild(dayNumber); + li.appendChild(dayCount); + + li.addEventListener("click", () => { + selectedDate = new Date(date); + render(); + }); + + weekDaysEl.appendChild(li); + }); +} + +/* ── Render: Todo List ── */ +function renderTodos() { + const dateKey = formatDateKey(selectedDate); + const currentTodos = todos[dateKey] || []; + + todoCountEl.textContent = `${currentTodos.length}개`; + todoList.innerHTML = ""; + + if (currentTodos.length === 0) { + const emptyLi = document.createElement("li"); + emptyLi.className = "todo-list__empty"; + emptyLi.textContent = "할 일이 없습니다"; + todoList.appendChild(emptyLi); + return; + } + + currentTodos.forEach((todo, index) => { + const li = document.createElement("li"); + li.className = "todo-item"; + if (todo.done) li.classList.add("todo-item--done"); + + const checkbox = document.createElement("button"); + checkbox.className = "todo-item__checkbox"; + checkbox.setAttribute("aria-label", "완료 토글"); + checkbox.addEventListener("click", () => toggleTodo(dateKey, index)); + + const text = document.createElement("span"); + text.className = "todo-item__text"; + text.textContent = todo.text; + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "todo-item__delete"; + deleteBtn.setAttribute("aria-label", "삭제"); + deleteBtn.textContent = "×"; + deleteBtn.addEventListener("click", () => deleteTodo(dateKey, index)); + + li.appendChild(checkbox); + li.appendChild(text); + li.appendChild(deleteBtn); + todoList.appendChild(li); + }); +} + +/* ── Render All ── */ +function render() { + currentDateEl.textContent = formatDisplayDate(selectedDate); + renderWeek(); + renderTodos(); +} + +/* ── Todo Actions ── */ +function addTodo(text) { + const dateKey = formatDateKey(selectedDate); + if (!todos[dateKey]) { + todos[dateKey] = []; + } + todos[dateKey].push({ text, done: false }); + saveTodos(); + render(); +} + +function toggleTodo(dateKey, index) { + todos[dateKey][index].done = !todos[dateKey][index].done; + saveTodos(); + render(); +} + +function deleteTodo(dateKey, index) { + todos[dateKey].splice(index, 1); + if (todos[dateKey].length === 0) { + delete todos[dateKey]; + } + saveTodos(); + render(); +} + +/* ── Dark Mode ── */ +function loadTheme() { + const savedTheme = localStorage.getItem("theme"); + if (savedTheme === "dark") { + document.body.classList.add("dark"); + themeToggleBtn.textContent = "\u2600\uFE0F"; + } +} + +function toggleTheme() { + const isDark = document.body.classList.toggle("dark"); + themeToggleBtn.textContent = isDark ? "\u2600\uFE0F" : "\uD83C\uDF19"; + localStorage.setItem("theme", isDark ? "dark" : "light"); +} + +/* ── Event Listeners ── */ +themeToggleBtn.addEventListener("click", toggleTheme); + +todoForm.addEventListener("submit", (e) => { + // 불필요한 페이지 새로고침 방지 및 현재 UI 유지 + e.preventDefault(); + + // 공백 문자열만 입력 시 등록 안되도록 공백 제거 + const text = todoInput.value.trim(); + if (text) { + addTodo(text); + todoInput.value = ""; + todoInput.focus(); + } +}); + +prevWeekBtn.addEventListener("click", () => { + weekOffset--; + render(); +}); + +nextWeekBtn.addEventListener("click", () => { + weekOffset++; + render(); +}); + +/* ── Init ── */ +loadTheme(); +render(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..9902e17 --- /dev/null +++ b/index.html @@ -0,0 +1,60 @@ + + + + + + Todo가 미래다 + + + + + +
+
+
+

ToDo

+ +
+

+
+ + + +
+
+

0개

+
+ +
+ + +
+ + +
+
+ + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..6d1d47f --- /dev/null +++ b/style.css @@ -0,0 +1,428 @@ +/* Reset & Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif; + background-color: #f5f5f7; + color: #1d1d1f; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 40px 16px; +} + +/* App Container */ +.app { + width: 100%; + max-width: 480px; + display: flex; + flex-direction: column; + gap: 24px; +} + +/* Header */ +.header { + display: flex; + flex-direction: column; + gap: 4px; +} + +.header__top { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header__title { + font-size: 32px; + font-weight: 700; + color: #1d1d1f; +} + +/* Theme Toggle */ +.theme-toggle { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + padding: 4px; + border-radius: 8px; + transition: background-color 0.2s; + line-height: 1; +} + +.theme-toggle:hover { + background-color: #e8e8ed; +} + +.header__date { + font-size: 14px; + color: #86868b; + font-weight: 400; +} + +/* Week Navigation */ +.week-nav { + display: flex; + align-items: center; + gap: 8px; +} + +.week-nav__btn { + background: none; + border: none; + font-size: 16px; + color: #86868b; + cursor: pointer; + padding: 8px; + border-radius: 8px; + transition: background-color 0.2s; + flex-shrink: 0; +} + +.week-nav__btn:hover { + background-color: #e8e8ed; +} + +.week-nav__days { + display: flex; + flex: 1; + gap: 4px; + list-style: none; +} + +.week-nav__day { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 10px 4px; + border-radius: 12px; + cursor: pointer; + transition: background-color 0.2s; +} + +.week-nav__day:hover { + background-color: #e8e8ed; +} + +.week-nav__day--selected { + background-color: #1d1d1f; + color: #ffffff; +} + +.week-nav__day--selected:hover { + background-color: #333336; +} + +.week-nav__day--today { + position: relative; +} + +.week-nav__day--today::after { + content: ''; + width: 5px; + height: 5px; + background-color: #ff3b30; + border-radius: 50%; + position: absolute; + top: 4px; + right: 8px; +} + +.week-nav__day-name { + font-size: 11px; + font-weight: 500; + color: #86868b; +} + +.week-nav__day--selected .week-nav__day-name { + color: #a1a1a6; +} + +.week-nav__day-number { + font-size: 16px; + font-weight: 600; +} + +.week-nav__day-count { + font-size: 10px; + font-weight: 500; + color: #86868b; + min-height: 14px; +} + +.week-nav__day--selected .week-nav__day-count { + color: #a1a1a6; +} + +/* Todo Section */ +.todo-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.todo-section__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.todo-section__count { + font-size: 18px; + font-weight: 600; + color: #1d1d1f; +} + +/* Todo Form */ +.todo-form { + display: flex; + gap: 8px; +} + +.todo-form__input { + flex: 1; + padding: 12px 16px; + border: 1px solid #d2d2d7; + border-radius: 12px; + font-size: 14px; + font-family: inherit; + outline: none; + transition: border-color 0.2s; + background-color: #ffffff; +} + +.todo-form__input:focus { + border-color: #1d1d1f; +} + +.todo-form__btn { + padding: 12px 20px; + background-color: #1d1d1f; + color: #ffffff; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: background-color 0.2s; + flex-shrink: 0; +} + +.todo-form__btn:hover { + background-color: #333336; +} + +/* Todo List */ +.todo-list { + display: flex; + flex-direction: column; + gap: 8px; + list-style: none; +} + +.todo-item { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + background-color: #ffffff; + border-radius: 12px; + transition: opacity 0.2s; +} + +.todo-item--done { + opacity: 0.5; +} + +.todo-item__checkbox { + width: 22px; + height: 22px; + border: 2px solid #d2d2d7; + border-radius: 50%; + cursor: pointer; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + background: none; + padding: 0; +} + +.todo-item__checkbox:hover { + border-color: #1d1d1f; +} + +.todo-item--done .todo-item__checkbox { + background-color: #1d1d1f; + border-color: #1d1d1f; +} + +.todo-item--done .todo-item__checkbox::after { + content: '✓'; + color: #ffffff; + font-size: 12px; + font-weight: 700; +} + +.todo-item__text { + flex: 1; + font-size: 14px; + font-weight: 400; + word-break: break-word; +} + +.todo-item--done .todo-item__text { + text-decoration: line-through; + color: #86868b; +} + +.todo-item__delete { + background: none; + border: none; + font-size: 18px; + color: #d2d2d7; + cursor: pointer; + padding: 4px; + line-height: 1; + transition: color 0.2s; +} + +.todo-item__delete:hover { + color: #ff3b30; +} + +/* Empty State */ +.todo-list__empty { + text-align: center; + padding: 40px 0; + color: #86868b; + font-size: 14px; +} + +/* Dark Mode */ +body.dark { + background-color: #1c1c1e; + color: #f5f5f7; +} + +body.dark .header__title { + color: #f5f5f7; +} + +body.dark .theme-toggle:hover { + background-color: #3a3a3c; +} + +body.dark .week-nav__btn { + color: #a1a1a6; +} + +body.dark .week-nav__btn:hover { + background-color: #3a3a3c; +} + +body.dark .week-nav__day:hover { + background-color: #3a3a3c; +} + +body.dark .week-nav__day--selected { + background-color: #f5f5f7; + color: #1c1c1e; +} + +body.dark .week-nav__day--selected:hover { + background-color: #e5e5e7; +} + +body.dark .week-nav__day--selected .week-nav__day-name { + color: #86868b; +} + +body.dark .week-nav__day--selected .week-nav__day-count { + color: #86868b; +} + +body.dark .todo-section__count { + color: #f5f5f7; +} + +body.dark .todo-form__input { + background-color: #2c2c2e; + border-color: #3a3a3c; + color: #f5f5f7; +} + +body.dark .todo-form__input:focus { + border-color: #f5f5f7; +} + +body.dark .todo-form__btn { + background-color: #f5f5f7; + color: #1c1c1e; +} + +body.dark .todo-form__btn:hover { + background-color: #e5e5e7; +} + +body.dark .todo-item { + background-color: #2c2c2e; +} + +body.dark .todo-item__checkbox { + border-color: #48484a; +} + +body.dark .todo-item__checkbox:hover { + border-color: #f5f5f7; +} + +body.dark .todo-item--done .todo-item__checkbox { + background-color: #f5f5f7; + border-color: #f5f5f7; +} + +body.dark .todo-item--done .todo-item__checkbox::after { + color: #1c1c1e; +} + +body.dark .todo-item__text { + color: #f5f5f7; +} + +body.dark .todo-item__delete { + color: #48484a; +} + +/* Responsive */ +@media (max-width: 480px) { + body { + padding: 24px 12px; + } + + .header__title { + font-size: 28px; + } + + .week-nav__day { + padding: 8px 2px; + } + + .week-nav__day-number { + font-size: 14px; + } + + .week-nav__day-name { + font-size: 10px; + } +}