diff --git a/.gitignore b/.gitignore
index a547bf36..a414912e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ dist-ssr
*.local
# Editor directories and files
+.prettierrc
.vscode/*
!.vscode/extensions.json
.idea
diff --git a/README.md b/README.md
index 3c0710d2..19c92b2c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,14 @@
-# react-todo-list-precourse
\ No newline at end of file
+# react-todo-list-precourse \_ 전남대 FE 박건규
+
+## 기능 요구사항
+
+할 일 목록을 업데이트 하는 할 일 목록을 구현한다.
+
+- 할 일을 추가하고 삭제할 수 있다.
+ - 추가할 떄 사용자는 enter 키나 추가 버튼을 사용하여 목록에 추가할 수 있어야 한다.
+ - 아무것도 입력하지 않은 경우 할 일을 추가할 수 없다.
+- 할 일의 목록을 볼 수 있다.
+- 할 일의 완료 상태를 전환할 수 있다.
+- 현재 진행중인 할 일, 완료된 할 일, 모든 할 일을 필터링 할 수 있다.
+- 해야할 일의 총 개수를 확인할 수 있다.
+- 새로고침을 하여도 이전에 작성한 데이터는 유지되어야 한다.
diff --git a/index.html b/index.html
index b021b5c8..e479bdc1 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,16 @@
-
-
+
+
-
+
+ TODO
-
+
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 00000000..1e61c23e
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,5 @@
+#App > h1 {
+ font-size: 3rem;
+ text-align: center;
+ margin-bottom: 1rem;
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 00000000..ce50f104
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,42 @@
+import InputForm from "./components/InputForm/InputForm";
+import { FilterStateType, Todo } from "./Model/Todo";
+import TodoLists from "./components/TodoList/TodoLists";
+import "./App.css";
+import TodoFilter from "./components/TodoFilter/TodoFilter";
+import useTodos from "./hooks/useTodos";
+
+const App = () => {
+ const {
+ todos,
+ leaseTodos,
+ filterState,
+ filteredTodos,
+ setTodos,
+ setFilterState,
+ setIsCompletedFromId,
+ deleteTodoFromId,
+ } = useTodos();
+
+ return (
+
+
todos
+ setTodos((prev) => [...prev, newTodo])}
+ />
+
+ {todos.length !== 0 && (
+ setFilterState(state)}
+ />
+ )}
+
+ );
+};
+
+export default App;
diff --git a/src/Model/Todo.ts b/src/Model/Todo.ts
new file mode 100644
index 00000000..3a391609
--- /dev/null
+++ b/src/Model/Todo.ts
@@ -0,0 +1,7 @@
+export interface Todo {
+ id: number;
+ text: string;
+ isCompleted: boolean;
+}
+
+export type FilterStateType = "all" | "active" | "completed";
diff --git a/src/components/InputForm/InputForm.css b/src/components/InputForm/InputForm.css
new file mode 100644
index 00000000..25dfc71c
--- /dev/null
+++ b/src/components/InputForm/InputForm.css
@@ -0,0 +1,9 @@
+#InputForm {
+ display: flex;
+}
+#InputForm > input {
+ flex: 1;
+ font-size: 1.5rem;
+ padding-block: 1rem;
+ padding-inline: 3rem;
+}
diff --git a/src/components/InputForm/InputForm.tsx b/src/components/InputForm/InputForm.tsx
new file mode 100644
index 00000000..493eafb2
--- /dev/null
+++ b/src/components/InputForm/InputForm.tsx
@@ -0,0 +1,33 @@
+import { useState } from "react";
+import { Todo } from "../../Model/Todo";
+import "./InputForm.css";
+
+interface InputFormProps {
+ setTodos: (newTodo: Todo) => void;
+}
+
+const InputForm = ({ setTodos }: InputFormProps) => {
+ const [text, setText] = useState("");
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!text.trim()) return;
+ const id = new Date().getTime();
+ const newTodo: Todo = { id, isCompleted: false, text };
+ setTodos(newTodo);
+ setText("");
+ };
+
+ return (
+
+ );
+};
+
+export default InputForm;
diff --git a/src/components/TodoFilter/TodoFilter.css b/src/components/TodoFilter/TodoFilter.css
new file mode 100644
index 00000000..0f0f743e
--- /dev/null
+++ b/src/components/TodoFilter/TodoFilter.css
@@ -0,0 +1,11 @@
+#TodoFilter {
+ display: flex;
+ justify-content: space-between;
+ margin: 20px 0;
+}
+
+#TodoFilter > ul {
+ display: flex;
+ gap: 1rem;
+ padding: 0;
+}
diff --git a/src/components/TodoFilter/TodoFilter.tsx b/src/components/TodoFilter/TodoFilter.tsx
new file mode 100644
index 00000000..9f28bb33
--- /dev/null
+++ b/src/components/TodoFilter/TodoFilter.tsx
@@ -0,0 +1,45 @@
+import { useEffect, useState } from "react";
+import { FilterStateType } from "../../Model/Todo";
+import "./TodoFilter.css";
+
+interface TodoFilterProps {
+ leaseTodos: number;
+ filterState: FilterStateType;
+ setFilterState: (state: FilterStateType) => void;
+}
+
+const FilterState: FilterStateType[] = ["all", "active", "completed"];
+
+const TodoFilter = ({
+ leaseTodos,
+ filterState,
+ setFilterState,
+}: TodoFilterProps) => {
+ const [filter, setFilter] = useState(filterState);
+
+ useEffect(() => {
+ setFilter(filterState);
+ }, [filterState]);
+
+ return (
+
+
{leaseTodos} items left!
+
+ {FilterState.map((state, i) => (
+
+ setFilterState(state)}
+ style={{
+ backgroundColor: (filter === state && "gray") || "white",
+ }}
+ >
+ {state}
+
+
+ ))}
+
+
+ );
+};
+
+export default TodoFilter;
diff --git a/src/components/TodoList/TodoLists.tsx b/src/components/TodoList/TodoLists.tsx
new file mode 100644
index 00000000..f89bbbc6
--- /dev/null
+++ b/src/components/TodoList/TodoLists.tsx
@@ -0,0 +1,31 @@
+import { Todo } from "../../Model/Todo";
+import TodoListItem from "../TodoListItem/TodoListItem";
+
+interface TodoListProps {
+ todos: Todo[];
+ setIsCompleted: (index: number) => void;
+ deleteTodoFromId: (index: number) => void;
+}
+
+const TodoLists = ({
+ todos,
+ setIsCompleted,
+ deleteTodoFromId,
+}: TodoListProps) => {
+ return (
+
+ {todos.map(({ isCompleted, text, id }, i) => (
+
+ ))}
+
+ );
+};
+
+export default TodoLists;
diff --git a/src/components/TodoListItem/TodoListItem.css b/src/components/TodoListItem/TodoListItem.css
new file mode 100644
index 00000000..4e11a67d
--- /dev/null
+++ b/src/components/TodoListItem/TodoListItem.css
@@ -0,0 +1,40 @@
+#TodoListItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-inline: 10px;
+ border-bottom: 1px solid #ccc;
+}
+
+#TodoListItem:hover {
+ background-color: #f9f9f9;
+}
+
+#TodoListItem > button {
+ outline: none;
+ border: none;
+ color: inherit;
+ background-color: transparent;
+}
+
+#TodoListItem-content {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: start;
+ align-items: center;
+ padding: 1rem;
+ border: 1px solid black;
+ background-color: black;
+ font-size: 1.2rem;
+ gap: 1rem;
+}
+
+#TodoListItem-delete:hover {
+ color: red;
+}
+
+.completed {
+ text-decoration: line-through;
+ color: #ccc;
+}
diff --git a/src/components/TodoListItem/TodoListItem.tsx b/src/components/TodoListItem/TodoListItem.tsx
new file mode 100644
index 00000000..c22a5405
--- /dev/null
+++ b/src/components/TodoListItem/TodoListItem.tsx
@@ -0,0 +1,48 @@
+import { useState } from "react";
+import "./TodoListItem.css";
+
+interface TodoListItemProps {
+ id: number;
+ text: string;
+ isCompleted: boolean;
+ setIsCompleted: (id: number) => void;
+ deleteTodoFromId: (id: number) => void;
+}
+
+const TodoListItem = ({
+ id,
+ text,
+ isCompleted,
+ setIsCompleted,
+ deleteTodoFromId,
+}: TodoListItemProps) => {
+ const [isMouseEnter, setIsMouseEnter] = useState(false);
+
+ const handleIsCompleted = (id: number) => {
+ setIsCompleted(id);
+ };
+
+ return (
+ setIsMouseEnter(true)}
+ onMouseLeave={() => setIsMouseEnter(false)}
+ >
+ handleIsCompleted(id)}
+ >
+
+ {text}
+
+ {isMouseEnter && (
+ deleteTodoFromId(id)}>
+ x
+
+ )}
+
+ );
+};
+
+export default TodoListItem;
diff --git a/src/hooks/useTodos.tsx b/src/hooks/useTodos.tsx
new file mode 100644
index 00000000..81d8bf70
--- /dev/null
+++ b/src/hooks/useTodos.tsx
@@ -0,0 +1,49 @@
+import { useEffect, useState } from "react";
+import { FilterStateType, Todo } from "../Model/Todo";
+import { filterTodos } from "../utils/todo";
+import {
+ getTodosFromLocalStorage,
+ setTodosToLocalStorage,
+} from "../utils/localstorage";
+
+const useTodos = () => {
+ const [todos, setTodos] = useState([]);
+ const [filterState, setFilterState] = useState("all");
+ const filteredTodos = filterTodos(todos, filterState);
+ const leaseTodos = filteredTodos.length;
+
+ useEffect(() => {
+ const savedTodos = getTodosFromLocalStorage();
+ if (savedTodos) setTodos(savedTodos);
+ }, []);
+
+ useEffect(() => {
+ if (todos.length === 0) return;
+ setTodosToLocalStorage(todos);
+ }, [todos]);
+
+ const setIsCompletedFromId = (id: number) => {
+ setTodos((prev) =>
+ prev.map((todo) => {
+ if (todo.id === id) return { ...todo, isCompleted: !todo.isCompleted };
+ return todo;
+ })
+ );
+ };
+
+ const deleteTodoFromId = (id: number) => {
+ setTodos((prev) => prev.filter((todo) => todo.id !== id));
+ };
+ return {
+ todos,
+ setTodos,
+ filterState,
+ setFilterState,
+ filteredTodos,
+ leaseTodos,
+ setIsCompletedFromId,
+ deleteTodoFromId,
+ };
+};
+
+export default useTodos;
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 00000000..9010ce9b
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,10 @@
+body {
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ background-color: whitesmoke;
+}
+#app {
+ max-width: 35rem;
+ margin-inline: auto;
+ background-color: white;
+}
diff --git a/src/main.js b/src/main.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 00000000..028a17c6
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,12 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App";
+import "./index.css";
+
+const root = document.getElementById("app")!;
+
+createRoot(root).render(
+
+
+
+);
diff --git a/src/utils/localstorage.ts b/src/utils/localstorage.ts
new file mode 100644
index 00000000..b2246e18
--- /dev/null
+++ b/src/utils/localstorage.ts
@@ -0,0 +1,10 @@
+import { Todo } from "../Model/Todo";
+
+export const setTodosToLocalStorage = (todos: Todo[]) => {
+ localStorage.setItem("todos", JSON.stringify(todos));
+};
+
+export const getTodosFromLocalStorage = (): Todo[] => {
+ const todos = localStorage.getItem("todos");
+ return todos ? JSON.parse(todos) : [];
+};
diff --git a/src/utils/todo.ts b/src/utils/todo.ts
new file mode 100644
index 00000000..f052118c
--- /dev/null
+++ b/src/utils/todo.ts
@@ -0,0 +1,10 @@
+import { FilterStateType, Todo } from "../Model/Todo";
+
+export const filterTodos = (todos: Todo[], filterState: FilterStateType) => {
+ return todos.filter((todo) => {
+ if (filterState === "all") return true;
+ if (filterState === "active") return !todo.isCompleted;
+ if (filterState === "completed") return todo.isCompleted;
+ return false;
+ });
+};