-
Notifications
You must be signed in to change notification settings - Fork 8
[1주차] 이승연 과제 제출합니다. #6
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 1 commit
be73ab6
b3eff5a
fdc9954
0c30c73
8f41876
09f77ac
eaac4ff
6f3c071
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,41 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Vanilla Todo</title> | ||
| <link rel="stylesheet" href="./styles.css" /> | ||
| <link href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css" rel="stylesheet"> | ||
| </head> | ||
| <body> | ||
| <header> | ||
| <button class="hamburger">☰</button> | ||
| <h2>To Do</h2> | ||
| </header> | ||
| <aside> | ||
| <div class="sidebar"> | ||
| <button class="close">◀</button> | ||
| <button class="weekBtn" data-days="-7">이전 주</button> | ||
| <button class="weekBtn" data-days="7">다음 주</button> | ||
| <button class="theme">다크모드 설정🌙</button> | ||
| </div> | ||
| </aside> | ||
| <nav> | ||
| <button class="dateButton" id="prev">◀</button> | ||
| <span id="currentDate"></span> | ||
| <div class="calendarWrapper"> | ||
| <input type="date" id="datePicker"/> | ||
| </div> | ||
| <button class="dateButton" id="next">▶</button> | ||
| </nav> | ||
| <main> | ||
| <div class="inputContainer"> | ||
| <input placeholder="오늘의 할 일을 적어주세요!" class="input" /> | ||
| <div class="count"><span id="todo-count">0</span>개</div> | ||
| <button class="enter">등록</button> | ||
| </div> | ||
| <div class="container"></div> | ||
| </main> | ||
| <script src="./main.js"></script> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| document.addEventListener ("DOMContentLoaded", function() { | ||
| //DOM 요소 가져오기 | ||
| const input = document.querySelector(".input"); | ||
| const enterButton = document.querySelector(".enter"); | ||
| const todoContainer = document.querySelector(".container"); | ||
| const currentDateSpan = document.getElementById("currentDate"); | ||
| const prevButton = document.getElementById("prev"); | ||
| const nextButton = document.getElementById("next"); | ||
| const datePicker = document.getElementById("datePicker"); | ||
| const themeBtn = document.querySelector(".theme"); | ||
| const body = document.body; | ||
| const homeButton = document.querySelector("h2"); | ||
| const sidebar = document.querySelector(".sidebar"); | ||
| const hamburger = document.querySelector(".hamburger"); | ||
| const closeBtn = document.querySelector(".close"); | ||
| const todoCountSpan = document.getElementById("todo-count"); | ||
|
|
||
| // 현재 선택된 날짜 | ||
| let currentDate = new Date().toISOString().split("T")[0]; | ||
|
|
||
| // 날짜 포맷 변환 함수 | ||
| function formatDate(dateString) { | ||
| const date = new Date(dateString); | ||
| return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`; | ||
| } | ||
|
|
||
| // UI 날짜 업데이트 | ||
| function updateDateUI() { | ||
| currentDateSpan.textContent = formatDate(currentDate); | ||
| datePicker.value = currentDate; | ||
| updateTodoCount(); | ||
| } | ||
|
|
||
| // 할 일 리스트 불러오기 (로컬 스토리지) | ||
| function loadTodos() { | ||
| todoContainer.innerHTML = ""; | ||
| const todos = JSON.parse(localStorage.getItem(currentDate)) || []; | ||
| todos.forEach(({ id, text, completed }) => | ||
| addTodoElement(id, text, completed) | ||
| ); | ||
| updateTodoCount(); | ||
| } | ||
|
|
||
| // 새로운 할 일 요소 추가 | ||
| function addTodoElement(id, text, completed = false) { | ||
| const todoDiv = document.createElement("div"); | ||
| todoDiv.classList.add("todo"); | ||
| todoDiv.setAttribute("draggable", true); /*드래그 가능 설정*/ | ||
| todoDiv.dataset.id = id; /*순서 저장용*/ | ||
|
|
||
| // 체크박스 | ||
| const checkbox = document.createElement("input"); | ||
| checkbox.type = "checkbox"; | ||
| checkbox.checked = completed; | ||
| checkbox.classList.add("checkbox"); | ||
| checkbox.addEventListener("change", () => | ||
| toggleComplete(id, checkbox.checked) | ||
| ); | ||
|
|
||
| // 텍스트 | ||
| const todoText = document.createElement("span"); | ||
| todoText.textContent = text; | ||
| if (completed) { | ||
| todoText.style.textDecoration = "line-through"; | ||
|
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. 스타일 변경은 JS 인라인보다 CSS 클래스로 분리하는 것이 이후의 유지 보수에도 용이하고 더 간결하고 보기 좋은 코드가 됩니다~ |
||
| todoText.style.color = "gray"; | ||
| } | ||
|
|
||
| // 삭제버튼 | ||
| const deleteButton = document.createElement("button"); | ||
| deleteButton.textContent = "삭제"; | ||
| deleteButton.classList.add("delete"); | ||
| deleteButton.addEventListener("click", () => removeTodo(id)); | ||
|
|
||
| todoDiv.append(checkbox, todoText, deleteButton); | ||
| todoContainer.appendChild(todoDiv); | ||
|
|
||
| // 드래그 이벤트 추가 | ||
| addDragAndDropListeners(todoDiv); | ||
| } | ||
|
|
||
| // 드래그 이벤트 함수 | ||
| let placeholder = null; | ||
|
|
||
| function addDragAndDropListeners(item) { | ||
| item.draggable = true; | ||
|
|
||
| item.addEventListener("dragstart", () => { | ||
| item.classList.add("dragging"); | ||
| placeholder = document.createElement("div"); | ||
| placeholder.classList.add("todo", "placeholder"); | ||
| item.parentNode.insertBefore(placeholder, item.nextSibling); | ||
| }); | ||
|
|
||
| item.addEventListener("dragend", () => { | ||
| item.classList.remove("dragging"); | ||
| if (placeholder) { | ||
| placeholder.parentNode.replaceChild(item, placeholder); | ||
| placeholder = null; | ||
| saveNewOrder(); // localStorage에 순서 저장 | ||
| } | ||
| }); | ||
|
|
||
| item.addEventListener("dragover", (e) => { | ||
| e.preventDefault(); | ||
| const container = item.parentNode; | ||
| const draggingItem = document.querySelector(".dragging"); | ||
| if (draggingItem === item) return; | ||
|
|
||
| const rect = item.getBoundingClientRect(); | ||
| const offset = e.clientY - rect.top; | ||
| const middle = rect.height / 2; | ||
|
|
||
| if (offset > middle) { | ||
| container.insertBefore(placeholder, item.nextSibling); | ||
| } else { | ||
| container.insertBefore(placeholder, item); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // 순서 변경 후 LocalStorage에 저장 | ||
| function saveNewOrder() { | ||
| const todos = []; | ||
| todoContainer.querySelectorAll(".todo").forEach((todoDiv) => { | ||
| const id = todoDiv.dataset.id; | ||
| const text = todoDiv.querySelector("span").textContent; | ||
| const completed = todoDiv.querySelector("input").checked; | ||
| todos.push({id, text, completed}); | ||
| }); | ||
| localStorage.setItem(currentDate, JSON.stringify(todos)); | ||
| } | ||
|
|
||
|
|
||
|
|
||
| // 할 일 추가 | ||
| function addTodo() { | ||
| const text = input.value.trim(); | ||
| if (!text) return; | ||
|
|
||
| const todos = JSON.parse(localStorage.getItem(currentDate)) || []; | ||
| const newTodo = { id: crypto.randomUUID(), text, completed: false }; | ||
|
|
||
| todos.push(newTodo); | ||
| localStorage.setItem(currentDate, JSON.stringify(todos)); | ||
| addTodoElement(newTodo.id, text); | ||
| input.value = ""; | ||
| updateTodoCount(); | ||
| } | ||
|
|
||
| // 할 일 삭제 | ||
| function removeTodo(id) { | ||
| let todos = JSON.parse(localStorage.getItem(currentDate)) || []; | ||
| todos = todos.filter((todo) => todo.id !== id); | ||
| localStorage.setItem(currentDate, JSON.stringify(todos)); | ||
| loadTodos(); | ||
| } | ||
|
|
||
| // 완료 상태 체크박스 | ||
| function toggleComplete(id, isCompleted) { | ||
| let todos = JSON.parse(localStorage.getItem(currentDate)) || []; | ||
| todos = todos.map((todo) => | ||
| todo.id === id ? { ...todo, completed: isCompleted } : todo | ||
| ); | ||
| localStorage.setItem(currentDate, JSON.stringify(todos)); | ||
| loadTodos(); | ||
| } | ||
|
|
||
| // Todo 개수 업데이트 함수 (체크된 항목 제외) | ||
| function updateTodoCount() { | ||
| const todos = JSON.parse(localStorage.getItem(currentDate)) || []; | ||
| const activeTodos = todos.filter((todo) => !todo.completed).length; | ||
| todoCountSpan.textContent = activeTodos; | ||
| } | ||
|
|
||
| // 날짜 변경 (이전/다음) | ||
| function changeDate(days) { | ||
| const newDate = new Date(currentDate); | ||
|
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.
|
||
| newDate.setDate(newDate.getDate() + days); | ||
| currentDate = newDate.toISOString().split("T")[0]; | ||
| updateDateUI(); | ||
| loadTodos(); | ||
| } | ||
|
|
||
| // 다크모드 설정 | ||
| function loadTheme() { | ||
| const isDarkMode = JSON.parse(localStorage.getItem("darkMode")); | ||
| body.classList.toggle("dark-mode", isDarkMode); | ||
| themeBtn.textContent = isDarkMode ? "☀️다크모드 해제" : "다크모드 설정🌙"; | ||
| } | ||
|
|
||
| function toggleTheme() { | ||
| const isDarkMode = body.classList.toggle("dark-mode"); | ||
| themeBtn.textContent = isDarkMode ? "☀️다크모드 해제" : "다크모드 설정🌙"; | ||
| localStorage.setItem("darkMode", isDarkMode); | ||
| } | ||
|
|
||
| // 사이드바 열기/닫기 기능 | ||
| function openSidebar() { | ||
| sidebar.style.left = "0"; | ||
| } | ||
|
|
||
| function closeSidebar() { | ||
| sidebar.style.left = "-250px"; | ||
| } | ||
|
|
||
| function handleOutsideClick(event) { | ||
| if (!sidebar.contains(event.target) && !hamburger.contains(event.target)) { | ||
| closeSidebar(); | ||
| } | ||
|
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. 사이드바 외부 클릭 처리 로직에서 Early Return 패턴을 적용해 보셔도 좋을 것 같습니다! function handleOutsideClick(event) {
if (sidebar.contains(event.target) || hamburger.contains(event.target)) return;
closeSidebar();
} |
||
| } | ||
|
|
||
| // 이벤트 리스너 등록 | ||
| enterButton.addEventListener("click", addTodo); | ||
| input.addEventListener("keypress", (event) => { | ||
|
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. 웹 표준에서는 KeyboardEvent.key |
||
| if (event.key === "Enter") addTodo(); | ||
| }); | ||
|
|
||
| prevButton.addEventListener("click", () => changeDate(-1)); | ||
| nextButton.addEventListener("click", () => changeDate(1)); | ||
|
|
||
| datePicker.addEventListener("change", function () { | ||
| currentDate = this.value; | ||
| updateDateUI(); | ||
| loadTodos(); | ||
| }); | ||
|
|
||
| document.querySelectorAll(".weekBtn").forEach((button) => { | ||
| button.addEventListener("click", () => | ||
| changeDate(parseInt(button.dataset.days)) | ||
| ); | ||
| }); | ||
|
|
||
| homeButton.addEventListener("click", () => { | ||
| currentDate = new Date().toISOString().split("T")[0]; | ||
| updateDateUI(); | ||
| loadTodos(); | ||
| }); | ||
|
|
||
| themeBtn.addEventListener("click", toggleTheme); | ||
| hamburger.addEventListener("click", openSidebar); | ||
| closeBtn.addEventListener("click", closeSidebar); | ||
| document.addEventListener("click", handleOutsideClick); | ||
|
|
||
| // 초기 실행 | ||
| updateDateUI(); | ||
| loadTodos(); | ||
| loadTheme(); | ||
| }); | ||
|
|
||
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.
aside, nav 시맨틱 태그로 분리해서 코드 작성해주셔서 영역 구분과 가독성이 더욱 좋은 것 같습니다!