Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
41 changes: 41 additions & 0 deletions index.html
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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

aside, nav 시맨틱 태그로 분리해서 코드 작성해주셔서 영역 구분과 가독성이 더욱 좋은 것 같습니다!

249 changes: 249 additions & 0 deletions main.js
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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

new Date(YYY-MM-DD) 형식의 문자열을 넘기면 UTC 기준으로 파싱되어 한국(UTC+9)에서 실행하면 실제로는 날짜가 하루 밀리는 버그가 생길 수 있습니다. const [y, m, d] = dateString.split("-")와 같이 스플릿 문자열을 사용하는 등의 방식을 고려해주셔도 좋을 것 같습니다!

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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

사이드바 외부 클릭 처리 로직에서 Early Return 패턴을 적용해 보셔도 좋을 것 같습니다!
사이드바나 햄버거 버튼 클릭처럼 '동작을 방해하지 않아야 할 상황'을 먼저 return으로 걸러내면, 본 로직을 중괄호 안에 가둘 필요가 없어 들여쓰기도 줄어들고 코드도 훨씬 직관적으로 변할 것 같아요~!

function handleOutsideClick(event) {
    if (sidebar.contains(event.target) || hamburger.contains(event.target)) return;
    closeSidebar();
}

}

// 이벤트 리스너 등록
enterButton.addEventListener("click", addTodo);
input.addEventListener("keypress", (event) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

웹 표준에서는 keypress보다는 keydown 혹은 keyup을 사용하는 것이 권장됩니다! 또한 한글 IME 이슈도 한번 보시면 좋을 것 같아요~

KeyboardEvent.key
input에 한글 입력 시 엔터 키 중복 실행 막기 (feat. isComposing)

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();
});

Loading