diff --git a/README.md b/README.md index 3c0710d2..21e352d1 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# react-todo-list-precourse \ No newline at end of file +# react-todo-list-precourse + +## 기능 요구 사항 + +하루 또는 한 주의 할 일 목록을 업데이트하는 할 일 목록을 구현한다. **React 라이브러리를 사용하여 웹 앱으로 구현한다.** + +- 할 일을 추가하고 삭제할 수 있다. + - 할 일을 추가할 때 사용자는 Enter 키나 추가 버튼을 사용하여 할 일을 목록에 추가할 수 있어야 한다. + - 사용자가 아무것도 입력하지 않은 경우에는 할 일을 추가할 수 없다. +- 할 일의 목록을 볼 수 있다. +- 할 일의 완료 상태를 전환할 수 있다. + +### 선택 요구 사항 + +- 현재 진행 중인 할 일, 완료된 할 일, 모든 할 일을 필터링할 수 있다. +- 해야 할 일의 총개수를 확인할 수 있다. +- 새로고침을 하여도 이전에 작성한 데이터는 유지되어야 한다. + +### 피드백 + +- 키보드로 할일을 입력할 때 이벤트가 2번 실행됩니다. 이는 한글과 같은 문자를 입력할 때 키보드 이벤트를 사용하는 경우 발생하는 문제입니다. -> react isComposing +- 완료한 할일의 삭제 버튼에 선 없애기 +- 함수 컴포넌트를 포함하여 함수는 15줄 이내로 작성 +- 컴포넌트와 유틸함수 분리 +- depth는 3 이내로 +- css 변경은 design을 사용할 것 diff --git a/index.html b/index.html index b021b5c8..fdc8a04b 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,12 @@ - + - + Todos📋
- + diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..5b841cae --- /dev/null +++ b/src/App.css @@ -0,0 +1,131 @@ +#root { + font-family: Arial, sans-serif; + background-color: #f0f0f0; + margin: 0; + padding: 0; + display: flex; + justify-content: flex-start; + align-items: center; + height: 100vh; +} + +.container { + background-color: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + width: 300px; + position: fixed; + top: 40px; + left: 50%; + transform: translateX(-50%); +} + +h1 { + text-align: center; + color: #333; +} + +.todo-input { + display: flex; + justify-content: space-between; +} + +.todo-input input { + flex: 1; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px 0 0 5px; + outline: none; +} + +.todo-input button { + padding: 10px; + border: none; + background-color: #28a745; + color: white; + border-radius: 0 5px 5px 0; + cursor: pointer; +} + +.todo-input button:hover { + background-color: #218838; +} + +ul { + list-style: none; + padding: 0; + margin-top: 20px; + max-height: 60vh; + overflow-y: scroll; +} + +li { + background-color: #fff; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +li span { + flex-grow: 1; + margin-right: 10px; +} + +li input[type="checkbox"] { + margin-right: 10px; +} + +.completed span { + text-decoration: line-through; + color: #888; +} + +.completed input[type="checkbox"] { + pointer-events: none; +} + +li button { + background-color: #dc3545; + border: none; + color: white; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; +} + +li button:hover { + background-color: #c82333; +} + +.filters { + display: flex; + justify-content: center; + margin-top: 20px; +} + +.filters button { + padding: 5px 10px; + border: 1px solid #ddd; + background-color: #fff; + cursor: pointer; +} + +.filters button:not(:last-child) { + border-right: none; +} + +.filters button.active { + background-color: #ddd; +} + +.todo-footer { + text-align: left; + margin-top: 20px; + font-size: 14px; + color: #555; +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 00000000..07d8d7ac --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,95 @@ +import React, { useState } from "react"; +import "./App.css"; + +function App() { + const [todos, setTodos] = useState([]); + const [newTodo, setNewTodo] = useState(""); + const [filter, setFilter] = useState("all"); // 'all', 'active', 'completed' + + const addTodo = () => { + if (newTodo.trim() === "") return; + setTodos([...todos, { text: newTodo, completed: false }]); + setNewTodo(""); + }; + + const toggleTodo = (index) => { + const newTodos = [...todos]; + newTodos[index].completed = !newTodos[index].completed; + setTodos(newTodos); + }; + + const deleteTodo = (index) => { + const newTodos = [...todos]; + newTodos.splice(index, 1); + setTodos(newTodos); + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + addTodo(); + } + }; + + const incompleteTodosCount = todos.filter((todo) => !todo.completed).length; + + const filteredTodos = todos.filter((todo) => { + if (filter === "all") return true; + if (filter === "active") return !todo.completed; + if (filter === "completed") return todo.completed; + return true; + }); + + return ( +
+

Todos📋

+
+ setNewTodo(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Add a new task..." + /> + +
+ +
+ {incompleteTodosCount} item{incompleteTodosCount !== 1 ? "s" : ""} left +
+
+ + + +
+
+ ); +} + +export default App; diff --git a/src/index.css b/src/index.css new file mode 100644 index 00000000..76fae189 --- /dev/null +++ b/src/index.css @@ -0,0 +1,88 @@ +body { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + background-color: #f0f0f0; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.container { + background-color: #fff; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + width: 300px; +} + +h1 { + text-align: center; + color: #333; +} + +.todo-input { + display: flex; + justify-content: space-between; +} + +.todo-input input { + flex: 1; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px 0 0 5px; + outline: none; +} + +.todo-input button { + padding: 10px; + border: none; + background-color: #28a745; + color: white; + border-radius: 0 5px 5px 0; + cursor: pointer; +} + +.todo-input button:hover { + background-color: #218838; +} + +ul { + list-style: none; + padding: 0; + margin-top: 20px; +} + +li { + background-color: #fff; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +li.completed { + text-decoration: line-through; + color: #888; +} + +li button { + background-color: #dc3545; + border: none; + color: white; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; +} + +li button:hover { + background-color: #c82333; +} + +li span { + cursor: pointer; +} 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..2b349aa2 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("app")).render( + + + +);