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
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": true,
"signleQuote": true,
"trailingComma": "es5",
"tabWidth": 2
}
27 changes: 27 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header><h1 id="goToday">Todo-List</h1></header>
<nav>
<button class="dateButton" id="prevButton"><</button>
<input type="date" id="currentDate" />
<button class="dateButton" id="nextButton">></button>
Comment on lines +12 to +14
Copy link
Member

Choose a reason for hiding this comment

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

현재 렌더링에는 문제가 없지만, 부등호는 잠재적으로 태그 시작/종료 기호이므로, 상황에 따라 오동작할 수 있습니다!
따라서 안전하게 표시하려면 < → &lt;, > → &gt;와 같이 HTML 엔티티로 대체해줘도 좋을 거 같아용

<span class="todoCount">0 개</span>
</nav>
<main>
<!-- 입력 목록 -->
<div class="inputContainer">
<input placeholder="할 일을 입력해주세요" id="todoInput" required />
Copy link
Member

Choose a reason for hiding this comment

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

required 속성을 넣어주신 점이 인상깊네용 😲

<span class="appendTodo">+</span>
Copy link
Member

Choose a reason for hiding this comment

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

button 태그를 사용해도 좋을 거 같아용

</div>
<!-- 투두리스트 목록 -->
</main>
<script src="script.js"></script>
</body>
</html>
162 changes: 162 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
document.addEventListener("DOMContentLoaded", () => {
const goToday = document.getElementById("goToday");
const prevButton = document.getElementById("prevButton");
const currentDate = document.getElementById("currentDate");
const nextButton = document.getElementById("nextButton");
const todoCount = document.querySelector(".todoCount");
const todoInput = document.getElementById("todoInput");
const appendTodo = document.querySelector(".appendTodo");
const mainContainer = document.querySelector("main");

// todo 리스트 컨테이너 div 태그 추가
const todoListContainer = document.createElement("div");
mainContainer.appendChild(todoListContainer);

// 날짜별 투두리스트 저장
let todos = {};

// 마지막으로 선택된 날짜
const savedDate = localStorage.getItem("lastSelectedDate");

// 날짜 디폴트값 : 당일
function setToday() {
const today = new Date();
const yyyy = today.getFullYear();
const mm = String(today.getMonth() + 1).padStart(2, "0"); // 1~9월 앞에 0 붙임
const dd = String(today.getDate()).padStart(2, "0"); // 1~9일 앞에 0 붙임

const defaultDate = `${yyyy}-${mm}-${dd}`;
currentDate.value = defaultDate;
localStorage.setItem("lastSelectedDate", defaultDate);
Copy link
Member

Choose a reason for hiding this comment

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

페이지 새로고침 시 마지막으로 선택했던 날짜가 유지되도록 구현해 주신 점이 인상 깊었습니다!
다만, 페이지를 닫고 다시 열었을 때는 오늘 날짜가 기본값으로 보이는 편이 더 자연스러울 수도 있을 것 같습니다. 이 경우 localStorage 대신 sessionStorage를 활용해 보시는 것도 좋겠습니다!!

loadTodos();
renderTodos(defaultDate);
}

// 페이지 새로고침 : 마지막에 본 날짜 유지
function setInitialDate() {
loadTodos();

if (savedDate) {
currentDate.value = savedDate;
} else {
setToday();
return;
}
renderTodos(currentDate.value);
}

setInitialDate(); // 페이지 로딩 시 날짜 설정

// 날짜 변경
function changeDate(days) {
// value를 연도, 월, 일로 분리
const [year, month, day] = currentDate.value.split("-").map(Number);
const selectedDate = new Date(year, month - 1, day); // JS에서 month는 0~11
selectedDate.setDate(selectedDate.getDate() + days);

const yyyy = selectedDate.getFullYear();
const mm = String(selectedDate.getMonth() + 1).padStart(2, "0");
const dd = String(selectedDate.getDate()).padStart(2, "0");
currentDate.value = `${yyyy}-${mm}-${dd}`;

localStorage.setItem("lastSelectedDate", currentDate.value);
renderTodos(currentDate.value);
}

// 버튼 클릭 시 날짜 변경
prevButton.addEventListener("click", () => {
changeDate(-1);
console.log("날짜:", currentDate.value);
Copy link
Member

Choose a reason for hiding this comment

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

console.log는 개발 과정에서 디버깅 용도로 사용되지만, 실제 배포 시에는 불필요하므로 제거해 주시는 것이 좋습니다!
앞으로 프로젝트를 진행하실 때도 참고해 주시면 좋겠습니다 :)

});
nextButton.addEventListener("click", () => {
changeDate(1);
console.log("날짜:", currentDate.value);
});

// 달력에서 날짜 변경
currentDate.addEventListener("change", () => {
localStorage.setItem("lastSelectedDate", currentDate.value); // 선택한 날짜 저장
renderTodos(currentDate.value);
console.log("날짜:", currentDate.value);
});

// Todo 추가
appendTodo.addEventListener("click", () => {
const text = todoInput.value.trim();
if (!text) return;

if (!todos[currentDate.value]) todos[currentDate.value] = [];
todos[currentDate.value].push({ text, checked: false }); // todo 객체 형태로 저장

todoInput.value = ""; // 입력값 초기화
saveTodos();
renderTodos(currentDate.value);
});
todoInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
appendTodo.click();
}
});
Comment on lines +95 to +99
Copy link
Member

Choose a reason for hiding this comment

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

스크린샷 2025-09-14 오후 2 20 06

승선님 리뷰에도 남겨놨었는데, 한글 입력 후 엔터로 등록을 할 경우 마지막 글자가 두 번 입력되는 현상이 있습니다!

이 문제는 IME 조합형 입력 중 keydown 이벤트가 조합이 완료되기 전에 먼저 실행되면서 발생하는 이슈인데요.

아래 참고 자료들에서 안내하는 것처럼 KeyboardEvent의 isComposing 속성을 활용하면
이런 상황을 방지할 수 있으니 참고해보시면 좋을 것 같아요 👍

https://developer.mozilla.org/ko/docs/Web/API/KeyboardEvent/isComposing
https://velog.io/@goldfrosch/is-composing


// Todo 렌더링
function renderTodos(date) {
todoListContainer.innerHTML = ""; // 기존 목록 초기화
const list = todos[date] || [];

list.forEach((todo, index) => {
const div = document.createElement("div");
div.className = "listContainer";

div.innerHTML = `
<input type="checkbox" class="checkboxButton" id="checkbox-${index}" ${
todo.checked ? "checked" : ""
} />
<span class="todoContent">${todo.text}</span>
<button class="deleteButton">x</button>
`;
Comment on lines +110 to +116
Copy link
Member

Choose a reason for hiding this comment

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

현재 renderTodos 함수에서 innerHTML을 통해 사용자 입력값(todo.text)을 그대로 삽입하고 있는데요,
innerHTML은 입력값을 HTML로 파싱하기 때문에 악성 스크립트가 실행될 수 있는 XSS 보안 취약점을 초래할 수 있습니다!
따라서 createElement, appendChild, textContent를 활용해 해당 코드를 재구현하는 방법도 고려해 보시면 좋겠습니다 🙂

참고자료: https://greathyeon.tistory.com/49


const checkbox = div.querySelector(`#checkbox-${index}`);
const todoContent = div.querySelector(".todoContent");
Copy link
Member

Choose a reason for hiding this comment

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

const todoContent = div.querySelector(".todoContent");

현재 코드에서 선언만 되고 실제로는 사용되지 않고 있는데요, 불필요한 코드는 제거해 주시면 더 좋을 거 같습니다!

const deleteButton = div.querySelector(".deleteButton");

// 체크박스
checkbox.addEventListener("change", () => {
todo.checked = checkbox.checked;
saveTodos();
renderTodos(date); // 상태 반영 위해 다시 렌더링
});

// 투두 삭제
deleteButton.addEventListener("click", () => {
todos[date].splice(index, 1);
saveTodos();
renderTodos(date);
});

todoListContainer.appendChild(div);
});

// 투두 개수
todoCount.textContent = `${list.filter((todo) => !todo.checked).length} 개`;
}

// 투두 리스트 로컬 스토리지에 저장
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
}
// 투두 리스트 로드
function loadTodos() {
const data = localStorage.getItem("todos");
if (data) {
todos = JSON.parse(data);
}
}

// 페이지 로드 시 투두리스트 로드
loadTodos();

// Todo-List 클릭 -> 오늘 날짜로
goToday.addEventListener("click", () => {
setToday();
});
});
161 changes: 161 additions & 0 deletions style.css
Copy link
Member

Choose a reason for hiding this comment

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

전반적으로 rem단위를 사용하신 것이 인상깊네요!

Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
* {
text-align: center;
font-family: Pretendard;
Copy link
Member

Choose a reason for hiding this comment

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

Pretendard는 기본 제공 폰트가 아니기 때문에 CDN, @import, @font-face 등의 방식으로 폰트를 불러와야 실제로 적용됩니다!
다음 과제에서 적용해보시면 좋겠네요 :)

margin: 0;
padding: 0;
box-sizing: border-box;
}

header {
display: flex;
justify-content: center;
padding: 2rem;
}

h1 {
cursor: pointer;
}

/* ------------- */

nav {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 0.5rem;
gap: 0.5rem;
}

.dateButton {
border: none;
background: none;

font-weight: 900;
cursor: pointer;
}

#currentDate {
border: none;
font-size: 1rem;
}
/* 달력 아이콘 스타일 */
#currentDate::-webkit-calendar-picker-indicator {
padding: 0;
margin: 0;
cursor: pointer;

margin-left: -0.2rem;
margin-right: 0.6rem;
}

.todoCount {
margin-left: 1rem;
}

/* ------------- */

main {
display: flex;
align-items: center;
justify-content: space-between;

flex-direction: column;
}

.inputContainer {
display: flex;
flex-direction: row;

padding: 0.5rem;
margin-bottom: 0.8rem;
width: 100%;
max-width: 20.8rem;
}

#todoInput {
text-align: start;
padding: 0.6rem;
padding-left: 0.7rem;
flex: 1;

border: 1px solid grey;
border-radius: 5px;
}
#todoInput::placeholder {
color: #b8b8b8;
}

.appendTodo {
position: relative;
top: 2px;

margin-left: 0.9rem;
color: grey;
font-size: 1.5rem;
}
.appendTodo:hover {
color: black;
cursor: pointer;
}

#todoInput:valid + .appendTodo {
color: black;
}
#todoInput:invalid + .appendTodo {
color: grey;
cursor: not-allowed;
}

/* ------------- */

.listContainer {
display: flex;
align-items: center;

padding: 0.1rem 0.3rem;
margin-bottom: 10px;
margin-left: 1px;
width: 20rem;
}

.checkboxButton {
margin-top: 1.5px;
margin-right: 13px;
transform: scale(1.4);
accent-color: #808080; /* 체크 시 배경색 기본 파랑->회색 */

cursor: pointer;
}

.todoContent {
flex: 1;
text-align: left;
word-break: break-all; /* 문자열 길어지면 강제 줄바꿈*/
}

/* 체크되었을 때 글자 색 변경 */
.checkboxButton:checked ~ .todoContent,
.checkboxButton:checked ~ .deleteButton {
color: #8b8b8b;
}
/* 체크되었을 때 취소선 */
.checkboxButton:checked ~ .todoContent {
text-decoration: line-through;
color: #8b8b8b;
}

.deleteButton {
position: relative;
top: -2px;
margin-left: 0.9rem;

font-size: 1.3rem;
border: none;
background: none;

cursor: pointer;
}
.deleteButton:hover,
.checkboxButton:checked ~ .deleteButton:hover {
color: #d1a29f;
}