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 ( + + ); +}; + +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 ( +
handleSubmit(e, inputValue, addTodo, setInputValue)} + > + + + + ); +}; + +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 ( + + ); +}; + +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 ( + + ); +}; + +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;