Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions app.js
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석으로 코드에 대한 설명을 자주 자세하게 작성해주셔서 코드를 전반적으로 이해하기 편했습니다! 특히 협업할 때 가지면 좋은 습관인 것 같습니다.

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 {};
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 상황을 대비해 try...catch로 예외 처리를 해두신 점이 좋았습니다!
localStorage 데이터가 깨져 있거나 예상과 다른 값일 때 앱이 바로 죽지 않도록 방어해둔 점이 안정적이라고 생각했어요. 덕분에 localStorage를 다룰 때 이런 식으로 방어적으로 작성할 수 있다는 걸 배울 수 있었습니다!


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", () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

렌더링될 때마다 같은 요소에 이벤트를 새로 바인딩하면 성능 문제가 발생할 수 있습니다. weekDaysEl 등에 이벤트 위임하는 식으로 처리하면 성능 문제를 관리할 수 있을 것 같아요!
하단의 checkbox, deletebtn 등에 대해서도 todoList에 이벤트 위임을 해서 처리하면 더 좋을 것 같습니다.

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) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forEach의 index를 todo 식별자로 사용하고 있는데,
지금은 render()에서 매번 리스트를 다시 그려서 괜찮지만,
다음 과제에서 react로 구현시 문제가 될 수 있습니다!


react는 변경된 요소만 재렌더링하기 때문에, 고유한 id를 주지 않는다면
중간 항목을 삭제하면 index가 밀리면서 어떤 항목이 삭제된 건지 올바르게 판단하지 못할 수도 있습니당


각 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다음 과제가 react 마이그레이션인 만큼,
render All 하는 부분에 대해서 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";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다크모드 상태를 localStorage에 저장해서 새로고침 후에도 유지되도록 구현한 점이 좋은 것 같습니다!
추가로 html 파일에서 버튼의 aria-label은 고정값인데, 다크/라이트 상태에 따라 "다크모드 켜기" / "라이트모드 켜기" 처럼 함께 바뀌면 접근성 면에서 더 좋을 것 같아요!

}

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();
60 changes: 60 additions & 0 deletions index.html
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="다크모드 토글">
&#127769;
</button>
</div>
<p class="header__date" id="currentDate"></p>
</header>

<nav class="week-nav">
<button class="week-nav__btn" id="prevWeek" aria-label="이전 주">
&#9664;
</button>
<ul class="week-nav__days" id="weekDays"></ul>
<button class="week-nav__btn" id="nextWeek" aria-label="다음 주">
&#9654;
</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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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>
Loading