diff --git a/README.md b/README.md index 3c0710d2..d8dba8c3 100644 --- a/README.md +++ b/README.md @@ -1 +1,68 @@ -# react-todo-list-precourse \ No newline at end of file +# react-todo-list-precourse + +# Setting +## App.js +- root를 생성해 Main을 로드합니다. + +## Main.jsx +- 생성됨과 동시에 hooks를 로드합니다 by. useTodoState +- UI에 state와 기능들을 전달합니다. + + + + +# UI +## SubmitForm +- user에게 submit을 받는 input을 포함한 UI입니다. +- hooks에서 받아온 onSubmit으로 사용자의 submit을 감지하고 item을 추가해 state를 변경하도록 합니다. + +## TodoList +- user가 입력한 todo를 화면에 그려줍니다. +- 앞의 체크박스를 체크하면 item의 completed 부분이 true로 변하며 state와 localstorage가 변경됩니다. +- hooks에서 받아온 HandleToggleComplete로 이벤트를 감지하여 style도 취소선으로 변경됩니다. + +## Footer +- lists를 확인하고 남은 개수를 보여줍니다. +- hooks에서 받아온 ShowCompletedToggle로 아래의 버튼을 눌렀을 시, condition(all, active, completed)에 따라 해당하는 아이템들만 보여주도록 했습니다. +- Clear Completed 버튼을 눌렀을 시, hooks에서 받아온 ClearCompleted로 state와 localstorage를 변경하였습니다. + + + + +# 기능 +- Hooks를 통해 State를 관리하고 handlers에 부여해주는 방식으로 구현하였습니다. + +## Hooks +### useTodoState +- 전체 아이템들을 관장하는 lists state를 관리합니다. +- item에 랜덤으로 부여되는 NewID를 가지고 있습니다. +- inputRef로 DOM을 관리합니다. +- handlers에게 state나 변수 기능들을 인수로 전달합니다. + +## handlers +### HandleSubmit +- submit시 newTodo를 생성하고 input의 value를 받아 todoObject 형식으로 만들어 state와 localstorage에 전달합니다. +- 이후 SubmitForm을 다시 초기화 합니다. + +### HandleDelete +- 아이템을 지웠을 시, filter를 거쳐 id와 일치하는 아이템을 지우고 state와 localstorage를 업데이트 합니다. + +### HandleToggleComplete +- item 빈배열을 만들고 localstorage의 모든 아이템을 가져와 상태를 확인합니다. +- 클릭된(completed) 아이템만 취소선을 그어줍니다. + +### ShowCompletedToggle +- Footer의 버튼들을 클릭하면 해당하는 condition을 받아옵니다. +- condition(all, active, completed)에 따라 보여줄 아이템들을 필터링합니다. +- 필터링한 아이템을 state와 localstorage에 반영합니다. + +### ClearCompleted +- 완료된 항목들을 필터링합니다. +- 필터링한(clear)된 항목들을 state에 반영하고 localstorage에서 제거합니다. + + + + + +# 느낀점 +- module로 분리해서 작업하다보니 state 관리로 애를 많이 먹었습니다. 다른 쿠키즈들의 코드를 보면서 공부하여 hooks 라는 새로운 방식이 있음을 알게 되고 활용하여 한 곳에서 state와 DOM, 기능들을 관리하는 법을 배운 것 같아 얻어가는게 많은 과제였습니다. diff --git a/index.html b/index.html index b021b5c8..fa9a2743 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,6 @@
- + diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..c744e2bb --- /dev/null +++ b/src/App.css @@ -0,0 +1,21 @@ +* { + box-sizing: border-box; + margin: 0px; + padding: 0px; +} + +.completed{ + text-decoration: line-through; +} + +.container{ + width: 640px; + height: 700px; + display: flex; + flex-direction: column; + position: absolute; + top: 5%; + left: 50%; + /* 화면 끝에서 50% 좌측으로 이동한 것 면적에서 x축기준 -50만큼 이동 */ + transform: translateX(-50%); +} \ No newline at end of file diff --git a/src/App.js b/src/App.js new file mode 100644 index 00000000..bfbdc828 --- /dev/null +++ b/src/App.js @@ -0,0 +1,6 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import Main from "./Main"; + +const root = ReactDOM.createRoot(document.getElementById("app")); +root.render(React.createElement(Main)); diff --git a/src/Main.jsx b/src/Main.jsx new file mode 100644 index 00000000..79e28d2f --- /dev/null +++ b/src/Main.jsx @@ -0,0 +1,26 @@ +import "./App.css"; +import React from "react"; +import useTodoState from "./components/hooks/useTodoState"; +import SubmitForm from "./components/ui/SubmitForm"; +import TodoList from "./components/ui/TodoList"; +import Footer from "./components/ui/Footer"; + +function Main() { + window.addEventListener("load", () => { + localStorage.clear(); + }); + + const { lists, setLists, inputRef, onSubmit, onDelete, onToggle, showConditionToggle, filteredTodoList } = useTodoState(); + + return ( +
+
+ + +
+
+ ); +} + +export default Main; diff --git a/src/components/handlers/ClearCompleted.js b/src/components/handlers/ClearCompleted.js new file mode 100644 index 00000000..143d14e6 --- /dev/null +++ b/src/components/handlers/ClearCompleted.js @@ -0,0 +1,35 @@ +import React from "react"; + +const ClearCompleted = (lists, setLists) => { + setLists((prevLists) => { + // 기존 localStorage 데이터를 불러옵니다. + const items = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + const value = JSON.parse(localStorage.getItem(key)); + items.push({ + id: value.id, + text: value.text, + completed: value.completed, + }); + } + + // 완료된 항목을 필터링합니다. + const completedItems = items.filter((item) => item.completed); + const newStoredLists = items.filter((item) => !item.completed); + + // localStorage에서 완료된 항목을 제거합니다. + completedItems.forEach((item) => { + localStorage.removeItem(item.text); + }); + + // localStorage에 업데이트된 목록을 저장합니다. + newStoredLists.forEach((item) => { + localStorage.setItem(item.text, JSON.stringify(item)); + }); + + return newStoredLists; + }); +}; + +export default ClearCompleted; diff --git a/src/components/handlers/HandleDeleteItem.js b/src/components/handlers/HandleDeleteItem.js new file mode 100644 index 00000000..2528e219 --- /dev/null +++ b/src/components/handlers/HandleDeleteItem.js @@ -0,0 +1,8 @@ +import React from "react"; + +const HandleDeleteItem = (id, lists, setLists) => { + const filteredItems = lists.filter((item) => item.id !== id); + setLists(filteredItems); +}; + +export default HandleDeleteItem; diff --git a/src/components/handlers/HandleSubmit.js b/src/components/handlers/HandleSubmit.js new file mode 100644 index 00000000..18ec7009 --- /dev/null +++ b/src/components/handlers/HandleSubmit.js @@ -0,0 +1,18 @@ +const HandleSubmit = (e, NewID, inputRef, lists, setLists) => { + e.preventDefault(); + + const newTodo = inputRef.current.value; + if (!newTodo.trim()) return; // Avoid empty submissions + + const todoObject = { id: NewID(), text: newTodo, completed: false }; + + localStorage.setItem(newTodo, JSON.stringify(todoObject)); + + const storageItem = localStorage.getItem(newTodo); + const newItem = JSON.parse(storageItem); + + setLists([...lists, newItem]); + inputRef.current.value = ""; +}; + +export default HandleSubmit; diff --git a/src/components/handlers/HandleToggleComplete.js b/src/components/handlers/HandleToggleComplete.js new file mode 100644 index 00000000..1a8dda68 --- /dev/null +++ b/src/components/handlers/HandleToggleComplete.js @@ -0,0 +1,35 @@ +import React from "react"; + +const HandleToggleComplete = (id, lists, setLists) => { + setLists((prevLists) => { + const updatedLists = prevLists.map((item) => + item.id === id ? { ...item, completed: !item.completed } : item + ); + + // 기존 localStorage 데이터를 불러옵니다. + const items = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + const value = JSON.parse(localStorage.getItem(key)); + items.push({ + id: value.id, + text: value.text, + completed: value.completed, + }); + } + + // 업데이트된 항목을 반영합니다. + const newStoredLists = items.map((item) => + item.id === id ? { ...item, completed: !item.completed } : item + ); + + // localStorage에 업데이트된 목록을 저장합니다. + newStoredLists.forEach((item) => { + localStorage.setItem(item.text, JSON.stringify(item)); + }); + + return updatedLists; + }); +}; + +export default HandleToggleComplete; diff --git a/src/components/handlers/ShowCompletedToggle.js b/src/components/handlers/ShowCompletedToggle.js new file mode 100644 index 00000000..bdb5382b --- /dev/null +++ b/src/components/handlers/ShowCompletedToggle.js @@ -0,0 +1,37 @@ +import React from "react"; + +const ShowCompletedToggle = (condition, lists, setLists) => { + //condition따라 toggle filter + // localStorage.getItem() + + setLists((prevLists) => { + // 기존 localStorage 데이터를 불러옵니다. + const items = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + const value = JSON.parse(localStorage.getItem(key)); + items.push({ + id: value.id, + text: value.text, + completed: value.completed, + }); + } + + // 업데이트된 항목을 반영합니다. + const newStoredLists = + condition === "active" + ? items.filter((item) => !item.completed) + : condition === "completed" + ? items.filter((item) => item.completed) + : items; + + // localStorage에 업데이트된 목록을 저장합니다. + newStoredLists.forEach((item) => { + localStorage.setItem(item.text, JSON.stringify(item)); + }); + + return newStoredLists; + }); +}; + +export default ShowCompletedToggle; diff --git a/src/components/hooks/useTodoState.js b/src/components/hooks/useTodoState.js new file mode 100644 index 00000000..d3d58441 --- /dev/null +++ b/src/components/hooks/useTodoState.js @@ -0,0 +1,33 @@ +import { useState, useRef } from "react"; +import HandleSubmit from "../handlers/HandleSubmit"; +import HandleDeleteItem from "../handlers/HandleDeleteItem"; +import HandleToggleComplete from "../handlers/HandleToggleComplete"; +import ClearCompleted from "../handlers/ClearCompleted"; +import ShowCompletedToggle from "../handlers/ShowCompletedToggle"; + +const useTodoState = () => { + const [lists, setLists] = useState([]); + const NewID = () => Math.random().toString(36).substr(2, 16); + const inputRef = useRef(); + + const onSubmit = (e) => HandleSubmit(e, NewID, inputRef, lists, setLists); + const onDelete = (id) => HandleDeleteItem(id, lists, setLists); + const onToggle = (id) => HandleToggleComplete(id, lists, setLists, NewID); + const showConditionToggle = (condition) => + ShowCompletedToggle(condition, lists, setLists); + const filteredTodoList = () => ClearCompleted(lists, setLists); + + return { + lists, + setLists, + NewID, + inputRef, + onSubmit, + onDelete, + onToggle, + filteredTodoList, + showConditionToggle, + }; +}; + +export default useTodoState; diff --git a/src/components/ui/Footer.jsx b/src/components/ui/Footer.jsx new file mode 100644 index 00000000..71624349 --- /dev/null +++ b/src/components/ui/Footer.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import styles from './Footer.module.css'; + +const Footer = ({lists, ShowCompletedToggle, ClearCompleted}) => { + return ( +
  • +
    +

    {lists.length}

    +

    items left

    +
    +
    + + + +
    +
    + +
    +
  • + ); +}; + +export default Footer; diff --git a/src/components/ui/Footer.module.css b/src/components/ui/Footer.module.css new file mode 100644 index 00000000..6a9de11d --- /dev/null +++ b/src/components/ui/Footer.module.css @@ -0,0 +1,60 @@ +.footer{ + width: 540px; + height: 51px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding-left:24px ; + } + + .count{ + display: flex; + flex-direction: row; + font-family: "Josefin Sans"; + color: #9495A5; + font-size: 14px; + } + + .buttons{ + width: 166px; + height: 14px; + display: flex; + flex-direction: row; + justify-content: space-between; + } + + .buttons button{ + font-family: "Josefin Sans"; + font-weight: 700px; + color: #9495A5; + border:0; + background-color: transparent; + cursor: pointer; + } + + .buttons button:hover { + color: #494C6B; + } + + .buttons button:active { + color: #3A7CFD; + } + + .buttons button:focus { + color: #3A7CFD; + } + + .clear button{ + border: 0; + background-color: transparent; + cursor: pointer; + margin-right: 24px; + font-family: "Josefin Sans"; + font-weight: 350; + } + + .clear button:hover{ + color: #494C6B; + } + \ No newline at end of file diff --git a/src/components/ui/SubmitForm.jsx b/src/components/ui/SubmitForm.jsx new file mode 100644 index 00000000..4dd84b13 --- /dev/null +++ b/src/components/ui/SubmitForm.jsx @@ -0,0 +1,29 @@ +import React, { useRef } from "react"; +import styles from "./SubmitForm.module.css"; + +const SubmitForm = ({e, onSubmit, inputRef, lists, setLists}) => { + + return ( +
    +
    +
    + + +
    + +
    +
    + ); +}; + +export default SubmitForm; diff --git a/src/components/ui/SubmitForm.module.css b/src/components/ui/SubmitForm.module.css new file mode 100644 index 00000000..b63bfb76 --- /dev/null +++ b/src/components/ui/SubmitForm.module.css @@ -0,0 +1,55 @@ +.todoCheck { + display: none; +} +.checkboxLabel { + width: 24px; + height: 24px; + background-color: #fff; /* 체크박스의 배경색 */ + border: 1px solid #e3e4f1; /* 체크박스의 테두리 스타일 */ + border-radius: 100%; + display: flex; + justify-content: center; + align-items: center; +} +.todoCheck:checked + .checkboxLabel { + background-size: cover; +} +.todoInputWrapper { + display: flex; + align-items: center; /* 수직 정렬 추가 */ + gap: 24px; /* 요소들 사이의 간격 조정 */ + width: 540px; + height: 64px; + border-radius: 5px; + background: #fff; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.5); + margin-top: 40px; + padding: 20px; +} + +.todoInput { + width: calc(100% - 24px); /* 텍스트 입력 필드의 너비 조정 */ + height: 100%; + border: none; + font-family: "Josefin Sans"; + font-size: 18px; + color: #494c6b; + font-weight: 400; + letter-spacing: -0.25px; + outline: none; +} + +.todoInputItem { + display: flex; + flex-direction: row; + align-items: center; + gap: 24px; +} + +.todoInput::placeholder { + font-family: "Josefin Sans"; + color: #9495a5; + font-size: 18px; + font-weight: 400; + letter-spacing: -0.25px; +} diff --git a/src/components/ui/TodoList.jsx b/src/components/ui/TodoList.jsx new file mode 100644 index 00000000..037cb870 --- /dev/null +++ b/src/components/ui/TodoList.jsx @@ -0,0 +1,32 @@ +import React from "react"; +import styles from "./TodoList.module.css"; +import HandleDeleteItem from "../handlers/HandleDeleteItem"; +import HandleToggleComplete from "../handlers/HandleToggleComplete"; + +const TodoList = ({ lists, HandleToggleComplete, HandleDeleteItem}) => { + return ( + + ); +}; + +export default TodoList; diff --git a/src/components/ui/TodoList.module.css b/src/components/ui/TodoList.module.css new file mode 100644 index 00000000..ef9b1293 --- /dev/null +++ b/src/components/ui/TodoList.module.css @@ -0,0 +1,47 @@ +.todoCheck { + display: none; +} +.checkboxLabel { + width: 24px; + height: 24px; + background-color: #fff; /* 체크박스의 배경색 */ + border: 1px solid #e3e4f1; /* 체크박스의 테두리 스타일 */ + border-radius: 100%; + display: flex; + justify-content: center; + align-items: center; +} +.todoCheck:checked + .checkboxLabel { + background-size: cover; +} +.todoList { + width: 100%; + height: auto; + border-radius: 5px; + background: #fff; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.5); + list-style-type: none; + padding: 0; +} +.todoItem { + color: #494c6b; + font-family: "Josefin Sans"; + font-size: 18px; + width: 100%; + height: 24px; + display: flex; + align-items: center; + gap: 26px; + padding: 25px; + border-bottom: 1px solid #e3e4f1; +} +.checkboxLabel { + width: 24px; + height: 24px; + background-color: #fff; + border: 1px solid #e3e4f1; + border-radius: 50%; /* 100%에서 50%로 수정 */ + display: flex; + justify-content: center; + align-items: center; +}