diff --git a/README.md b/README.md
index 3c0710d2..79746ce5 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,11 @@
-# react-todo-list-precourse
\ No newline at end of file
+# react-todo-list-precourse
+
+## 기능 구현 목록
+
+- 사용자 입력과 할 일 목록 생성
+- 엔터 누를 시에 추가
+- 할 일 상태 업데이트
+- 할 일 삭제
+- 남은 할 일 개수 표시
+- 완료한 할 일 전체 삭제
+- 완료한 할 일과 남은 할 일을 보이기
diff --git a/index.html b/index.html
index b021b5c8..d9f615bf 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,12 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ React Todo List
+
+
+
+
+
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 00000000..91831d9f
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,153 @@
+/* 기본 스타일 설정 */
+.App {
+ text-align: center;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ background: #f5f5f5;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 20px;
+}
+
+.App h1 {
+ color: #b83f45;
+ font-size: 100px;
+ font-weight: 100;
+ margin: 40px 0;
+}
+
+.todo-container {
+ background: white;
+ width: 550px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ margin-bottom: 20px;
+ position: relative;
+ border-radius: 4px;
+}
+
+.todo-input-container {
+ padding: 16px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-input {
+ width: 100%;
+ padding-top: 16px;
+ border: none;
+ font-size: 24px;
+ box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
+ outline: none;
+}
+
+.todo-input::placeholder {
+ color: #e6e6e6;
+ font-style: italic;
+}
+
+.todo-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.todo-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 15px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-item.completed .todo-text {
+ text-decoration: line-through;
+ color: #d9d9d9;
+}
+
+.todo-item .todo-content {
+ display: flex;
+ align-items: center;
+ width: 100%;
+}
+
+.todo-item input[type='checkbox'] {
+ margin-right: 20px;
+ width: 24px;
+ height: 24px;
+ appearance: none;
+ background-color: white;
+ border: 1px solid #e6e6e6;
+ border-radius: 50%;
+ cursor: pointer;
+ position: relative;
+}
+
+.todo-item input[type='checkbox']:checked::after {
+ content: '✔';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 16px;
+ color: #b83f45;
+}
+
+.todo-item .todo-text {
+ font-size: 24px;
+}
+
+.todo-item .delete-button {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ color: #cc9a9a;
+ visibility: hidden;
+}
+
+.todo-item:hover .delete-button {
+ visibility: visible;
+}
+
+.todo-item .delete-button:hover {
+ color: #af5b5e;
+}
+
+.todo-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 15px;
+ border-top: 1px solid #e6e6e6;
+ color: #777;
+ background: white;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+}
+
+.todo-footer button {
+ background: none;
+ border: none;
+ color: #777;
+ cursor: pointer;
+}
+
+.todo-footer button:hover {
+ text-decoration: underline;
+}
+
+.todo-filter {
+ display: flex;
+ gap: 10px;
+}
+
+.todo-filter button {
+ background: none;
+ border: 1px solid transparent;
+ padding: 3px 7px;
+ cursor: pointer;
+}
+
+.todo-filter .selected {
+ border-color: rgba(175, 47, 47, 0.2);
+}
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 00000000..5842e48b
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,71 @@
+import React, { useState, useEffect } from 'react';
+import './App.css';
+import TodoInput from './components/TodoInput';
+import TodoList from './components/TodoList';
+import TodoFilter from './components/TodoFilter';
+
+function App() {
+ const [todos, setTodos] = useState(() => {
+ const savedTodos = localStorage.getItem('todos');
+ return savedTodos ? JSON.parse(savedTodos) : [];
+ });
+ const [filter, setFilter] = useState('all');
+
+ useEffect(() => {
+ localStorage.setItem('todos', JSON.stringify(todos));
+ }, [todos]);
+
+ const addTodo = (text) => {
+ setTodos([...todos, { text, completed: false }]);
+ };
+
+ const toggleTodo = (index) => {
+ const updatedTodos = todos.map((todo, i) => (i === index ? { ...todo, completed: !todo.completed } : todo));
+ setTodos(updatedTodos);
+ };
+
+ const deleteTodo = (index) => {
+ setTodos(todos.filter((_, i) => i !== index));
+ };
+
+ const clearCompleted = () => {
+ setTodos(todos.filter((todo) => !todo.completed));
+ };
+
+ const filteredTodos = todos.filter((todo) => {
+ if (filter === 'completed') return todo.completed;
+ if (filter === 'active') return !todo.completed;
+ return true;
+ });
+
+ const remainingCount = todos.filter((todo) => !todo.completed).length;
+
+ return (
+
+
todos
+
+
+
+
+ {todos.length > 0 && (
+ <>
+
+
+
+ {remainingCount} {remainingCount === 1 ? 'item' : 'items'} left
+
+
+
+ Clear completed
+
+
+ >
+ )}
+
+
+ );
+}
+
+export default App;
diff --git a/src/components/TodoFilter.jsx b/src/components/TodoFilter.jsx
new file mode 100644
index 00000000..8b301533
--- /dev/null
+++ b/src/components/TodoFilter.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+function TodoFilter({ filter, setFilter }) {
+ return (
+
+ setFilter('all')} className={filter === 'all' ? 'selected' : ''}>
+ All
+
+ setFilter('active')} className={filter === 'active' ? 'selected' : ''}>
+ Active
+
+ setFilter('completed')} className={filter === 'completed' ? 'selected' : ''}>
+ Completed
+
+
+ );
+}
+
+export default TodoFilter;
diff --git a/src/components/TodoInput.jsx b/src/components/TodoInput.jsx
new file mode 100644
index 00000000..36e62d91
--- /dev/null
+++ b/src/components/TodoInput.jsx
@@ -0,0 +1,24 @@
+import React, { useState } from 'react';
+
+function TodoInput({ addTodo }) {
+ const [newTodo, setNewTodo] = useState('');
+
+ const handleAddTodo = () => {
+ if (newTodo.trim() === '') return;
+ addTodo(newTodo);
+ setNewTodo('');
+ };
+
+ return (
+ setNewTodo(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()}
+ placeholder="What needs to be done?"
+ className="todo-input"
+ />
+ );
+}
+
+export default TodoInput;
diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx
new file mode 100644
index 00000000..376b75d3
--- /dev/null
+++ b/src/components/TodoItem.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+function TodoItem({ todo, index, toggleTodo, deleteTodo }) {
+ return (
+
+
+ toggleTodo(index)} />
+ toggleTodo(index)}>
+ {todo.text}
+
+ deleteTodo(index)} className="delete-button">
+ ✕
+
+
+
+ );
+}
+
+export default TodoItem;
diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx
new file mode 100644
index 00000000..4d972b17
--- /dev/null
+++ b/src/components/TodoList.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import TodoItem from './TodoItem';
+
+function TodoList({ todos, toggleTodo, deleteTodo }) {
+ return (
+ <>
+ {todos.map((todo, index) => (
+
+ ))}
+ >
+ );
+}
+
+export default TodoList;
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..a9cbcd75
--- /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 './App.css';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+);
diff --git a/vite.config.ts b/vite.config.ts
index 5a33944a..d7ec70e1 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,7 +1,7 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()],
-})
+ plugins: [react()],
+});