diff --git a/index.html b/index.html new file mode 100644 index 0000000..4fa389e --- /dev/null +++ b/index.html @@ -0,0 +1,41 @@ + + + + + + Vanilla Todo + + + + +
+ +

To Do

+
+ + +
+
+ +
0
+ +
+
+
+ + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..bd216fe --- /dev/null +++ b/main.js @@ -0,0 +1,277 @@ +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"); + + // Date 객체를 YYYY-MM-DD 형식의 문자열로 변환 + function formatDateToYYYYMMDD(date) { + return [ + date.getFullYear(), + String(date.getMonth() + 1).padStart(2, "0"), + String(date.getDate()).padStart(2, "0") + ].join("-"); + } + + // 현재 선택된 날짜 (YYYY-MM-DD 형식) + let currentDate = formatDateToYYYYMMDD(new Date()); + + // YYYY-MM-DD 문자열을 "YYYY년 M월 D일" 형식으로 변환 + function formatDate(dateString) { + const [y, m, d] = dateString.split("-").map(Number); + return `${y}년 ${m}월 ${d}일`; + } + + // 현재 날짜 상태를 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", () => { + // UI 클래스 토글 먼저 + if (checkbox.checked) { + todoText.classList.add("completed"); + } else { + todoText.classList.remove("completed"); + } + + // localStorage Update + toggleComplete(id, checkbox.checked); + + /*console 확인용 + console.log( + `Todo "${todoText.textContent}" completed:`, + todoText.classList.contains("completed") + );*/ + }); + + // Todo 텍스트 요소 생성 (완료 시 CSS 클래스 적용) + const todoText = document.createElement("span"); + todoText.textContent = text; + if (completed) { + todoText.classList.add("completed"); + } + + // 삭제버튼 + 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; + } + + // 현재 날짜를 기준으로 days 만큼 이동 + function changeDate(days) { + const [y, m, d] = currentDate.split("-").map(Number); + const newDate = new Date(y, m - 1, d); + + newDate.setDate(newDate.getDate() + days); + currentDate = formatDateToYYYYMMDD(newDate); + + 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"; + } + + // 사이드바 외부 클릭 처리 - Early Return 패턴 적용 + function handleOutsideClick(event) { + if (sidebar.contains(event.target) || hamburger.contains(event.target)) return; + closeSidebar(); + } + + // Enter 입력 시 Todo 추가 (한글 IME 조합 중 입력 방지 <-- React에서 수정필요!) + enterButton.addEventListener("click", addTodo); + + input.addEventListener("keydown", (event) => { + 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 = formatDateToYYYYMMDD(new Date()); + updateDateUI(); + loadTodos(); + }); + + themeBtn.addEventListener("click", toggleTheme); + hamburger.addEventListener("click", openSidebar); + closeBtn.addEventListener("click", closeSidebar); + document.addEventListener("click", handleOutsideClick); + + // 초기 실행 + updateDateUI(); + loadTodos(); + loadTheme(); +}); + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..eab7ad7 --- /dev/null +++ b/styles.css @@ -0,0 +1,427 @@ +:root { + --bg: #cbdef0; + --bg_inputContainer: #ffffff; + --bg_enter: #ffc933; + --bg_todoitem: #f9c254; + + --bg_checkbox: #f9f9f9; + --checkbox: #ff4d4d; + + --text: #000000; + --text_nav: #ffffff; + --text_todoitem: #000000; + --text_todocount: #b2b0b0; + --text_countitem: #b2b0b0; + --dateButton: #000000; + /*할 일 목록*/ + --deleteButton: #f3f4f6; + --hover_deleteButton: #e5e7eb; + --text_deleteButton: #000000; + + /*사이드 바*/ + --hamburger: #000000; + --bg_sidebar: #fbf0d6; + --text_sidebarButton: #000000; + --text_datePicker: #a5a3a3; + --border_datePicker: #ccc; + --closeButton: #a5a3a3; + + /*다크모드 설정 시*/ + --darkmode_hamburger:#ffffff; + --bg_darkmode: #3d3c3c; + --text_darkmode_header: #ffffff; + --bg_inputContainer_darkmode: #2c2c2c; + --bg_todo_darkmode: #d6d7d8; + --text_darkmode: #000000; + --text_darkmode_sidebar: #a5a3a3; + --text_darkmode_enter: #ffffff; + --bg_sidebar_darkmode: #ffffff; + --text_darkmode_todo: #000000; +} + +/*기본 스타일 설정*/ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body { + font-family: 'Pretendard', sans-serif; +} + +/* body 스타일*/ +body { + background-color: var(--bg); + color: var(--text); +} + +body.dark-mode { + background-color: var(--bg_darkmode); + color: var(--text_darkmode); +} + +/* 헤더 스타일*/ +header { + display: flex; + position:relative; + align-items: center; + justify-content: center; + color: var(--text_header); + width: 100%; + height: 4rem; + cursor: default; +} + +body.dark-mode header { + color: var(--text_darkmode_header); +} + +/*햄버거 메뉴 버튼 스타일*/ +.hamburger { + position: absolute; + left: 1rem; + padding: 0; + margin: 0; + cursor: pointer; + color: var(--hamburger); + border: none; + background-color: transparent; + font-size: 2rem; +} + +body.dark-mode .hamburger { + color: var(--darkmode_hamburger); /*피드백 수정; color값 오타*/ +} + +/*내비게이션 바 스타일*/ +nav { + display: flex; + gap: 10px; + margin-top: 1rem; + justify-content: center; + align-items: center; +} + +@media (max-width: 500px) { + .inputContainer, .todo { + width: 90%; + } + + nav { + flex-wrap: wrap; + gap: 5px; + } +} + +.calendarWrapper { + position: relative; + width: 22px; + height: 22px; +} + +.calendarWrapper:hover { + transform: scale(1.1); +} + +#datePicker { + position: absolute; + background: transparent; + inset: 0; + border: none; + opacity: 100; + cursor: pointer; +} + +#datePicker::-webkit-datetime-edit { + display: none; /*날짜 텍스트 숨기기*/ +} + +#datePicker::-webkit-calendar-picker-indicator { + cursor: pointer; + width: 18px; + height: 18px; + transform: scale(1.2); +} + +body.dark-mode nav { + color: var(--text_darkmode); +} + +/*main 스타일*/ +main { + display: flex; + align-items: center; + flex-direction: column; +} + +/*화살표 버튼 스타일*/ +.dateButton { + border: none; + cursor: pointer; + background-color: transparent; + color: var(--dateButton); +} + +body.dark-mode .dateButton { + color: var(--text_darkmode); +} + +/*카운트 숫자*/ +#todo-count { + margin-left: 10px; + font-size: 14px; + color: var(--text_todocount); +} + +/*카운트 item*/ +.count { + color: var(--text_countitem); + font-size: 14px; +} + +/*입력창 스타일*/ +.inputContainer { + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); + display: flex; + border-radius: 1rem; + width: 20rem; + padding: 10px; + margin: 2rem 0 1rem 0; + background-color: var(--bg_inputContainer); + align-items: center; + justify-content: center; + position: relative; +} + +.input { + border: none; + outline: none; + text-align: left; + flex-grow: 1; +} + +.enter { + display: flex; + cursor: pointer; + border: none; + padding: 5px 10px; + margin-left: 10px; + border-radius: 5px; + background-color: var(--bg_enter); + font-weight: 500; + font-size: 14px; +} + +.enter:active { + transform: scale(0.95); +} + +body.dark-mode .inputContainer, +body.dark-mode .enter, +body.dark-mode .input { + background: var(--bg_inputContainer_darkmode); + color: var(--text_darkmode_enter); +} + +/*할 일 리스트 스타일*/ +.container { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; + height: 100%; + max-height: 500px; + overflow-y: scroll; + margin-bottom: 100px; +} + +/*할 일 item*/ +.todo { + transition: transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 2px 6px rgba(0,0,0,0.05); + animation: fadeIn 0.2s ease; + display: flex; + align-items: center; + background-color: var(--bg_todoitem); + color: var(--text_todoitem); + font-weight: 500; + font-size: 14px; + border-radius: 1rem; + width: 20rem; + padding: 10px; + gap: 10px; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0) + } +} + +.todo:hover { + transform: translateY(-1px); +} + +.todo.dragging { + opacity: 0.3; + transform: scale(1.05); + box-shadow: 0 5px 15px rgba(0,0,0,0.2); +} + +.todo.placeholder { + height: 50px; + margin: 10px 0; + border: 2px dashed #ccc; + border-radius: 1rem; + transition: height 0.2s ease; +} + +body.dark-mode .todo { + background: var(--bg_todo_darkmode); + color: var(--text_darkmode_todo); +} + +/*체크박스 스타일*/ +.todo input[type="checkbox"] { + appearance: none; + flex-shrink: 0; /*피드백 수정; 왼쪽 체크박스 찌그러짐*/ + width: 18px; + height: 18px; + border-radius: 5px; + border: 2px solid var(--bg_checkbox); + /*display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease-in-out;*/ + cursor: pointer; + position: relative; + background-color: var(--bg_checkbox); +} + +.todo input[type="checkbox"]:checked { + background-color: var(--bg_checkbox); +} + +.todo input[type="checkbox"]:checked::after { + content: "✓"; + position: absolute; + color: var(--checkbox); + font-size: 14px; + left: 3px; + top: -2px; +} + +/*할 일 텍스트 스타일*/ +.todo span { + flex-grow: 1; + font-size: 14px; + text-align: left; + word-break: break-all; /*피드백 수정; 영문, 숫자의 줄바꿈 안됨*/ +} + +/*완료된 할 일 텍스트 스타일*/ +.completed { + text-decoration: line-through; + color: gray; +} + +/*할 일 삭제 버튼 스타일*/ +.delete { + flex-shrink: 0; + background: var(--deleteButton); + color: var(--text_deleteButton); + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; + transition: background 0.2s; +} + +.delete:hover { + background: var(--hover_deleteButton); +} + +/*사이드바 스타일*/ +.sidebar { + position: fixed; + top: 0; + left: -250px; + width: 250px; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + background: var(--bg_sidebar); + transition: left 0.3s ease-in-out; + padding: 0.2rem; +} + +body.dark-mode .sidebar { + background: var(--bg_sidebar_darkmode); + color: var(--text_darkmode_sidebar); +} + +.sidebar button:not(.close) { + display: block; + width: 100%; + margin: 10px 0; + padding: 10px; + font-size: 15px; + font-weight: 500; + background: transparent; + color: var(--text_sidebarButton); + border: none; + cursor: pointer; +} + +/*사이드바 닫기 버튼*/ +.close { + width: auto; + margin: 20px 0 0 0; + align-self: flex-start; + border:none; + font-size: 19px; + cursor: pointer; + background-color: transparent; + color: var(--closeButton); +} + +.close:hover { + transform: scale(1.1); +} + +body.dark-mode .close { + color: var(--text_darkmode); +} + +/*주간 버튼 스타일*/ +.weekBtn { + display: block; + width: 100%; + padding: 10px; + margin: 5px 0; + background: white; + color: var(--text); + border: none; + cursor: pointer; +} + +body.dark-mode .weekBtn { + color: var(--text_darkmode); +} + +/*테마 스타일*/ +.theme { + width:100%; +} + +body.dark-mode #currentDate { + color: var(--text_darkmode); +} \ No newline at end of file