-
Notifications
You must be signed in to change notification settings - Fork 8
[1주차] 김홍엽 과제 제출합니다. #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e5d0985
5da754b
3dc6ed9
41bf284
c9501c4
e1100d1
78f8911
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 {}; | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 상황을 대비해 try...catch로 예외 처리를 해두신 점이 좋았습니다! |
||
|
|
||
| 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", () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 렌더링될 때마다 같은 요소에 이벤트를 새로 바인딩하면 성능 문제가 발생할 수 있습니다. weekDaysEl 등에 이벤트 위임하는 식으로 처리하면 성능 문제를 관리할 수 있을 것 같아요! |
||
| 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) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. forEach의 index를 todo 식별자로 사용하고 있는데, react는 변경된 요소만 재렌더링하기 때문에, 고유한 id를 주지 않는다면 각 todo에 고유 id(crypto.randomUUID() (꼭 uuid를 무조건 쓰라는건 아닙니다 "고유한"에 좀 더 중점을 맞추시는게 좋음)) |
||
| 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(); | ||
| } | ||
|
Comment on lines
+156
to
+161
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다음 과제가 react 마이그레이션인 만큼, |
||
|
|
||
| /* ── 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"; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다크모드 상태를 localStorage에 저장해서 새로고침 후에도 유지되도록 구현한 점이 좋은 것 같습니다! |
||
| } | ||
|
|
||
| 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(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="ko"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Todo가 미래다</title> | ||
| <!-- 웹폰트 pretendard 적용 --> | ||
| <link | ||
| rel="stylesheet" | ||
| as="style" | ||
| crossorigin | ||
| href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" | ||
| /> | ||
| <link rel="stylesheet" href="style.css" /> | ||
| </head> | ||
| <body> | ||
| <main class="app"> | ||
| <header class="header"> | ||
| <div class="header__top"> | ||
| <h1 class="header__title">ToDo</h1> | ||
| <button class="theme-toggle" id="themeToggle" aria-label="다크모드 토글"> | ||
| 🌙 | ||
| </button> | ||
| </div> | ||
| <p class="header__date" id="currentDate"></p> | ||
| </header> | ||
|
|
||
| <nav class="week-nav"> | ||
| <button class="week-nav__btn" id="prevWeek" aria-label="이전 주"> | ||
| ◀ | ||
| </button> | ||
| <ul class="week-nav__days" id="weekDays"></ul> | ||
| <button class="week-nav__btn" id="nextWeek" aria-label="다음 주"> | ||
| ▶ | ||
| </button> | ||
| </nav> | ||
|
|
||
| <section class="todo-section"> | ||
| <div class="todo-section__header"> | ||
| <h2 class="todo-section__count" id="todoCount">0개</h2> | ||
| </div> | ||
|
|
||
| <form class="todo-form" id="todoForm"> | ||
| <input | ||
| type="text" | ||
| class="todo-form__input" | ||
| id="todoInput" | ||
| placeholder="할 일을 입력하세요" | ||
| required | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. required를 넣어주셔서 빈 입력을 제출할 수 없도록 처리한 점이 좋았습니다! |
||
| /> | ||
| <button type="submit" class="todo-form__btn">등록</button> | ||
| </form> | ||
|
|
||
| <ul class="todo-list" id="todoList"></ul> | ||
| </section> | ||
| </main> | ||
|
|
||
| <script src="app.js"></script> | ||
| </body> | ||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주석으로 코드에 대한 설명을 자주 자세하게 작성해주셔서 코드를 전반적으로 이해하기 편했습니다! 특히 협업할 때 가지면 좋은 습관인 것 같습니다.