diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..1258a3b --- /dev/null +++ b/css/style.css @@ -0,0 +1,368 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: #ffd6e7; + color: #2e2a26; +} + +button, +input { + font: inherit; +} + +button { + border: none; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.15s ease, + opacity 0.2s ease; +} + +.app { + min-height: 100vh; + display: flex; + justify-content: center; + padding: 24px; +} + +.todoCard { + width: 100%; + max-width: 520px; + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; +} + +.todoHeader { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + position: relative; + padding-top: 8px; +} + +.menuButton { + position: absolute; + left: 0; + top: 0; + background: transparent; + font-size: 28px; + color: #2e2a26; + padding: 4px 8px; + z-index: 20; +} + +.menuButton:hover { + opacity: 0.7; +} + +.viewMenu { + position: absolute; + left: 0; + top: 42px; + display: flex; + flex-direction: column; + gap: 8px; + background-color: #ffe6f0; + padding: 10px; + border-radius: 14px; + box-shadow: 0 8px 18px rgba(90, 60, 70, 0.14); + z-index: 15; +} + +.viewMenuButton { + min-width: 110px; + background-color: white; + color: #2e2a26; + border-radius: 10px; + padding: 8px 12px; + font-weight: 600; + text-align: left; +} + +.viewMenuButton:hover { + background-color: #ffcade; + transform: translateY(-1px); +} + +.hidden { + display: none; +} + +.appTitle { + margin: 0; + font-size: 28px; + font-weight: 700; +} + +.dateSection { + display: flex; + align-items: center; + gap: 24px; +} + +.dateButton { + background: transparent; + color: #2e2a26; + font-size: 18px; + padding: 4px 8px; +} + +.dateButton:hover { + opacity: 0.7; + transform: scale(1.05); +} + +.selectedDate { + margin: 0; + font-size: 16px; + font-weight: 600; +} + +.todoInputSection, +.todoListSection, +.todoSummarySection { + width: 100%; +} + +.todoForm { + display: flex; + align-items: center; + gap: 8px; + background-color: #ffe6f0; + border-radius: 20px; + padding: 10px 12px; +} + +.todoInput { + flex: 1; + border: none; + outline: none; + background: transparent; + font-size: 15px; + color: #2e2a26; +} + +.todoInput::placeholder { + color: #8c7a73; +} + +.todoInput:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.addButton { + background-color: #ff7aa2; + color: white; + font-weight: 700; + border-radius: 12px; + padding: 10px 16px; +} + +.addButton:hover { + background-color: #f0628f; + transform: translateY(-1px); +} + +.addButton:disabled { + background-color: #d9a7b9; + cursor: not-allowed; + transform: none; +} + +.todoList { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +.todoItem { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + background-color: #ffb3cc; + border-radius: 20px; + padding: 12px 14px; + transition: transform 0.15s ease, box-shadow 0.2s ease; +} + +.todoItem:hover { + transform: translateY(-2px); + box-shadow: 0 8px 18px rgba(120, 72, 88, 0.14); +} + +.todoLeft { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.todoCheckbox { + width: 22px; + height: 22px; + appearance: none; + -webkit-appearance: none; + background-color: white; + border: 2px solid #efb2c8; + border-radius: 6px; + position: relative; + cursor: pointer; + flex-shrink: 0; +} + +.todoCheckbox:hover { + border-color: #ff8fb2; +} + +.todoCheckbox:checked { + background-color: white; + border-color: #efb2c8; +} + +.todoCheckbox:checked::after { + content: "✔"; + position: absolute; + color: #ff5c8a; + font-size: 16px; + font-weight: 900; + left: 3px; + top: -1px; +} + +.todoText { + font-size: 15px; + font-weight: 400; + word-break: break-word; + color: #2e2a26; +} + +.completedTodo { + text-decoration: line-through; + opacity: 0.7; +} + +.deleteButton { + background-color: #4a403b; + color: white; + border-radius: 12px; + padding: 8px 14px; + font-weight: 700; + flex-shrink: 0; +} + +.deleteButton:hover { + background-color: #6f645e; + transform: translateY(-1px); +} + +.summaryCardList { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.summaryCard { + flex: 1 1 150px; + background-color: #ffe6f0; + border-radius: 18px; + padding: 16px; + text-align: center; + transition: transform 0.15s ease, box-shadow 0.2s ease; +} + +.summaryCard:hover { + transform: translateY(-2px); + box-shadow: 0 8px 18px rgba(120, 72, 88, 0.1); +} + +.summaryTitle { + margin: 0 0 10px; + font-size: 15px; + color: #5c514b; +} + +.summaryValue { + margin: 0; + font-size: 22px; + font-weight: 700; + color: #d94b77; +} + +/* Weekly view */ +.weeklyTodoGroup { + display: flex; + flex-direction: column; + gap: 10px; + background-color: #ffdbe8; + border-radius: 18px; + padding: 14px; +} + +.weeklyTodoTitle { + margin: 0; + font-size: 16px; + font-weight: 700; + color: #4a403b; +} + +.weeklyEmptyText { + margin: 0; + font-size: 14px; + color: #6f645e; + padding-left: 4px; +} + +.emptyTodoText { + margin: 0; + font-size: 15px; + font-weight: 500; + color: #5c514b; + text-align: center; +} + +@media (max-width: 600px) { + .app { + padding: 16px; + } + + .todoCard { + gap: 20px; + } + + .dateSection { + gap: 14px; + } + + .todoForm { + padding: 10px; + } + + .addButton { + padding: 10px 14px; + } + + .todoItem { + padding: 12px; + } + + .summaryCardList { + flex-direction: column; + } + + .viewMenu { + top: 40px; + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3bd6f8a --- /dev/null +++ b/index.html @@ -0,0 +1,104 @@ + + + + + + vanilla-todo + + + + +
+
+
+ + + + +

To-Do

+
+ +
+ + +

2026년 3월 14일

+ + +
+ +
+
+ + +
+
+ +
+
    +
    + +
    +
    +
    +

    전체 투두

    +

    0개

    +
    + +
    +

    완료한 투두

    +

    0개

    +
    + +
    +

    달성률

    +

    0%

    +
    +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/js/script.js b/js/script.js new file mode 100644 index 0000000..b521ddc --- /dev/null +++ b/js/script.js @@ -0,0 +1,311 @@ +const selectedDate = document.querySelector("#selectedDate"); +const prevDateButton = document.querySelector(".prevDateButton"); +const nextDateButton = document.querySelector(".nextDateButton"); +const todoForm = document.querySelector("#todoForm"); +const todoInput = document.querySelector("#todoInput"); +const todoList = document.querySelector("#todoList"); + +const totalTodoCount = document.querySelector("#totalTodoCount"); +const completedTodoCount = document.querySelector("#completedTodoCount"); +const achievementRate = document.querySelector("#achievementRate"); + +const menuButton = document.querySelector("#menuButton"); +const viewMenu = document.querySelector("#viewMenu"); +const viewMenuButtons = document.querySelectorAll(".viewMenuButton"); + +let currentDate = new Date(); +let viewMode = "daily"; + +const todoData = {}; + +function formatDateKey(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function formatDisplayDate(date) { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + return `${year}년 ${month}월 ${day}일`; +} + +function getWeekOfMonth(date) { + const firstDay = new Date(date.getFullYear(), date.getMonth(), 1); + const day = date.getDate(); + const offset = firstDay.getDay(); + return Math.ceil((day + offset) / 7); +} + +function formatWeeklyDisplay(date) { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const week = getWeekOfMonth(date); + return `${year}년 ${month}월 ${week}째주`; +} + +function renderSelectedDate() { + if (viewMode === "daily") { + selectedDate.textContent = formatDisplayDate(currentDate); + } else { + selectedDate.textContent = formatWeeklyDisplay(currentDate); + } +} + +function getCurrentTodos() { + const dateKey = formatDateKey(currentDate); + if (!todoData[dateKey]) { + todoData[dateKey] = []; + } + return todoData[dateKey]; +} + +function getStartOfWeek(date) { + const start = new Date(date); + const day = start.getDay(); + start.setDate(start.getDate() - day); + start.setHours(0, 0, 0, 0); + return start; +} + +function getWeeklyTodos() { + const start = getStartOfWeek(currentDate); + const result = []; + + for (let i = 0; i < 7; i++) { + const date = new Date(start); + date.setDate(start.getDate() + i); + + const key = formatDateKey(date); + const todos = todoData[key] || []; + + result.push({ + date, + todos + }); + } + + return result; +} + +function createTodoItemElement(todo) { + const todoItem = document.createElement("li"); + todoItem.className = "todoItem"; + + const todoLeft = document.createElement("div"); + todoLeft.className = "todoLeft"; + + const todoCheckbox = document.createElement("input"); + todoCheckbox.type = "checkbox"; + todoCheckbox.className = "todoCheckbox"; + todoCheckbox.id = `todo-${todo.id}`; + todoCheckbox.checked = todo.isCompleted; + + const todoLabel = document.createElement("label"); + todoLabel.className = "todoText"; + todoLabel.setAttribute("for", `todo-${todo.id}`); + todoLabel.textContent = todo.text; + + if (todo.isCompleted) { + todoLabel.classList.add("completedTodo"); + } + + const deleteButton = document.createElement("button"); + deleteButton.type = "button"; + deleteButton.className = "deleteButton"; + deleteButton.textContent = "삭제"; + + todoCheckbox.addEventListener("change", function () { + todo.isCompleted = todoCheckbox.checked; + + if (todo.isCompleted) { + todoLabel.classList.add("completedTodo"); + } else { + todoLabel.classList.remove("completedTodo"); + } + + renderSummary(); + }); + + deleteButton.addEventListener("click", function () { + deleteTodo(todo.id); + }); + + todoLeft.append(todoCheckbox, todoLabel); + todoItem.append(todoLeft, deleteButton); + + return todoItem; +} + +function renderDailyTodoList() { + const todos = getCurrentTodos(); + + if (todos.length === 0) { + const emptyItem = document.createElement("li"); + emptyItem.className = "todoItem"; + + const emptyText = document.createElement("p"); + emptyText.className = "emptyTodoText"; + emptyText.textContent = "아직 등록된 할 일이 없어요."; + + emptyItem.append(emptyText); + todoList.append(emptyItem); + return; + } + + todos.forEach(todo => { + const element = createTodoItemElement(todo); + todoList.append(element); + }); +} + +function renderWeeklyTodoList() { + const weekly = getWeeklyTodos(); + + weekly.forEach(day => { + const group = document.createElement("li"); + group.className = "weeklyTodoGroup"; + + const title = document.createElement("h3"); + title.className = "weeklyTodoTitle"; + title.textContent = formatDisplayDate(day.date); + + group.append(title); + + if (day.todos.length === 0) { + const empty = document.createElement("p"); + empty.className = "weeklyEmptyText"; + empty.textContent = "등록된 할 일이 없어요."; + group.append(empty); + } else { + day.todos.forEach(todo => { + const element = createTodoItemElement(todo); + group.append(element); + }); + } + + todoList.append(group); + }); +} + +function renderTodoList() { + todoList.innerHTML = ""; + + if (viewMode === "daily") { + renderDailyTodoList(); + } else { + renderWeeklyTodoList(); + } +} + +function renderSummary() { + let todos = []; + + if (viewMode === "daily") { + todos = getCurrentTodos(); + } else { + const weekly = getWeeklyTodos(); + todos = weekly.flatMap(d => d.todos); + } + + const totalCount = todos.length; + const completedCount = todos.filter(t => t.isCompleted).length; + + let rate = 0; + if (totalCount > 0) { + rate = Math.round((completedCount / totalCount) * 100); + } + + totalTodoCount.textContent = `${totalCount}개`; + completedTodoCount.textContent = `${completedCount}개`; + achievementRate.textContent = `${rate}%`; +} + +function renderInputState() { + if (viewMode === "daily") { + todoInput.disabled = false; + todoInput.placeholder = "오늘의 할 일을 적어주세요!"; + } else { + todoInput.disabled = true; + todoInput.placeholder = "주간 보기에서는 추가할 수 없어요."; + } +} + +function renderApp() { + renderSelectedDate(); + renderInputState(); + renderTodoList(); + renderSummary(); +} + +function addTodo(text) { + const todos = getCurrentTodos(); + + const newTodo = { + id: Date.now(), + text, + isCompleted: false + }; + + todos.push(newTodo); + renderApp(); +} + +function deleteTodo(id) { + const todos = getCurrentTodos(); + const updated = todos.filter(todo => todo.id !== id); + + const key = formatDateKey(currentDate); + todoData[key] = updated; + + renderApp(); +} + +function changePeriod(offset) { + const newDate = new Date(currentDate); + + if (viewMode === "daily") { + newDate.setDate(newDate.getDate() + offset); + } else { + newDate.setDate(newDate.getDate() + offset * 7); + } + + currentDate = newDate; + renderApp(); +} + +menuButton.addEventListener("click", () => { + viewMenu.classList.toggle("hidden"); +}); + +viewMenuButtons.forEach(button => { + button.addEventListener("click", () => { + viewMode = button.dataset.view; + viewMenu.classList.add("hidden"); + renderApp(); + }); +}); + +todoForm.addEventListener("submit", event => { + event.preventDefault(); + + if (viewMode !== "daily") return; + + const text = todoInput.value.trim(); + if (text === "") return; + + addTodo(text); + todoInput.value = ""; +}); + +prevDateButton.addEventListener("click", () => { + changePeriod(-1); +}); + +nextDateButton.addEventListener("click", () => { + changePeriod(1); +}); + +renderApp(); \ No newline at end of file