diff --git a/README.md b/README.md
index 3c0710d2..ffca5a25 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,11 @@
-# react-todo-list-precourse
\ No newline at end of file
+# react-todo-list-precourse
+
+1. 할 일 추가 및 삭제 (Enter키나 추가 버튼)
+2. todos 관리를 위한 util 함수 작성(추가, 삭제, 체크, 필터링)
+3. todoList 구성
+4. 할 일 완료 checkbox 구현
+5. 현재 진행 중인 할 일, 완료된 할 일, 모든 할 일 필터링 기능
+6. footer 구현
+7. 디자인 변경
+8. 새로고침 시 작성한 데이터 유지
+9. 컴포넌트 리팩토링 및 코드 스타일 수정 (하나의 기능, 15줄 넘지 않게 작성)
diff --git a/index.html b/index.html
index b021b5c8..76aa23f7 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,16 @@
-
+
-
+
+ Hyoeun TodoList
-
-
+
+
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..c5f7e962
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,6 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import Main from "./main";
+
+const root = createRoot(document.getElementById("root"));
+root.render(React.createElement(Main));
\ No newline at end of file
diff --git a/src/components/form/CheckBox.jsx b/src/components/form/CheckBox.jsx
new file mode 100644
index 00000000..c8da64e3
--- /dev/null
+++ b/src/components/form/CheckBox.jsx
@@ -0,0 +1,18 @@
+import { checkTodo } from "../../util/todoUtilFunc";
+import "../../style/CheckBox.css";
+const CheckBox = ({ className, checked, handleCheck }) => {
+ return (
+
+
+
+ {checked ? "check_box" : "check_box_outline_blank"}
+
+
+ );
+};
+export default CheckBox;
diff --git a/src/components/form/Filter.jsx b/src/components/form/Filter.jsx
new file mode 100644
index 00000000..e6f2988c
--- /dev/null
+++ b/src/components/form/Filter.jsx
@@ -0,0 +1,13 @@
+const Filter = ({ text, filter, setFilter }) => {
+ return (
+ setFilter(text)}
+ style={{
+ borderColor: filter === text ? "rgb(56, 56, 180)" : null,
+ }}
+ >
+ {text}
+
+ );
+};
+export default Filter;
diff --git a/src/components/form/FilterContainer.jsx b/src/components/form/FilterContainer.jsx
new file mode 100644
index 00000000..a100b058
--- /dev/null
+++ b/src/components/form/FilterContainer.jsx
@@ -0,0 +1,13 @@
+import Filter from "./Filter";
+import "../../style/Filter.css";
+
+const FilterContainer = ({ filter, setFilter }) => {
+ return (
+
+ );
+};
+export default FilterContainer;
diff --git a/src/components/form/ListBottom.jsx b/src/components/form/ListBottom.jsx
new file mode 100644
index 00000000..6fe85b88
--- /dev/null
+++ b/src/components/form/ListBottom.jsx
@@ -0,0 +1,15 @@
+import FilterContainer from "./FilterContainer";
+import { activeNum, handleClear } from "../../util/todoUtilFunc";
+import "../../style/ListBottom.css";
+const ListBottom = ({ todos, setTodos, filter, setFilter }) => {
+ return (
+
+ {activeNum(todos)}개 남음!
+
+ handleClear(todos, setTodos)}>
+ Clear completed
+
+
+ );
+};
+export default ListBottom;
diff --git a/src/components/form/ListItem.jsx b/src/components/form/ListItem.jsx
new file mode 100644
index 00000000..83a1436f
--- /dev/null
+++ b/src/components/form/ListItem.jsx
@@ -0,0 +1,19 @@
+import React from "react";
+import CheckBox from "./CheckBox";
+import { checkTodo, deleteTodo } from "../../util/todoUtilFunc";
+import "../../style/ListItem.css";
+
+const ListItem = ({ todos, setTodos, todo }) => {
+ return (
+
+ checkTodo(todos, setTodos, todo.id)}
+ />
+ {todo.text}
+ deleteTodo(todos, setTodos, todo.id)}>x
+
+ );
+};
+export default ListItem;
diff --git a/src/components/main/Footer.jsx b/src/components/main/Footer.jsx
new file mode 100644
index 00000000..2cf4abe7
--- /dev/null
+++ b/src/components/main/Footer.jsx
@@ -0,0 +1,13 @@
+import "../../style/Footer.css";
+
+const Footer = () => {
+ return (
+
+
Double-click edit a todo
+
Creted by Hyoeun Kookie
+
Part of KakaoTechCampus
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/main/Input.jsx b/src/components/main/Input.jsx
new file mode 100644
index 00000000..4db9c91f
--- /dev/null
+++ b/src/components/main/Input.jsx
@@ -0,0 +1,22 @@
+import React, { useRef, useState } from "react";
+import { useInputTodo } from "../../hooks/useInputTodo";
+import "../../style/Input.css";
+
+const Input = ({ todos, setTodos }) => {
+ const { handleText, text, onChange } = useInputTodo({ todos, setTodos });
+ const inputRef = useRef();
+ return (
+
+ {
+ if (e.key === "Enter") handleText(inputRef.current.value);
+ }}
+ />
+ handleText(inputRef.current.value)}>+
+
+ );
+};
+
+export default Input;
diff --git a/src/components/main/Todo.jsx b/src/components/main/Todo.jsx
new file mode 100644
index 00000000..d24ebb8c
--- /dev/null
+++ b/src/components/main/Todo.jsx
@@ -0,0 +1,24 @@
+import { useState } from "react";
+import { allCheckTodo, filterTodos } from "../../util/todoUtilFunc";
+import CheckBox from "../form/CheckBox";
+import ListItem from "../form/ListItem";
+import ListBottom from "../form/ListBottom";
+import "../../style/Todo.css";
+
+export default function Todo({ todos, setTodos }) {
+ const [filter, setFilter] = useState("All");
+ const filtered = filterTodos(todos, filter);
+ if (todos.length === 0) return null;
+ return (
+
+
todo.done)}
+ handleCheck={(e) => allCheckTodo(todos, setTodos, e.target.checked)}
+ />
+
+ {filtered.map((todo) => ( ))}
+
+
+
+ );
+}
diff --git a/src/hooks/useInputTodo.jsx b/src/hooks/useInputTodo.jsx
new file mode 100644
index 00000000..06e92650
--- /dev/null
+++ b/src/hooks/useInputTodo.jsx
@@ -0,0 +1,20 @@
+import { useState, useCallback, useEffect } from "react";
+import { addTodo } from "../util/todoUtilFunc";
+
+export const useInputTodo = ({ todos, setTodos }) => {
+ const [text, setText] = useState("");
+
+ const handleText = useCallback(
+ (text) => {
+ if (text === "") return;
+ addTodo(todos, setTodos, text);
+ setText("");
+ },
+ [todos, setTodos]
+ );
+ const onChange = (e) => {
+ setText(e.target.value);
+ };
+
+ return { handleText, onChange, text };
+};
diff --git a/src/main.js b/src/main.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 00000000..8a80a1a3
--- /dev/null
+++ b/src/main.jsx
@@ -0,0 +1,22 @@
+import { useEffect, useState } from "react";
+import Input from "./components/main/Input";
+import Todo from "./components/main/Todo";
+import Footer from "./components/main/Footer";
+import "./style/Global.css";
+
+export default function Main() {
+ const [todos, setTodos] = useState(JSON.parse(localStorage.getItem("todos")));
+ useEffect(() => {
+ localStorage.setItem("todos", JSON.stringify(todos));
+ }, [todos]);
+ return (
+ <>
+
+
Hyo's Todo
+
+
+
+
+ >
+ );
+}
diff --git a/src/style/CheckBox.css b/src/style/CheckBox.css
new file mode 100644
index 00000000..5148240c
--- /dev/null
+++ b/src/style/CheckBox.css
@@ -0,0 +1,17 @@
+.allCheck {
+ position: absolute;
+ top: -57px;
+ left: 68px;
+}
+input[type="checkbox"] {
+ display: none;
+}
+input[type="checkbox"] + label {
+ display: block;
+ cursor: pointer;
+ color: grey;
+}
+input[type="checkbox"]:checked + label {
+ display: block;
+ color: rgb(77, 77, 202);
+}
diff --git a/src/style/Filter.css b/src/style/Filter.css
new file mode 100644
index 00000000..7eaed12f
--- /dev/null
+++ b/src/style/Filter.css
@@ -0,0 +1,14 @@
+.filter-container {
+ display: flex;
+ list-style: none;
+ text-decoration: none;
+ align-items: center;
+ padding: 0;
+ gap: 10px;
+}
+.filter-container > li {
+ padding: 0px 4px;
+ border: 2px solid rgb(250, 250, 255);
+ border-radius: 4px;
+ cursor: pointer;
+}
diff --git a/src/style/Footer.css b/src/style/Footer.css
new file mode 100644
index 00000000..da580aea
--- /dev/null
+++ b/src/style/Footer.css
@@ -0,0 +1,9 @@
+.footer {
+ margin: 50px 0px;
+}
+.footer > p {
+ font-size: 12px;
+ text-align: center;
+ color: rgb(220, 220, 220);
+ line-height: 8px;
+}
diff --git a/src/style/Global.css b/src/style/Global.css
new file mode 100644
index 00000000..242860c2
--- /dev/null
+++ b/src/style/Global.css
@@ -0,0 +1,22 @@
+body {
+ background-color: rgb(149, 149, 173);
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+.todo {
+ width: min(70%, 600px);
+ margin: 20px auto;
+ padding: 10px 0px;
+
+ text-align: center;
+
+ background-color: rgb(250, 250, 255);
+ border-radius: 5px;
+ box-shadow: 0px 0px 50px 0px rgba(0, 0, 0, 0.2);
+}
+.todo > h1 {
+ color: rgb(56, 56, 180);
+ text-shadow: 1px 1px 4px rgb(157, 157, 181);
+}
diff --git a/src/style/Input.css b/src/style/Input.css
new file mode 100644
index 00000000..306cde14
--- /dev/null
+++ b/src/style/Input.css
@@ -0,0 +1,31 @@
+.input-container {
+ display: flex;
+ margin: 20px auto;
+ width: 80%;
+ box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
+ justify-content: space-between;
+}
+.input-container > input:focus {
+ outline: 2px solid rgb(56, 56, 180);
+ outline-offset: -2px;
+}
+.input-container > input {
+ height: 50px;
+ width: 85%;
+ border: none;
+ padding: 0;
+ padding-left: 50px;
+ box-shadow: 0 -2px 1px rgba(0, 0, 0, 0.03);
+}
+
+.input-container > button {
+ width: 15%;
+ font-size: 20px;
+ border: none;
+ cursor: pointer;
+ background-color: rgb(214, 214, 214);
+}
+
+.input-container > button:hover {
+ border: 2px solid rgb(56, 56, 180);
+}
diff --git a/src/style/ListBottom.css b/src/style/ListBottom.css
new file mode 100644
index 00000000..e545fe29
--- /dev/null
+++ b/src/style/ListBottom.css
@@ -0,0 +1,17 @@
+.list-bottom {
+ width: 80%;
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+}
+
+.list-bottom > button {
+ border: none;
+ height: 20px;
+ background: none;
+}
+.list-bottom > button:hover {
+ text-decoration: underline;
+}
diff --git a/src/style/ListItem.css b/src/style/ListItem.css
new file mode 100644
index 00000000..84c391f8
--- /dev/null
+++ b/src/style/ListItem.css
@@ -0,0 +1,30 @@
+.list-container {
+ display: flex;
+ position: relative;
+ gap: 18px;
+ align-items: center;
+ font-size: 18px;
+ padding: 8px 0px 8px 8px;
+ border-bottom: 1px solid rgb(228, 225, 225);
+}
+.list-container > button {
+ display: none;
+ position: absolute;
+ right: 10px;
+ background: none;
+ border: none;
+ font-size: 16px;
+ color: rgb(56, 56, 180);
+}
+.list-container:hover > button {
+ cursor: pointer;
+ display: block;
+}
+.list-container > span {
+ color: black;
+ text-decoration: none;
+}
+.list-container > .done {
+ color: grey;
+ text-decoration: line-through;
+}
diff --git a/src/style/Todo.css b/src/style/Todo.css
new file mode 100644
index 00000000..a6a1e037
--- /dev/null
+++ b/src/style/Todo.css
@@ -0,0 +1,8 @@
+.todo-container {
+ position: relative;
+}
+.todo-wrapper {
+ width: 80%;
+ padding: 2px 0px;
+ margin: 20px auto;
+}
diff --git a/src/util/todoUtilFunc.jsx b/src/util/todoUtilFunc.jsx
new file mode 100644
index 00000000..73b6e8b9
--- /dev/null
+++ b/src/util/todoUtilFunc.jsx
@@ -0,0 +1,35 @@
+export function addTodo(todos, setTodos, todo) {
+ setTodos([...todos, { id: Date.now(), text: todo, done: false }]);
+}
+
+export function deleteTodo(todos, setTodos, id) {
+ setTodos(todos.filter((todo) => todo.id !== id));
+}
+
+export function checkTodo(todos, setTodos, id) {
+ setTodos(
+ todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo))
+ );
+}
+
+export function allCheckTodo(todos, setTodos, done) {
+ setTodos(
+ todos.map((todo) => (todo.done !== done ? { ...todo, done: done } : todo))
+ );
+}
+
+export const activeNum = (todos) => {
+ return todos.filter((todo) => !todo.done).length;
+};
+
+export function filterTodos(todos, filter) {
+ return todos.filter((todo) => {
+ if (filter === "Active") return !todo.done;
+ if (filter === "Completed") return todo.done;
+ return true;
+ });
+}
+
+export function handleClear(todos, setTodos) {
+ setTodos(todos.filter((todo) => !todo.done));
+}