Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
# react-todo-list-precourse
# react-todo-list-precourse

## 기능 요구 사항

하루 또는 한 주의 할 일 목록을 업데이트하는 할 일 목록을 구현한다. **React 라이브러리를 사용하여 웹 앱으로 구현한다.**

- 할 일을 추가하고 삭제할 수 있다.
- 할 일을 추가할 때 사용자는 Enter 키나 추가 버튼을 사용하여 할 일을 목록에 추가할 수 있어야 한다.
- 사용자가 아무것도 입력하지 않은 경우에는 할 일을 추가할 수 없다.
- 할 일의 목록을 볼 수 있다.
- 할 일의 완료 상태를 전환할 수 있다.

### 선택 요구 사항

- 현재 진행 중인 할 일, 완료된 할 일, 모든 할 일을 필터링할 수 있다.
- 해야 할 일의 총개수를 확인할 수 있다.
- 새로고침을 하여도 이전에 작성한 데이터는 유지되어야 한다.

### 피드백

- 키보드로 할일을 입력할 때 이벤트가 2번 실행됩니다. 이는 한글과 같은 문자를 입력할 때 키보드 이벤트를 사용하는 경우 발생하는 문제입니다. -> react isComposing
- 완료한 할일의 삭제 버튼에 선 없애기
- 함수 컴포넌트를 포함하여 함수는 15줄 이내로 작성
- 컴포넌트와 유틸함수 분리
- depth는 3 이내로
- css 변경은 design을 사용할 것
6 changes: 3 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<title>Todos📋</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
131 changes: 131 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -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;
}
95 changes: 95 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container">
<h1>Todos📋</h1>
<div className="todo-input">
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Add a new task..."
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{filteredTodos.map((todo, index) => (
<li key={index} className={todo.completed ? "completed" : ""}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(index)}
/>
<span onClick={() => toggleTodo(index)}>
{todo.completed ? <s>{todo.text}</s> : todo.text}
</span>
<button onClick={() => deleteTodo(index)}>Delete</button>
</li>
))}
</ul>
<div className="todo-footer">
{incompleteTodosCount} item{incompleteTodosCount !== 1 ? "s" : ""} left
</div>
<div className="filters">
<button onClick={() => setFilter("all")} className={filter === "all" ? "active" : ""}>
All
</button>
<button
onClick={() => setFilter("active")}
className={filter === "active" ? "active" : ""}
>
Active
</button>
<button
onClick={() => setFilter("completed")}
className={filter === "completed" ? "active" : ""}
>
Completed
</button>
</div>
</div>
);
}

export default App;
88 changes: 88 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -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;
}
Empty file removed src/main.js
Empty file.
10 changes: 10 additions & 0 deletions src/main.jsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);