Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1주차] 지민재 미션 제출합니다. #7

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions checkmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 18 additions & 2 deletions index.html
Copy link

Choose a reason for hiding this comment

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

처음에 모두 영어로 할 게 아니라면
<html land='ko'>로 바꿔주는 것도 좋을 것 같아요!

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand All @@ -8,7 +8,23 @@
</head>

<body>
<div class="container"></div>
<div class="PageContainer">
<header>
<h1>To Do ✏️</h1>
</header>
<main>
<div class="dateContainer">
<h2>Today's To-Do 🍀</h2>
<p class="date"></p>
<p class="count">✅ : <span class="totalCount">0</span> 🎉 : <span class="completedCount">0</span></p>
<form>
<input type="text" placeholder="할 일 추가">
<button type="submit" id = "submitBtn">추가</button>
</form>
<ul class ="todoBox" ></ul>
</main>
</div>
</body>
<script src="script.js"></script>
</html>

142 changes: 142 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -1 +1,143 @@
//😍CEOS 20기 프론트엔드 파이팅😍

document.addEventListener("DOMContentLoaded", function () {
Copy link
Member

Choose a reason for hiding this comment

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

DOMContentLoaded 이벤트를 사용한 것 멋져요!! 저도 잘 안쓰는 개념이였는데, 덕분에 좋은 개념 하나 챙겨갑니다!!
"HTML 문서의 DOM이 모두 로드된 후에 JavaScript 코드가 안전하게 실행되도록 보장"

const today = new Date(); // 현재 날짜와 시간을 가져오는 Date 객체
const options = { month: "long", day: "numeric", weekday: "long" };
// 날짜, 요일 등 포맷 시 month와 weekday는 긴 형식으로 (9월, 목요일) day는 숫자 형식 (5, 25)

const formattedDate = today.toLocaleDateString("ko-KR", options); // options 형식의 한국어 날짜
document.querySelector(".date").textContent = formattedDate;
Comment on lines +4 to +9
Copy link

Choose a reason for hiding this comment

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

저는 new Date()로 현재 날짜와 시간을 가져왔을 때,
각각
const day= today.getDate() 와 같은 메소드를 사용했었거든요!
지민재님 방식도 너무너무 좋은 방식인 거 같아요!
그치만 월, 일 정도만 필요할 때에는 getDate()와 같은 메소드를 사용하면 조금 더 간소화할 수 있을 것 같아요!

Copy link
Member

Choose a reason for hiding this comment

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

이부분도 같은 요소를 반복적으로 사용할 때는 DOM 접근을 최소화하기 위해 캐싱하는 것이 좋을 것 같아요!

Suggested change
document.querySelector(".date").textContent = formattedDate;
const dateElement = document.querySelector(".date");```

// .Date 요소의 textcontent를 formattedDate으로 설정

const todoInput = document.querySelector("input"); // input 요소 가지고 오기
const todoBox = document.querySelector(".todoBox"); // class가 .todo-box인 요소를 가지고 오기
const submitBtn = document.getElementById("submitBtn"); // id가 submitbtn인 요소를 가지고 오기
const completedCountElem = document.querySelector(".completedCount"); // 완료된 todo 수를 셀 요소
const totalCountElem = document.querySelector(".totalCount"); // 전체 todo 수를 셀 요소

let todoList = []; // 로컬스토리지에 저장될 todo 배열

function setting() {
loadStorage(); // localStorage에 저장된 todoList의 todo들 불러오기
submitBtn.addEventListener("click", function (event) {
event.preventDefault(); // input에 값 입력 후 추가 버튼을 눌러도 새로고침 되지 않도록.
createList();
});
}

function createList() {
Copy link
Member

Choose a reason for hiding this comment

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

createList() 함수 내부에서 유효성 검사, 중복 체크, 할 일 추가, UI 업데이트, 카운트 업데이트 등이 모두 처리되고 있는 것 같아요. 더 유지보수 가능하게 만들기 위해 각 부분을 개별 함수로 분리하는 것도 좋은 방법이므로, 리액트를 사용할때도 고려하면서 코드를 작성해보면 좋을 것 같아요 :)

const newTodo = todoInput.value.trim(); // 문자열 앞 뒤 공백을 제거하는 trim을 이용, 사용자가 input에 입력한 todo를 저장
if (newTodo === ""){
Copy link

Choose a reason for hiding this comment

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

아마 빈문자열 ""는 falsy한 성질을 가지기 때문에
if (!newTodo) 와 같은 방식으로도 바꿀 수 있을 것 같습니다!

alert('오늘의 할 일을 적어주세요!🍀');
return; // 사용자가 입력하지 않았으면 함수 종료
}
// 이미 같은 내용의 투두가 있는지 확인

const isDuplicate = todoList.some((todo) => todo.text === newTodo); //some 메서드, 배열의 각 요소를 순회하면서, 주어진 조건을 만족하는 요소가 하나라도 있으면 true를 반환하고, 조건을 만족하는 요소가 없으면 false를 반환
Copy link

Choose a reason for hiding this comment

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

헉 요건 생각지도 못한 요소였던 거 같아요!!! 좋은 기능,,, 잘 알아갑니당,,,

if (isDuplicate) {
alert("이미 동일한 투두가 있습니다!👏🏻"); // 알림창으로 중복 투두 알림
todoInput.value = "";
return; // 중복되면 함수 종료
}
Comment on lines +36 to +41
Copy link
Member

Choose a reason for hiding this comment

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

중복 예외처리를 some() 메서드를 사용하여 이미 동일한 할 일이 있는지 검사 로직을 구현한점이 인상적이예요!


todoList.push({ text: newTodo, completed: false }); // 배열에 입력 값 저장
Copy link
Member

Choose a reason for hiding this comment

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

현재는 todoList의 항목이 텍스트 값만으로 관리되고 있는 것 같아요. 물론 그에 대한 중복 입력 예외처리가 되어있지만, 하지만 일부 상황에서는 같은 이름의 할 일을 여러 번 추가하고 싶을 수 있음을 고려하면 각 할 일에 고유 ID를 추가하는 방법을 사용하면 해결할 수 있어요! 텍스트 중복을 허용하면서도 할 일의 개별적인 상태(완료 여부 등)를 관리할 수 있을 것 같아요 🤩

Suggested change
todoList.push({ text: newTodo, completed: false }); // 배열에 입력 값 저장
todoList.push({ id: Date.now(), text: newTodo, completed: false });

Copy link
Author

Choose a reason for hiding this comment

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

중복 입력 예외 처리를 한 이유가 고유 ID가 아닌 text로만 todo들을 관리하다 보니 중복 todo가 생겼을 때 하나의 todo를 완료 및 삭제를 해도 다른 중복 todo까지 동일한 이벤트가 적용되어 이 버그의 해결책으로 some()메서드를 이용해 같은 text를 가진 todo가 있으면 입력을 방지하는 것을 선택했었습니다 😂😂

근데 정말 중요한 todo라서 잊지 않기 위해 여러 개 중복 입력할 수도 있을 테고, 또 좀 더 복잡한 중복 입력 예외처리 보다는 고유 ID를 추가해 더 간결하게 기존에 발생했던 버그를 해결할 수 있을 것 같아 고유 ID 추가하는 방식으로 수정하겠습니다!
알려주셔서 감사합니다 🔥👍🏻👍🏻

saveStorage(); // list에 새로운 todo가 추가 됨으로써 변경되었으니 다시 localStorage에 todoList 저장
displayTodo(newTodo, false);
todoInput.value = ""; // 배열에 todo를 저장하고 렌더링 했다면 input을 지워서 다시 입력할 수 있도록
updateCounts(); // 전체 todo 개수에 +1
}

function saveStorage() {
localStorage.setItem("todos", JSON.stringify(todoList)); //localStorage는 문자열 형식의 데이터만 저장할 수 있기 때문에, JSON.stringify()를 사용해 자바스크립트 객체나 배열을 JSON 문자열로 변환한 후 저장. 다시 불러올 때는 JSON.parse() 이용
} // setItem(key,value) 특정 key에 해당 value 할당

function loadStorage() {
const storedTodos = localStorage.getItem("todos"); // 기존에 localStorage에 저장되어있던 배열을 불러오기, 만약 없다면 null이 저장 됨
if (storedTodos) {
todoList = JSON.parse(storedTodos);
todoList.forEach((todo) => displayTodo(todo.text, todo.completed)); // todoList 배열을 순회하며 저장된 모든 todo를 화면에 렌더링
}
updateCounts(); // 새로 고침 시 기존 todoList 배열 불러와서 총 개수 맞게 렌더링
}

function displayTodo(todoText, isCompleted) {
const li = document.createElement("li"); // 새로운 <li>요소 생성. 하나의 todo를 나타냄
Copy link

Choose a reason for hiding this comment

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

그냥 기본 tag이름보다 명확한 네이밍을 해주는 게 좋을 것 같습니다


const checkbox = document.createElement("input"); // 새로운 input 요소 checkbox 생성
checkbox.type = "checkbox"; // 이 요소의 type = checkbox
checkbox.classList.add("todoCheckBox"); // 스타일링을 위해 클래스 목록에 클래스 이름 추가! 즉 checkbox에 할당되는 클래스 이름이 todo-checkbox
checkbox.checked = isCompleted; // isCompleted가 true라면 checkbox가 체크 됨

checkbox.addEventListener("change", function () { // checkbox의 상태가 바뀔 때, 즉 checked의 속성이 변경될 때 (사용자가 체크박스를 체크하거나 해제할 때) 실행될 이벤트 핸들러
toggleTodoCompletion(todoText);
li.querySelector("span").style.textDecoration = checkbox.checked ? "line-through" : "none";
}); // <li> 요소의 첫 번째 <span> 요소를 찾아 스타일링. 만약 체크박스가 체크 되어있으면? 선을 긋고 해제되어 있으면 선을 없앤다!

const todoSpan = document.createElement("span"); //<li> 요소 안에 쓰여질 텍스트를 <span> 요소로 설정
todoSpan.classList.add("todoSpan");
todoSpan.textContent = todoText; // 매개변수로 전달 받은 input의 text를 변수 todoSpan의 텍스트 내용으로 저장
if (isCompleted) {
todoSpan.style.textDecoration = "line-through";
}

const deleteBtn = document.createElement("button"); // 새로운 <button> 요소 생성
deleteBtn.textContent = "삭제"; // deleteBtn의 텍스트 내용을 삭제라고 지정
deleteBtn.classList.add("deleteBtn");
deleteBtn.addEventListener("click", function () { // 버튼이 클릭 되었을 때 실행될 함수!!
if(confirm("정말 삭제하시겠습니까?")){
alert("todo가 삭제되었습니다.");
deleteTodo(todoText, li);
}else{
alert("Kepp going!🔥");
}
});

li.append(checkbox, todoSpan, deleteBtn); // appendChild를 append로 간소화!

todoBox.appendChild(li); // <ul> 요소를 불러 온 todoBox에 <li>들 추가
}

function toggleTodoCompletion(todoText) { // 배열 todoList에서 특정 할 일 항목의 완료 상태를 토글시키는 역할
todoList = todoList.map((todo) => { // map 메서드는 배열의 각 항목을 변형하여 새로운 배열을 반환한다!!
if (todo.text === todoText) { // 순회하던 배열의 특정 원소의 text와 display 함수에서 props로 전달 받은 체크 상태가 변경된 todo의 text가 동일하다면?
return { ...todo, completed: !todo.completed }; // 그 원소의 completed 상태 토글
}
return todo;
});
saveStorage(); // todoList가 업데이트 되어 새롭게 localStorage에 저장
updateCounts(); // 체크 표시 상태에 따라 완료된 todo 개수 변경
}
Comment on lines +100 to +109
Copy link

Choose a reason for hiding this comment

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

toggleTodoCompletion 함수를 보니까 map 메서드를 통해 모든 todolist를 순회하면서 todo의 상태를 바꾸고 있는 구조인 것 같아요. 물론 이 방식도 유효하지만 find() 메서드를 사용하여 좀 더 효율적으로 코드를 수정할 수 있을 것 같아요. 대략적으로 아래와 같이 바꾸면 어떨까요?

Suggested change
function toggleTodoCompletion(todoText) { // 배열 todoList에서 특정 할 일 항목의 완료 상태를 토글시키는 역할
todoList = todoList.map((todo) => { // map 메서드는 배열의 각 항목을 변형하여 새로운 배열을 반환한다!!
if (todo.text === todoText) { // 순회하던 배열의 특정 원소의 text와 display 함수에서 props로 전달 받은 체크 상태가 변경된 todo의 text가 동일하다면?
return { ...todo, completed: !todo.completed }; // 그 원소의 completed 상태 토글
}
return todo;
});
saveStorage(); // todoList가 업데이트 되어 새롭게 localStorage에 저장
updateCounts(); // 체크 표시 상태에 따라 완료된 todo 개수 변경
}
function toggleTodoCompletion(todoText) {
const todo = todoList.find((todo) => todo.text === todoText);
if (todo) {
todo.completed = !todo.completed;
saveStorage();
updateCounts();
}
}

map() 메서드는 배열의 모든 요소를 순회하면서 각 요소를 변형한 새로운 배열을 반환하는데, 이번 바닐라JS 투두리스트에서 완료 상태를 토글하는 작업은 굳이 배열 전체를 변형할 필요가 없을 것 같습니다. find() 메서드는 배열을 순회하다가 첫번째로 조건을 만족하는 요소를 찾으면 그 요소를 반환하고 순회를 멈추기 때문에 하나의 요소만 업데이트해야하는 토글 함수에 더 효율적인 방향이라고 생각합니다..! 가독성 면에서도 짧고 간단해서 보기가 수월할 것 같구요..!

map()은 일반적으로 불변성을 유지하기 위해 새로운 배열을 반환하는 상황에서 많이 사용된다고 알고 있는데, (제 생각으로는) 이 부분의 코드에서는 불변성 유지가 필수적이지 않은 것 같아요. 만약 리액트와 같은 상태 관리 시스템에서 개발을 한다면 어디서든 불변성을 유지하는 것이 매우 중요하지만, (리액트는 상태변화가 있을 때 렌더링을 트리거하기 때문에 불변성을 지켜야 변경된 상태만 효율적으로 감지할 수 있기 때문) 로컬 스토리지에 업데이트하는 단순한 데이터 처리에서는 불변성을 유지하지 않아도 성능에 문제가 생길 확률이 적습니다..!

불변성 유지 여부에 따른 코드 전개


function deleteTodo(todoText, li) {
todoList = todoList.filter((todo) => todo.text !== todoText); // 배열을 순회하면서 props로 전달 받은 text와 동일하지 않은 text만 걸러서 즉, 전달받은 text는 존재하지 않는 배열을 새롭게 만들어 낸다
saveStorage();
li.classList.add('remove'); // 삭제될 요소의 스타일링을 위해 class name 부여
li.addEventListener('animationend', () => {
li.remove(); // 애니메이션 끝난 후 실제로 제거
});
Comment on lines +114 to +117
Copy link
Member

Choose a reason for hiding this comment

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

할 일 삭제시 애니메이션 및 애니메이션이 끝난 후에 DOM에서 완전히 제거하는 방식을 적용한 디테일 멋져요오!

updateCounts();
}

let isAllCompleted = JSON.parse(localStorage.getItem("isAllCompleted")) || false; // localStorage에서 값을 가져오거나 없으면 false로 초기화

function updateCounts() {
const totalTodos = todoList.length;
const completedTodos = todoList.filter((todo) => todo.completed).length; // 완료된 todo 필터링

completedCountElem.textContent = completedTodos;
totalCountElem.textContent = totalTodos;

// 만약 할 일이 모두 완료되었고, 그 상태가 처음으로 발생한 경우에만 alert 표시
if (totalTodos > 0 && completedTodos === totalTodos && !isAllCompleted) {
alert('축하합니다! 모든 할 일을 완료하셨습니다! 🎉');
isAllCompleted = true; // 모든 할 일이 완료되었다고 기록
localStorage.setItem("isAllCompleted", JSON.stringify(isAllCompleted)); // 상태를 localStorage에 저장
} else if (completedTodos < totalTodos) {
// 할 일이 삭제되거나 새로 추가되면 상태를 초기화
isAllCompleted = false;
localStorage.setItem("isAllCompleted", JSON.stringify(isAllCompleted)); // 상태를 localStorage에 저장
}
}

setting();
});
206 changes: 206 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
@@ -1 +1,207 @@
/* 본인의 디자인 감각을 최대한 발휘해주세요! */
body{
margin: 0;
}
.PageContainer{
display: flex;
width: 100vw;
Copy link
Member

Choose a reason for hiding this comment

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

vw는 브라우저 뷰포트 크기에 맞추는 단위라 이를 사용하는 것도 좋지만 저의 경우는 전체 너비의 경우에는 상대단위 (%)를 사용하는 것이 오류 발생률도 적어서 반응형 구현할 때 자주 씁니다. 민재님도 상대단위 한번 사용해보시고 편한 방법으로 사용해보세요!!

height: 100vh;
flex-direction: column; /* 요소 정렬 방향: 세로 */
align-items: center; /* 수평 정렬, 세로 축이 주축이므로 교차 축인 가로 축에서의 정렬을 담당 */
overflow-x:hidden; /* 가로 스크롤 제거 */
}
header{
display: flex;
background-color: rgba(110, 190, 110, 0.895);
width: 100%; /* 화면 꽉 차게 */
height: 4rem;
color: white;
font-weight: bold;
align-items: center; /* 주축: 가로 축, 수직 정렬*/
justify-content: center; /* 수평 정렬 */
}

h1{
display: flex;
margin: 0;
text-shadow: 0 5px 5px rgba(0, 0, 0, 0.205);
}
h2{
display: flex;
margin: 0;
}

main{
display: flex;
flex-direction: column;
width: 30rem;
height: 90%; /* 화면의 100% 높이로 고정 */
overflow-y: scroll; /* 할 일이 많아지면 스크롤이 생기도록 설정 */
box-shadow: 0 0.875rem 1.75rem rgba(0,0,0,0.25), 0 0.625rem 0.625rem rgba(0,0,0,0.22);
border-radius: 2rem;
/*box-shadow: [x-offset] [y-offset] [blur-radius] [spread-radius] [color]; 깊이감과 입체감을 더하기 위해 2개 그림자 사용*/
}

/* 스크롤바 숨기기 (웹킷 기반 브라우저(Chrome, Safari 등)전용) */
main::-webkit-scrollbar {
display: none; /* 스크롤바 숨기기 */
}


.dateContainer{
display: flex;
flex-direction: column;
margin: 3.125rem;
gap: 0.3125rem;
color: rgba(84, 170, 84, 0.922);
}
.date{
display: flex;
font-size: 0.9375rem;
}

.count{
margin-top: 0;
}

form {
display: flex;
position: relative;
width: calc(100% - 5rem);
padding: 1.3rem 1.3rem 1.3rem 3.3rem; /* background로 넣은 체크 이미지와 안 겹치도록 왼쪽 패딩 추가*/
box-shadow: 0 0 5px rgba(0, 0, 0, 0.11), 0 5px 5px rgba(0, 0, 0, 0.178);
border-radius: 0.625rem;
background-image: url('checkmark.svg');
background-size: 1.5rem;
background-position: 1rem center; /* background 이미지 위치, 왼쪽으로부터 1rem 떨어진 곳*/
background-repeat: no-repeat;
}

input{
width:calc(100% - 4rem); /* 왼쪽 추가 버튼이 차지한 공간을 제외한 부분*/
font-size: 1rem;
border: none;
outline: 0;
}
input::-webkit-input-placeholder {
color: rgb(94, 169, 139);
}

#submitBtn {
Copy link

Choose a reason for hiding this comment

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

submitBtn과 deleteBtn은 색상 외에 거의 비슷한 스타일을 가지고 있는데, 코드 상 너비와 배치 방식이 서로 다르게 설정되어 있습니다. (제출 버튼은 absolute 포지션, 삭제 버튼은 flexbox 통한 정렬) button 등으로 공통된 요소를 맞춰주신 다음에 id명, class명으로 세부적인 요소를 조절해주시면 통일성 측면에서도, 나중에 유지 보수 측면에서도 더욱 좋을 것 같아요!

position: absolute; /* 부모 요소인 form 기준으로 배치 */
top: 50%; /* 부모 요소의 상단에서 50% 지점에 배치 */
transform: translateY(-50%); /* 자신의 높이의 50%만큼 위쪽으로 이동, 수직 중앙 정렬 시키기 */
right: 1.25rem; /* 부모 요소의 오른쪽에서 1.25rem */
background-color: transparent;
color: rgb(94, 169, 139);
padding: 5px 10px;
border: 1.5px solid rgb(94, 169, 139);
border-radius: 0.625rem;
cursor: pointer;
}

#submitBtn:hover {
background-color:#b5b6b779;
font-weight: bold;
}

.todoBox {
display: flex;
gap: 0.625rem; /* todo끼리의 gap*/
padding: 0;
flex-direction: column;
}

.todoBox li {
display: flex;
width: calc(100% - 2.5rem);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.11), 0 5px 5px rgba(0, 0, 0, 0.178);
Copy link

Choose a reason for hiding this comment

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

현재 동일한 box-shadow 속성이 여러 곳에서 쓰이고 있네요. 저도 이번에 공부하면서 처음 알게 된 건데.. 이 경우 css 변수를 활용해봐도 좋을 것 같아요!

:root {
    --box-shadow: 0 0.875rem 1.75rem rgba(0,0,0,0.25), 0 0.625rem 0.625rem rgba(0,0,0,0.22);
}

이렇게 css 파일에서 변수를 선언하면, 해당 변수는 전역에서 사용될 수 있게 돼요. (단, 어디에도 자동으로 적용되지는 않아요!) 저렇게 변수를 선언한 이후에는

main, .todoBox li {
    box-shadow: var(--box-shadow); /* 정의한 변수를 여기서 사용 */
}

이와 같이 var 을 활용해서 간단하게 표현할 수 있답니다..!

CSS 변수 정리

padding: 1rem 1.3rem 1rem 1rem;
font-size: 1rem;
color: white;
border: none;
border-radius: 10px;
justify-content: space-between;
align-items: center; /* 수직 정렬 */
font-size: 1rem;
color: rgb(94, 169, 139);
animation: slideDown 0.3s ease-out; /* ease-out, 애니메이션이 끝날 때 속도가 점점 줄어드는 효과 */
}

.todoBox .todoSpan{
width: 15.625rem;
overflow-wrap: break-word;
}

/* 새로운 요소가 추가될 때 slide 되는 애니메이션 */
@keyframes slideDown {
0% {
opacity: 0; /* 요소가 처음에는 투명함 */
transform: translateY(-20px); /* 애니메이션이 시작 될 때, 처음엔 위쪽에 위치 */
}
100% {
opacity: 1; /* 투명도가 0에서 1로 변화 */
transform: translateY(0); /* 제자리로 이동, 원래의 위치! */
}
} /* 결국 위에서 아래로 부드럽게 이동하는 효과*/

/* 요소가 삭제될 때 애니메이션 */
.todoBox .remove {
animation: fadeOut 0.3s forwards; /* fadeOut 애니메이션의 마지막 상태가 계속 유지 됨. 즉 투명한 상태로!! */
}

@keyframes fadeOut {
0% {
opacity: 1; /* 처음에는 완전히 보이고 */
transform: scale(1); /* 원래 크기임 */
}
100% {
opacity: 0;
transform: scale(0.9); /* 요소가 원래 크기의 90%로 축소 */
}
}


.todoBox .deleteBtn {
border: none;
color: rgba(209, 46, 46, 0.664);
padding: 5px 10px;
border-radius: 10px;
cursor: pointer;
background-color: transparent;
border: 1.5px solid rgba(193, 49, 49, 0.664);
}

.todoBox li button.deleteBtn:hover {
background-color: #b5b6b779; /* 삭제 버튼에 호버 효과 */
font-weight: bold;
}

/* Custom Checkbox */
.todoCheckBox {
width: 1.5rem;
height: 1.5rem;
cursor: pointer;
appearance: none; /* 기본 체크박스 스타일 제거 */
border-radius: 50%; /* 둥근 모양 */
border: 2px solid rgb(94, 169, 139); /* 체크되지 않았을 때 테두리 */
background-color: transparent; /* 기본 배경 투명 */
}

/* 체크박스가 체크되었을 때 */
.todoCheckBox:checked {
background-color: rgb(103, 219, 147); /* 체크 시 배경색 */
border-color: rgb(103, 219, 147); /* 테두리도 배경색과 동일하게 */
position: relative;
}

/* 체크박스가 체크되었을 때의 내부 체크 표시 */
.todoCheckBox:checked::before {
content: '✓'; /* 체크 표시 */
font-size: 1rem;
color: white; /* 체크 표시 색상 */
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%); /* 중앙 정렬 */
}