diff --git a/README.md b/README.md
index 3c0710d2..a4e96771 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,98 @@
-# react-todo-list-precourse
\ No newline at end of file
+# [강원대 FE_허윤수]
+
+이 애플리케이션은 사용자가 할 일을 추가, 삭제, 완료 상태로 전환할 수 있는 간단한 할 일 목록(Todo List) 관리 웹 애플리케이션입니다. 동작은 TodoMVC의 기능 동작을 참고하였습니다.
+
+---
+
+## 과제 진행 소감
+
+"함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라"라는 요구사항에 맞추기 위해 최대한 노력했습니다. TodoApp(src\components\TodoApp\TodoApp.jsx) 컴포넌트를 더 작은 단위로 분리하여 상태 관리를 보다 효율적으로 하려고 시도했습니다. 이를 위해 상태를 저장하는 부분을 여러 컨테이너로 분리했으나, 이 과정에서 각 컨테이너 간에 상태가 공유되지 않는 문제점이 발생했습니다. 😓
+
+이 문제를 해결하기 위해 CONTEXT API를 사용할 수 있다는 것을 알게 되었습니다. 하지만 아직 CONTEXT API에 대한 공부가 충분히 이루어지지 않아, 이를 적용하는 데 어려움을 겪었습니다. 앞으로 고급 개념을 학습하여 더 나은 코드를 작성할 수 있도록 노력할 것입니다. 📚
+
+---
+
+## 저장소
+
+저장소 URL: [https://github.com/sugoring/react-todo-list-precourse.git](https://github.com/sugoring/react-todo-list-precourse.git)
+
+## 브랜치
+
+- **메인 브랜치(과제 제출)**: `sugoring`
+- **오류 수정 브랜치**: `fix/addTodo-function-error`
+- **개발 브랜치**: `develop`
+
+### 1. 초기 개발
+
+초기에 메인 브랜치인 `sugoring`에서 개발을 시작했습니다.
+
+### 2. 오류 수정
+
+개발 도중 `addTodo` 함수와 관련된 오류가 발생하여, 오류를 해결하기 위해 `fix/addTodo-function-error` 브랜치에서 수정 작업을 진행하였습니다.
+
+### 3. 개발 브랜치 전환
+
+오류 수정 후, 메인 브랜치(`sugoring`)로 돌아와서 오류가 해결된 상태를 반영한 뒤, 이후 개발 작업은 `develop` 브랜치에서 계속 진행하였습니다.
+
+### 4. 과제 제출
+
+개발 작업을 완료한 후, `develop` 브랜치에서 작업 내용을 `sugoring` 브랜치로 PR(Pull Request)하여 병합하였고, 과제를 제출하였습니다.
+
+---
+
+## 기본 기능
+
+### [할 일 추가 기능 구현]
+
+- [x] **할 일 입력 폼 생성**
+ - 사용자는 Enter 키 또는 추가 버튼을 통해 새로운 할 일을 입력할 수 있습니다.
+- [x] **할 일 데이터 유효성 검사 (빈 문자열)**
+ - 입력된 할 일이 빈 문자열인 경우, 경고 메시지를 표시하고 추가하지 않습니다.
+- [x] **할 일 목록 업데이트**
+ - 유효한 할 일은 목록에 추가되고 화면에 반영됩니다.
+
+### [할 일 삭제 기능 구현]
+
+- [x] **삭제 요청 인터페이스 생성**
+ - 각 할 일 항목 옆에 삭제 버튼이 있습니다.
+- [x] **삭제 요청 처리**
+ - 사용자가 삭제 버튼을 클릭하면 해당 할 일이 목록에서 제거됩니다.
+
+### [할 일 목록 보기 기능 구현]
+
+- [x] **목록 불러오기**
+ - 할 일 목록을 불러옵니다.
+- [x] **목록 표시**
+ - 불러온 할 일 목록을 화면에 표시합니다.
+
+### [할 일 완료 상태 전환 기능 구현]
+
+- [x] **완료 상태 전환 인터페이스 생성**
+ - 각 할 일 항목 옆에 완료/미완료 버튼이 있습니다.
+- [x] **상태 전환 요청 처리**
+ - 사용자가 완료/미완료 버튼을 클릭하면 해당 할 일의 완료 상태가 토글됩니다.
+
+---
+
+## 선택 요구 사항
+
+### [할 일 필터링]
+
+- [x] **필터링 버튼 추가**
+ - 할 일 목록 상단에 '전체', '진행 중', '완료' 버튼을 추가합니다.
+- [x] **필터링 기능 구현**
+ - 사용자는 이 버튼들을 클릭하여 필터링할 수 있습니다.
+- [x] **실시간 필터링 반영**
+ - 필터링된 목록이 화면에 반영됩니다.
+
+### [해야 할 일의 총 개수 확인]
+
+- [x] **총 개수 표시 영역 추가**
+ - 할 일 목록 하단에 남아있는 할 일의 총 개수를 표시합니다.
+- [x] **개별 개수 표시 옵션**
+ - 완료된 할 일과 미완료된 할 일의 개수를 각각 표시합니다.
+
+### [데이터 지속성]
+
+- [x] **데이터 저장 기능 구현**
+ - 새로고침을 하여도 이전에 작성한 데이터는 유지됩니다.
diff --git a/index.html b/index.html
index b021b5c8..de977dc3 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,12 @@
-
-
+
+
-
+ [강원대 FE_허윤수]
-
+
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..5926fa77
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import Main from "./Main.jsx";
+
+const App = () => {
+ return React.createElement(Main);
+};
+
+const root = ReactDOM.createRoot(document.getElementById("app"));
+root.render(React.createElement(App));
diff --git a/src/Main.css b/src/Main.css
new file mode 100644
index 00000000..cc9bf63e
--- /dev/null
+++ b/src/Main.css
@@ -0,0 +1,11 @@
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: #f5f5f5;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ }
+
diff --git a/src/Main.jsx b/src/Main.jsx
new file mode 100644
index 00000000..e8359024
--- /dev/null
+++ b/src/Main.jsx
@@ -0,0 +1,13 @@
+import React from "react";
+import TodoContainer from "./components/todoApp/todoContainer";
+import "./Main.css";
+
+const Main = () => {
+ return (
+
+
+
+ );
+};
+
+export default Main;
diff --git a/src/components/todoApp/stats/activeTodos.jsx b/src/components/todoApp/stats/activeTodos.jsx
new file mode 100644
index 00000000..1f70a664
--- /dev/null
+++ b/src/components/todoApp/stats/activeTodos.jsx
@@ -0,0 +1,8 @@
+// 진행 중인 할 일 개수를 표시하는 컴포넌트
+import React from "react";
+
+const ActiveTodos = ({ active }) => {
+ return 진행 중인 할 일: {active} ;
+};
+
+export default ActiveTodos;
diff --git a/src/components/todoApp/stats/completedTodos.jsx b/src/components/todoApp/stats/completedTodos.jsx
new file mode 100644
index 00000000..53b3c8ee
--- /dev/null
+++ b/src/components/todoApp/stats/completedTodos.jsx
@@ -0,0 +1,8 @@
+// 완료된 할 일 개수를 표시하는 컴포넌트
+import React from "react";
+
+const CompletedTodos = ({ completed }) => {
+ return 완료된 할 일: {completed} ;
+};
+
+export default CompletedTodos;
diff --git a/src/components/todoApp/stats/totalTodos.jsx b/src/components/todoApp/stats/totalTodos.jsx
new file mode 100644
index 00000000..c58278a4
--- /dev/null
+++ b/src/components/todoApp/stats/totalTodos.jsx
@@ -0,0 +1,8 @@
+// 전체 할 일 개수를 표시하는 컴포넌트
+import React from "react";
+
+const TotalTodos = ({ total }) => {
+ return 총 할 일 개수: {total} ;
+};
+
+export default TotalTodos;
diff --git a/src/components/todoApp/todoApp.css b/src/components/todoApp/todoApp.css
new file mode 100644
index 00000000..28b1aa69
--- /dev/null
+++ b/src/components/todoApp/todoApp.css
@@ -0,0 +1,103 @@
+.todo-app {
+ background-color: #ffffff;
+ border-radius: 10px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ max-width: 800px;
+ width: 100%;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ }
+
+ .todo-form,
+ .todo-list,
+ .todo-filter,
+ .todo-stats {
+ background-color: #f9f9f9;
+ border-radius: 8px;
+ padding: 15px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ .todo-form input,
+ .todo-form button {
+ padding: 10px;
+ font-size: 16px;
+ border-radius: 5px;
+ border: 1px solid #ddd;
+ }
+
+ .todo-form button {
+ background-color: #b83f45;
+ color: #fff;
+ border: none;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ }
+
+ .todo-form button:hover {
+ background-color: #a6373e;
+ }
+
+ .todo-list .todo-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px;
+ border-bottom: 1px solid #eee;
+ }
+
+ .todo-list .todo-item:last-child {
+ border-bottom: none;
+ }
+
+ .todo-filter {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ }
+
+ .todo-filter button {
+ padding: 10px 20px;
+ border: 2px solid #b83f45;
+ border-radius: 5px;
+ background-color: transparent;
+ color: #b83f45;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: 16px;
+ }
+
+ .todo-filter button:hover {
+ background-color: #b83f45;
+ color: #fff;
+ }
+
+ .todo-filter button.active {
+ background-color: #b83f45;
+ color: #fff;
+ font-weight: bold;
+ }
+
+ .todo-stats {
+ display: flex;
+ justify-content: space-around;
+ }
+
+ .todo-stats .stat {
+ text-align: center;
+ }
+
+ .todo-stats .stat h3 {
+ margin: 0;
+ font-size: 24px;
+ color: #b83f45;
+ }
+
+ .todo-stats .stat p {
+ margin: 5px 0 0;
+ font-size: 16px;
+ color: #666;
+ }
+
\ No newline at end of file
diff --git a/src/components/todoApp/todoApp.jsx b/src/components/todoApp/todoApp.jsx
new file mode 100644
index 00000000..f441ba9d
--- /dev/null
+++ b/src/components/todoApp/todoApp.jsx
@@ -0,0 +1,41 @@
+// src/components/todoApp/todoApp.jsx
+import React from "react";
+import TodoForm from "../todoForm/todoForm";
+import TodoList from "../todoList/todoList";
+import TodoFilter from "../todoFilter/todoFilter";
+import TodoStats from "./todoStats";
+import useTodos from "../../hooks/todos/useTodos";
+import "./todoApp.css";
+
+// 할 일 앱 컴포넌트: 할 일 입력 폼, 할 일 필터, 할 일 목록, 할 일 통계를 표시하는 앱
+const TodoApp = () => {
+ const {
+ todos,
+ allTodos,
+ handleAddTodo,
+ handleToggleComplete,
+ handleDeleteTodo,
+ handleSetFilter,
+ filter,
+ FILTERS,
+ } = useTodos();
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default TodoApp;
diff --git a/src/components/todoApp/todoContainer.css b/src/components/todoApp/todoContainer.css
new file mode 100644
index 00000000..43b1b40f
--- /dev/null
+++ b/src/components/todoApp/todoContainer.css
@@ -0,0 +1,22 @@
+.todo-container {
+ text-align: center;
+ margin: 20px auto;
+}
+
+.todo-container h1 {
+ font-size: 80px;
+ color: #b83f45;
+ margin-bottom: 10px;
+}
+
+.todo-container p {
+ font-size: 16px;
+ color: #666;
+ margin-bottom: 20px;
+}
+
+.footer {
+ font-size: 14px;
+ color: #999;
+ margin-top: 20px;
+}
diff --git a/src/components/todoApp/todoContainer.jsx b/src/components/todoApp/todoContainer.jsx
new file mode 100644
index 00000000..3a1e6ef4
--- /dev/null
+++ b/src/components/todoApp/todoContainer.jsx
@@ -0,0 +1,16 @@
+import React from "react";
+import TodoApp from "./todoApp";
+import "./todoContainer.css";
+
+const TodoContainer = () => {
+ return (
+
+
todos
+
Enter 키나 추가 버튼을 사용하여 할 일을 목록에 추가하세요.
+
+
Created by [강원대 FE_허윤수]
+
+ );
+};
+
+export default TodoContainer;
diff --git a/src/components/todoApp/todoFilterContainer.jsx b/src/components/todoApp/todoFilterContainer.jsx
new file mode 100644
index 00000000..6f103bc5
--- /dev/null
+++ b/src/components/todoApp/todoFilterContainer.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import TodoFilter from "../todoFilter/todoFilter";
+
+// 할 일 필터 컨테이너 컴포넌트: 할 일 필터를 표시하고 설정하는 기능을 제공하는 컴포넌트
+const TodoFilterContainer = ({ useTodos }) => {
+ const { filter, FILTERS, handleSetFilter } = useTodos();
+
+ return (
+
+ );
+};
+
+export default TodoFilterContainer;
diff --git a/src/components/todoApp/todoFormContainer.jsx b/src/components/todoApp/todoFormContainer.jsx
new file mode 100644
index 00000000..6bc8d4b3
--- /dev/null
+++ b/src/components/todoApp/todoFormContainer.jsx
@@ -0,0 +1,11 @@
+import React from "react";
+import TodoForm from "../todoForm/todoForm";
+
+// 할 일 입력 폼 컨테이너 컴포넌트: 할 일 입력 폼을 표시하고 할 일 추가 기능을 제공하는 컴포넌트
+const TodoFormContainer = ({ useTodos }) => {
+ const { handleAddTodo } = useTodos();
+
+ return ;
+};
+
+export default TodoFormContainer;
diff --git a/src/components/todoApp/todoListContainer.jsx b/src/components/todoApp/todoListContainer.jsx
new file mode 100644
index 00000000..1c39b669
--- /dev/null
+++ b/src/components/todoApp/todoListContainer.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import TodoList from "../todoList/todoList";
+
+// 할 일 목록 컨테이너 컴포넌트: 할 일 목록을 표시하고 완료 토글 및 삭제 기능을 제공하는 컴포넌트
+const TodoListContainer = ({ useTodos }) => {
+ const { todos, handleToggleComplete, handleDeleteTodo } = useTodos();
+
+ return (
+
+ );
+};
+
+export default TodoListContainer;
diff --git a/src/components/todoApp/todoStats.jsx b/src/components/todoApp/todoStats.jsx
new file mode 100644
index 00000000..2bb081de
--- /dev/null
+++ b/src/components/todoApp/todoStats.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import TotalTodos from "./stats/totalTodos";
+import CompletedTodos from "./stats/completedTodos";
+import ActiveTodos from "./stats/activeTodos";
+
+// 할 일 통계 컴포넌트: 전체 할 일, 완료된 할 일을 표시하는 컴포넌트
+const TodoStats = ({ todos, allTodos }) => {
+ const totalTodos = allTodos.length;
+ const completedTodos = allTodos.filter((todo) => todo.completed).length;
+ const activeTodos = totalTodos - completedTodos;
+
+ return (
+
+ );
+};
+
+export default TodoStats;
diff --git a/src/components/todoApp/todoStatsContainer.jsx b/src/components/todoApp/todoStatsContainer.jsx
new file mode 100644
index 00000000..2f466f86
--- /dev/null
+++ b/src/components/todoApp/todoStatsContainer.jsx
@@ -0,0 +1,15 @@
+import React from "react";
+import TodoStats from "./todoStats";
+
+// 할 일 통계 컨테이너 컴포넌트: 할 일 통계를 표시하는 컴포넌트
+const TodoStatsContainer = ({ useTodos }) => {
+ const { todos, allTodos, filter } = useTodos();
+
+ return (
+
+
+
+ );
+};
+
+export default TodoStatsContainer;
diff --git a/src/components/todoFilter/buttons/activeFilterButton.jsx b/src/components/todoFilter/buttons/activeFilterButton.jsx
new file mode 100644
index 00000000..0d595406
--- /dev/null
+++ b/src/components/todoFilter/buttons/activeFilterButton.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import FilterButton from "./filterButton";
+import FILTERS from "../../../utils/filters";
+
+// 활성 필터 버튼 컴포넌트: 완료되지 않은 할 일을 필터링하는 버튼
+const ActiveFilterButton = ({ filter, handleSetFilter }) => {
+ return (
+ handleSetFilter(FILTERS.ACTIVE)}
+ >
+ 진행 중
+
+ );
+};
+
+export default ActiveFilterButton;
diff --git a/src/components/todoFilter/buttons/allFilterButton.jsx b/src/components/todoFilter/buttons/allFilterButton.jsx
new file mode 100644
index 00000000..872ed1ce
--- /dev/null
+++ b/src/components/todoFilter/buttons/allFilterButton.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import FilterButton from "./filterButton";
+import FILTERS from "../../../utils/filters";
+
+// 전체 필터 버튼 컴포넌트: 모든 할 일을 표시하는 버튼
+const AllFilterButton = ({ filter, handleSetFilter }) => {
+ return (
+ handleSetFilter(FILTERS.ALL)}
+ >
+ 전체
+
+ );
+};
+
+export default AllFilterButton;
diff --git a/src/components/todoFilter/buttons/completedFilterButton.jsx b/src/components/todoFilter/buttons/completedFilterButton.jsx
new file mode 100644
index 00000000..c9bd432e
--- /dev/null
+++ b/src/components/todoFilter/buttons/completedFilterButton.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import FilterButton from "./filterButton";
+import FILTERS from "../../../utils/filters";
+
+// 완료 필터 버튼 컴포넌트: 완료된 할 일을 필터링하는 버튼
+const CompletedFilterButton = ({ filter, handleSetFilter }) => {
+ return (
+ handleSetFilter(FILTERS.COMPLETED)}
+ >
+ 완료
+
+ );
+};
+
+export default CompletedFilterButton;
diff --git a/src/components/todoFilter/buttons/filterButton.jsx b/src/components/todoFilter/buttons/filterButton.jsx
new file mode 100644
index 00000000..17289a24
--- /dev/null
+++ b/src/components/todoFilter/buttons/filterButton.jsx
@@ -0,0 +1,14 @@
+import React from "react";
+// 필터 버튼 컴포넌트: 활성화 여부에 따라 스타일이 변경되는 버튼
+const FilterButton = ({ isActive, onClick, children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default FilterButton;
diff --git a/src/components/todoFilter/todoFilter.css b/src/components/todoFilter/todoFilter.css
new file mode 100644
index 00000000..920980c7
--- /dev/null
+++ b/src/components/todoFilter/todoFilter.css
@@ -0,0 +1,31 @@
+.todo-filter {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ margin: 20px 0;
+ }
+
+ .filter-button {
+ padding: 10px 20px;
+ border: 2px solid #b83f45;
+ border-radius: 5px;
+ background-color: transparent;
+ color: #b83f45;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: 16px;
+ outline: none;
+ }
+
+ .filter-button:hover {
+ background-color: #b83f45;
+ color: #fff;
+ transform: scale(1.05);
+
+ .filter-button.active {
+ background-color: #b83f45;
+ color: #fff;
+ font-weight: bold;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ }
+}
\ No newline at end of file
diff --git a/src/components/todoFilter/todoFilter.jsx b/src/components/todoFilter/todoFilter.jsx
new file mode 100644
index 00000000..2ec1f4e0
--- /dev/null
+++ b/src/components/todoFilter/todoFilter.jsx
@@ -0,0 +1,20 @@
+import React from "react";
+import AllFilterButton from "./buttons/allFilterButton";
+import ActiveFilterButton from "./buttons/activeFilterButton";
+import CompletedFilterButton from "./buttons/completedFilterButton";
+import "./todoFilter.css";
+
+const TodoFilter = ({ filter, handleSetFilter }) => {
+ return (
+
+ );
+};
+
+export default TodoFilter;
diff --git a/src/components/todoForm/todoForm.css b/src/components/todoForm/todoForm.css
new file mode 100644
index 00000000..3ac31332
--- /dev/null
+++ b/src/components/todoForm/todoForm.css
@@ -0,0 +1,44 @@
+.todo-form {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 20px;
+ }
+
+ .todo-form input {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 16px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: border-color 0.3s ease;
+ }
+
+ .todo-form input:focus {
+ outline: none;
+ border-color: #b83f45;
+ }
+
+ .todo-form input::placeholder {
+ color: #999;
+ font-style: italic;
+ }
+
+ .add-button {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 5px;
+ background-color: #b83f45;
+ color: #fff;
+ font-size: 16px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ .add-button:hover {
+ background-color: #a6373e;
+ }
+
\ No newline at end of file
diff --git a/src/components/todoForm/todoForm.jsx b/src/components/todoForm/todoForm.jsx
new file mode 100644
index 00000000..7015ece5
--- /dev/null
+++ b/src/components/todoForm/todoForm.jsx
@@ -0,0 +1,22 @@
+import React, { useState } from "react";
+import TodoInput from "./todoInput";
+import handleSubmit from "../../handlers/handleSubmit";
+import "./todoForm.css";
+
+const TodoForm = ({ addTodo }) => {
+ const [inputValue, setInputValue] = useState("");
+
+ return (
+
+ );
+};
+
+export default TodoForm;
diff --git a/src/components/todoForm/todoInput.jsx b/src/components/todoForm/todoInput.jsx
new file mode 100644
index 00000000..b8d0d13b
--- /dev/null
+++ b/src/components/todoForm/todoInput.jsx
@@ -0,0 +1,16 @@
+// src/components/todoForm/todoInput.jsx
+import React from "react";
+
+const TodoInput = ({ inputValue, setInputValue }) => {
+ return (
+ setInputValue(e.target.value)}
+ placeholder="What needs to be done?"
+ className="todo-input"
+ />
+ );
+};
+
+export default TodoInput;
diff --git a/src/components/todoList/buttons/deleteButton.jsx b/src/components/todoList/buttons/deleteButton.jsx
new file mode 100644
index 00000000..430126b4
--- /dev/null
+++ b/src/components/todoList/buttons/deleteButton.jsx
@@ -0,0 +1,11 @@
+import React from "react";
+// 삭제 버튼 컴포넌트: 클릭하면 삭제 동작 실행
+const DeleteButton = ({ onDelete }) => {
+ return (
+
+ x
+
+ );
+};
+
+export default DeleteButton;
diff --git a/src/components/todoList/buttons/toggleCompleteButton.jsx b/src/components/todoList/buttons/toggleCompleteButton.jsx
new file mode 100644
index 00000000..a4f749d1
--- /dev/null
+++ b/src/components/todoList/buttons/toggleCompleteButton.jsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+// 완료 토글 버튼 컴포넌트: 클릭하여 완료 또는 미완료로 토글
+const ToggleCompleteButton = ({ completed, onToggle }) => {
+ return (
+
+ {completed ? "✅" : "⬜"}
+
+ );
+};
+
+export default ToggleCompleteButton;
diff --git a/src/components/todoList/todoItem.css b/src/components/todoList/todoItem.css
new file mode 100644
index 00000000..b3a1ce4b
--- /dev/null
+++ b/src/components/todoList/todoItem.css
@@ -0,0 +1,55 @@
+/* todoItem.css */
+
+.todo-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 15px;
+ margin-bottom: 10px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ transition: background-color 0.3s ease, box-shadow 0.3s ease;
+ background-color: #fff;
+ }
+
+ .todo-item:first-child {
+ margin-top: 0; /* 첫 번째 아이템의 상단 마진 제거 */
+ }
+
+ .todo-item:last-child {
+ margin-bottom: 0; /* 마지막 아이템의 하단 마진 제거 */
+ }
+
+ .todo-item:hover {
+ background-color: #f9f9f9;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ .toggle-button,
+ .delete-button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 20px;
+ transition: color 0.3s ease, transform 0.3s ease;
+ }
+
+ .toggle-button:hover,
+ .delete-button:hover {
+ color: #b83f45;
+ transform: scale(1.2);
+ }
+
+ .todo-text {
+ flex-grow: 1;
+ margin: 0 10px;
+ font-size: 18px;
+ color: #333;
+ transition: color 0.3s ease, text-decoration 0.3s ease;
+ }
+
+ .todo-text.completed {
+ text-decoration: line-through;
+ color: #aaa;
+ }
+
\ No newline at end of file
diff --git a/src/components/todoList/todoItem.jsx b/src/components/todoList/todoItem.jsx
new file mode 100644
index 00000000..6a01fcb5
--- /dev/null
+++ b/src/components/todoList/todoItem.jsx
@@ -0,0 +1,20 @@
+import React from "react";
+import TodoText from "./todoText";
+import ToggleCompleteButton from "./buttons/toggleCompleteButton";
+import DeleteButton from "./buttons/deleteButton";
+import "./todoItem.css";
+
+const TodoItem = ({ todo, index, toggleComplete, deleteTodo }) => {
+ return (
+
+ toggleComplete(index)}
+ />
+
+ deleteTodo(index)} />
+
+ );
+};
+
+export default TodoItem;
diff --git a/src/components/todoList/todoList.css b/src/components/todoList/todoList.css
new file mode 100644
index 00000000..92aca1f8
--- /dev/null
+++ b/src/components/todoList/todoList.css
@@ -0,0 +1,44 @@
+.todo-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+
+ .todo-item:last-child {
+ margin-bottom: 0;
+ }
+
+ .todo-item:hover {
+ background-color: #f9f9f9;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ .toggle-button,
+ .delete-button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 20px;
+ transition: color 0.3s ease, transform 0.3s ease;
+ }
+
+ .toggle-button:hover,
+ .delete-button:hover {
+ color: #b83f45;
+ transform: scale(1.2);
+ }
+
+ .todo-text {
+ flex-grow: 1;
+ margin: 0 10px;
+ font-size: 18px;
+ color: #333;
+ transition: color 0.3s ease, text-decoration 0.3s ease;
+ }
+
+ .todo-text.completed {
+ text-decoration: line-through;
+ color: #aaa;
+ }
+
\ No newline at end of file
diff --git a/src/components/todoList/todoList.jsx b/src/components/todoList/todoList.jsx
new file mode 100644
index 00000000..2433c405
--- /dev/null
+++ b/src/components/todoList/todoList.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+import TodoItem from "./todoItem";
+import "./todoList.css";
+
+const TodoList = ({ todos, toggleComplete, deleteTodo }) => {
+ return (
+
+ {todos.map((todo, index) => (
+
+ ))}
+
+ );
+};
+
+export default TodoList;
diff --git a/src/components/todoList/todoText.jsx b/src/components/todoList/todoText.jsx
new file mode 100644
index 00000000..c9c75999
--- /dev/null
+++ b/src/components/todoList/todoText.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+
+// 할 일 텍스트 컴포넌트: 완료된 경우에는 텍스트에 줄 긋기
+const TodoText = ({ text, completed }) => {
+ return (
+ {text}
+ );
+};
+
+export default TodoText;
diff --git a/src/handlers/handleAddTodo.js b/src/handlers/handleAddTodo.js
new file mode 100644
index 00000000..7a636929
--- /dev/null
+++ b/src/handlers/handleAddTodo.js
@@ -0,0 +1,8 @@
+// 할 일을 추가하는 함수
+import addTodo from "../utils/addTodo";
+
+const addTodoHandler = (todos, setTodos, todo) => {
+ setTodos((prevTodos) => addTodo(prevTodos, todo));
+};
+
+export default addTodoHandler;
diff --git a/src/handlers/handleDeleteTodo.js b/src/handlers/handleDeleteTodo.js
new file mode 100644
index 00000000..d11e7e8d
--- /dev/null
+++ b/src/handlers/handleDeleteTodo.js
@@ -0,0 +1,8 @@
+// 할 일을 삭제하는 함수
+import deleteTodo from "../utils/deleteTodo";
+
+const deleteTodoHandler = (todos, setTodos, index) => {
+ setTodos((prevTodos) => deleteTodo(prevTodos, index));
+};
+
+export default deleteTodoHandler;
diff --git a/src/handlers/handleInputChange.js b/src/handlers/handleInputChange.js
new file mode 100644
index 00000000..3a92f247
--- /dev/null
+++ b/src/handlers/handleInputChange.js
@@ -0,0 +1,6 @@
+// 입력 값 변경을 처리하는 핸들러 함수
+const handleInputChange = (e, setInputValue) => {
+ setInputValue(e.target.value);
+};
+
+export default handleInputChange;
diff --git a/src/handlers/handleSetFilter.js b/src/handlers/handleSetFilter.js
new file mode 100644
index 00000000..aac37e5c
--- /dev/null
+++ b/src/handlers/handleSetFilter.js
@@ -0,0 +1,6 @@
+// 필터를 설정하는 함수
+const setFilterHandler = (setFilter, filter) => {
+ setFilter(filter);
+};
+
+export default setFilterHandler;
diff --git a/src/handlers/handleSubmit.js b/src/handlers/handleSubmit.js
new file mode 100644
index 00000000..c0e36726
--- /dev/null
+++ b/src/handlers/handleSubmit.js
@@ -0,0 +1,13 @@
+// 이벤트의 기본 동작을 막고, 입력 값이 유효한지 확인한 후 할 일을 추가하고 입력값을 리셋하는 함수
+import preventDefault from "../utils/preventDefault";
+import validateInputValue from "../utils/validateInputValue";
+import addAndResetTodo from "../utils/addAndResetTodo";
+
+const handleFormSubmit = (e, inputValue, addTodo, setInputValue) => {
+ preventDefault(e);
+ if (validateInputValue(inputValue)) {
+ addAndResetTodo(inputValue, addTodo, setInputValue);
+ }
+};
+
+export default handleFormSubmit;
diff --git a/src/handlers/handleToggleComplete.js b/src/handlers/handleToggleComplete.js
new file mode 100644
index 00000000..abeb3841
--- /dev/null
+++ b/src/handlers/handleToggleComplete.js
@@ -0,0 +1,8 @@
+// toggleComplete 유틸리티 함수를 사용하여 할 일의 완료 상태를 토글하는 함수
+import toggleComplete from "../utils/toggleComplete";
+
+const toggleTodoComplete = (todos, setTodos, index) => {
+ setTodos((prevTodos) => toggleComplete(prevTodos, index));
+};
+
+export default toggleTodoComplete;
diff --git a/src/handlers/index.js b/src/handlers/index.js
new file mode 100644
index 00000000..19f90fb5
--- /dev/null
+++ b/src/handlers/index.js
@@ -0,0 +1,4 @@
+export { default as handleAddTodo } from "./handleAddTodo";
+export { default as handleToggleComplete } from "./handleToggleComplete";
+export { default as handleDeleteTodo } from "./handleDeleteTodo";
+export { default as handleSetFilter } from "./handleSetFilter";
diff --git a/src/hooks/filteredTodos/useFilteredTodos.js b/src/hooks/filteredTodos/useFilteredTodos.js
new file mode 100644
index 00000000..9e177c2e
--- /dev/null
+++ b/src/hooks/filteredTodos/useFilteredTodos.js
@@ -0,0 +1,13 @@
+// 필터를 적용하여 할 일 목록을 반환하는 훅
+import { useMemo } from "react";
+import filterTodos from "../../utils/filterTodos";
+
+const useFilteredTodos = (todos, filter) => {
+ const filteredTodos = useMemo(
+ () => filterTodos(todos, filter),
+ [todos, filter]
+ );
+ return filteredTodos;
+};
+
+export default useFilteredTodos;
diff --git a/src/hooks/filteredTodos/useFilteredTodosState.js b/src/hooks/filteredTodos/useFilteredTodosState.js
new file mode 100644
index 00000000..19cf2a0e
--- /dev/null
+++ b/src/hooks/filteredTodos/useFilteredTodosState.js
@@ -0,0 +1,18 @@
+// 필터링된 할 일 목록과 관련된 상태를 관리하는 훅
+import useTodosState from "../todos/useTodosState";
+import useFilteredTodos from "./useFilteredTodos";
+
+const useFilteredTodosState = () => {
+ const { todos, setTodos, filter, setFilter } = useTodosState();
+ const filteredTodos = useFilteredTodos(todos, filter);
+
+ return {
+ todos: filteredTodos,
+ allTodos: todos,
+ setTodos,
+ filter,
+ setFilter,
+ };
+};
+
+export default useFilteredTodosState;
diff --git a/src/hooks/filteredTodos/useTodoStateManager.js b/src/hooks/filteredTodos/useTodoStateManager.js
new file mode 100644
index 00000000..13e18a5d
--- /dev/null
+++ b/src/hooks/filteredTodos/useTodoStateManager.js
@@ -0,0 +1,17 @@
+// 필터링된 할 일 목록과 관련된 상태와 함수를 제공하는 훅
+import useFilteredTodosState from "./useFilteredTodosState";
+
+const useTodoStateManager = () => {
+ const { todos, allTodos, setTodos, filter, setFilter } =
+ useFilteredTodosState();
+
+ return {
+ todos,
+ allTodos,
+ setTodos,
+ filter,
+ setFilter,
+ };
+};
+
+export default useTodoStateManager;
diff --git a/src/hooks/filters/useFilterState.js b/src/hooks/filters/useFilterState.js
new file mode 100644
index 00000000..c2eab5ea
--- /dev/null
+++ b/src/hooks/filters/useFilterState.js
@@ -0,0 +1,14 @@
+// 필터 상태를 관리하는 훅
+import { useState } from "react";
+import FILTERS from "../../utils/filters";
+
+const useFilterState = () => {
+ const [filter, setFilter] = useState(FILTERS.ALL);
+
+ return {
+ filter,
+ setFilter,
+ };
+};
+
+export default useFilterState;
diff --git a/src/hooks/storage/useLoadFilter.js b/src/hooks/storage/useLoadFilter.js
new file mode 100644
index 00000000..e800afcd
--- /dev/null
+++ b/src/hooks/storage/useLoadFilter.js
@@ -0,0 +1,12 @@
+// 로컬 스토리지에서 필터 상태를 불러오는 훅
+import { useEffect } from "react";
+import FILTERS from "../../utils/filters";
+
+const useLoadFilter = (setFilter) => {
+ useEffect(() => {
+ const storedFilter = localStorage.getItem("filter") || FILTERS.ALL;
+ setFilter(storedFilter);
+ }, [setFilter]);
+};
+
+export default useLoadFilter;
diff --git a/src/hooks/storage/useLoadTodos.js b/src/hooks/storage/useLoadTodos.js
new file mode 100644
index 00000000..47a11c61
--- /dev/null
+++ b/src/hooks/storage/useLoadTodos.js
@@ -0,0 +1,11 @@
+// 로컬 스토리지에서 할 일 목록을 불러오는 훅
+import { useEffect } from "react";
+
+const useLoadTodos = (setTodos) => {
+ useEffect(() => {
+ const storedTodos = JSON.parse(localStorage.getItem("todos")) || [];
+ setTodos(storedTodos);
+ }, [setTodos]);
+};
+
+export default useLoadTodos;
diff --git a/src/hooks/storage/useSaveFilter.js b/src/hooks/storage/useSaveFilter.js
new file mode 100644
index 00000000..8c9deb6e
--- /dev/null
+++ b/src/hooks/storage/useSaveFilter.js
@@ -0,0 +1,10 @@
+// 로컬 스토리지에 필터 상태를 저장하는 훅
+import { useEffect } from "react";
+
+const useSaveFilter = (filter) => {
+ useEffect(() => {
+ localStorage.setItem("filter", filter);
+ }, [filter]);
+};
+
+export default useSaveFilter;
diff --git a/src/hooks/storage/useSaveTodos.js b/src/hooks/storage/useSaveTodos.js
new file mode 100644
index 00000000..010ec5a6
--- /dev/null
+++ b/src/hooks/storage/useSaveTodos.js
@@ -0,0 +1,10 @@
+// 로컬 스토리지에 할 일 목록을 저장하는 훅
+import { useEffect } from "react";
+
+const useSaveTodos = (allTodos) => {
+ useEffect(() => {
+ localStorage.setItem("todos", JSON.stringify(allTodos));
+ }, [allTodos]);
+};
+
+export default useSaveTodos;
diff --git a/src/hooks/todos/useTodoList.js b/src/hooks/todos/useTodoList.js
new file mode 100644
index 00000000..8a9e40dc
--- /dev/null
+++ b/src/hooks/todos/useTodoList.js
@@ -0,0 +1,13 @@
+// 할 일 목록 상태를 관리하는 훅
+import { useState } from "react";
+
+const useTodoList = () => {
+ const [todos, setTodos] = useState([]);
+
+ return {
+ todos,
+ setTodos,
+ };
+};
+
+export default useTodoList;
diff --git a/src/hooks/todos/useTodos.js b/src/hooks/todos/useTodos.js
new file mode 100644
index 00000000..44b57d4a
--- /dev/null
+++ b/src/hooks/todos/useTodos.js
@@ -0,0 +1,29 @@
+// 모든 상태와 핸들러를 결합하는 훅
+import useTodoStateManager from "../filteredTodos/useTodoStateManager";
+import useTodoHandlers from "../useTodoHandlers";
+import useLoadTodos from "../storage/useLoadTodos";
+import useLoadFilter from "../storage/useLoadFilter";
+import useSaveTodos from "../storage/useSaveTodos";
+import useSaveFilter from "../storage/useSaveFilter";
+import FILTERS from "../../utils/filters";
+
+const useTodos = () => {
+ const { todos, allTodos, setTodos, filter, setFilter } =
+ useTodoStateManager();
+ const handlers = useTodoHandlers(allTodos, setTodos, setFilter);
+
+ useLoadTodos(setTodos);
+ useLoadFilter(setFilter);
+ useSaveTodos(allTodos);
+ useSaveFilter(filter);
+
+ return {
+ todos,
+ allTodos,
+ filter,
+ FILTERS,
+ ...handlers,
+ };
+};
+
+export default useTodos;
diff --git a/src/hooks/todos/useTodosState.js b/src/hooks/todos/useTodosState.js
new file mode 100644
index 00000000..d268cb08
--- /dev/null
+++ b/src/hooks/todos/useTodosState.js
@@ -0,0 +1,17 @@
+// 할 일 목록과 필터 상태를 관리하는 훅
+import useTodoList from "./useTodoList";
+import useFilterState from "../filters/useFilterState";
+
+const useTodosState = () => {
+ const { todos, setTodos } = useTodoList();
+ const { filter, setFilter } = useFilterState();
+
+ return {
+ todos,
+ setTodos,
+ filter,
+ setFilter,
+ };
+};
+
+export default useTodosState;
diff --git a/src/hooks/useTodoHandlers.js b/src/hooks/useTodoHandlers.js
new file mode 100644
index 00000000..aeb18104
--- /dev/null
+++ b/src/hooks/useTodoHandlers.js
@@ -0,0 +1,16 @@
+// 할 일 관련 핸들러를 관리하는 훅
+import {
+ handleAddTodo,
+ handleToggleComplete,
+ handleDeleteTodo,
+ handleSetFilter,
+} from "../handlers";
+
+const useTodoHandlers = (todos, setTodos, setFilter) => ({
+ handleAddTodo: (todo) => handleAddTodo(todos, setTodos, todo),
+ handleToggleComplete: (index) => handleToggleComplete(todos, setTodos, index),
+ handleDeleteTodo: (index) => handleDeleteTodo(todos, setTodos, index),
+ handleSetFilter: (filter) => handleSetFilter(setFilter, filter),
+});
+
+export default useTodoHandlers;
diff --git a/src/main.js b/src/main.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/utils/activeFilter.js b/src/utils/activeFilter.js
new file mode 100644
index 00000000..f153abdf
--- /dev/null
+++ b/src/utils/activeFilter.js
@@ -0,0 +1,4 @@
+// 할 일 목록 중 완료되지 않은 항목을 필터링하는 함수
+const activeFilter = (todos) => todos.filter((todo) => !todo.completed);
+
+export default activeFilter;
diff --git a/src/utils/addAndResetTodo.js b/src/utils/addAndResetTodo.js
new file mode 100644
index 00000000..905c13d7
--- /dev/null
+++ b/src/utils/addAndResetTodo.js
@@ -0,0 +1,7 @@
+// 새로운 할 일을 추가하고 입력 필드를 초기화하는 함수
+const addAndResetTodo = (inputValue, addTodo, setInputValue) => {
+ addTodo(inputValue);
+ setInputValue("");
+};
+
+export default addAndResetTodo;
diff --git a/src/utils/addTodo.js b/src/utils/addTodo.js
new file mode 100644
index 00000000..15d812ae
--- /dev/null
+++ b/src/utils/addTodo.js
@@ -0,0 +1,7 @@
+// 새로운 할 일을 추가하는 함수
+const addTodo = (todos, newTodo) => [
+ ...todos,
+ { text: newTodo, completed: false },
+];
+
+export default addTodo;
diff --git a/src/utils/completedFilter.js b/src/utils/completedFilter.js
new file mode 100644
index 00000000..d1118aa8
--- /dev/null
+++ b/src/utils/completedFilter.js
@@ -0,0 +1,4 @@
+// 할 일 목록 중 완료된 항목을 필터링하는 함수
+const completedFilter = (todos) => todos.filter((todo) => todo.completed);
+
+export default completedFilter;
diff --git a/src/utils/deleteTodo.js b/src/utils/deleteTodo.js
new file mode 100644
index 00000000..13edda9b
--- /dev/null
+++ b/src/utils/deleteTodo.js
@@ -0,0 +1,4 @@
+// 할 일을 삭제하는 함수
+const deleteTodo = (todos, index) => todos.filter((_, idx) => idx !== index);
+
+export default deleteTodo;
diff --git a/src/utils/filterTodos.js b/src/utils/filterTodos.js
new file mode 100644
index 00000000..4442c744
--- /dev/null
+++ b/src/utils/filterTodos.js
@@ -0,0 +1,17 @@
+// 할 일 목록을 필터링하는 함수
+import FILTERS from "./filters";
+import activeFilter from "./activeFilter";
+import completedFilter from "./completedFilter";
+
+const filterTodos = (todos, filter) => {
+ switch (filter) {
+ case FILTERS.ACTIVE:
+ return activeFilter(todos);
+ case FILTERS.COMPLETED:
+ return completedFilter(todos);
+ default:
+ return todos;
+ }
+};
+
+export default filterTodos;
diff --git a/src/utils/filters.js b/src/utils/filters.js
new file mode 100644
index 00000000..75c6d807
--- /dev/null
+++ b/src/utils/filters.js
@@ -0,0 +1,8 @@
+// 필터 종류를 정의한 객체
+const FILTERS = {
+ ALL: "all",
+ ACTIVE: "active",
+ COMPLETED: "completed",
+};
+
+export default FILTERS;
diff --git a/src/utils/preventDefault.js b/src/utils/preventDefault.js
new file mode 100644
index 00000000..ce5a2fa4
--- /dev/null
+++ b/src/utils/preventDefault.js
@@ -0,0 +1,6 @@
+// 이벤트의 기본 동작을 막는 함수
+const preventDefault = (e) => {
+ e.preventDefault();
+};
+
+export default preventDefault;
diff --git a/src/utils/toggleComplete.js b/src/utils/toggleComplete.js
new file mode 100644
index 00000000..12f32476
--- /dev/null
+++ b/src/utils/toggleComplete.js
@@ -0,0 +1,7 @@
+// 할 일의 완료 상태를 토글하는 함수
+const toggleComplete = (todos, index) =>
+ todos.map((todo, idx) =>
+ idx === index ? { ...todo, completed: !todo.completed } : todo
+ );
+
+export default toggleComplete;
diff --git a/src/utils/validateInputValue.js b/src/utils/validateInputValue.js
new file mode 100644
index 00000000..334d176f
--- /dev/null
+++ b/src/utils/validateInputValue.js
@@ -0,0 +1,12 @@
+// 입력값을 유효성 검사하여 확인합니다.
+import validateTodo from "./validateTodo";
+
+const validateInputValue = (inputValue) => {
+ if (!validateTodo(inputValue)) {
+ alert("할 일을 입력하세요.");
+ return false;
+ }
+ return true;
+};
+
+export default validateInputValue;
diff --git a/src/utils/validateTodo.js b/src/utils/validateTodo.js
new file mode 100644
index 00000000..f4931173
--- /dev/null
+++ b/src/utils/validateTodo.js
@@ -0,0 +1,4 @@
+// 할 일 유효성 검사 함수
+const validateTodo = (todo) => todo.trim() !== "";
+
+export default validateTodo;