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
43 changes: 43 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// eslint.config.js (v9 Flat Config) - 추천 설정
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import unusedImports from 'eslint-plugin-unused-imports';
import prettierConfig from 'eslint-config-prettier';

export default [
{ ignores: ['dist', 'node_modules'] },
js.configs.recommended,

{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
parserOptions: { ecmaFeatures: { jsx: true } },
globals: { ...globals.browser },
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'unused-imports': unusedImports,
},
rules: {
// 코드 품질
'prefer-const': 'error', // 재할당 없으면 const 강제
'no-var': 'error', // var 금지
eqeqeq: ['error', 'always'], // === 강제

// 미사용 코드 정리
'no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error', // 미사용 import 자동 제거

// React
...reactHooks.configs.recommended.rules, // Hooks 규칙
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
},

prettierConfig, // Prettier 충돌 방지 (반드시 마지막에)
];
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}
29 changes: 29 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])
3 changes: 2 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/src/style.css" rel="stylesheet">
<title>leets</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
</html>
16 changes: 13 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"tailwind:init": "tailwindcss init -p"
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.2"
},
"devDependencies": {
"@babel/core": "^7.29.0",
Expand All @@ -20,11 +23,18 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.4.27",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"postcss": "^8.5.8",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4.2.2",
"vite": "^8.0.1"
}
}
}
12 changes: 1 addition & 11 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
.app-container {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 20px;
}

.main-title {
width: 100%;
text-align: left;
}
@import "tailwindcss";
147 changes: 51 additions & 96 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,130 +1,85 @@
import { useState } from 'react';
import Text from './components/Text';
import Button from './components/Button';
import Input from './components/Input';
import Checkbox from './components/Checkbox';
import './App.css';
import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import Form from './features/Form';
import FilterButtons from './features/FilterButtons';
import TodoItem from './features/TodoItem';

function App() {
const [newTask, setNewTask] = useState('');
const [todos, setTodos] = useState([
{ id: 1, text: 'Eat', completed: true },
{ id: 2, text: 'Sleep', completed: false },
{ id: 3, text: 'Repeat', completed: false },
]);

const [filter, setFilter] = useState('All');
const [editingId, setEditingId] = useState(null);
const [editingText, setEditingText] = useState('');

const handleAdd = () => {
const trimmed = newTask.trim();
if (!trimmed) return;

setTodos((prev) => [
...prev,
{ id: Date.now(), text: trimmed, completed: false },
]);
setNewTask('');

const navigate = useNavigate();
const location = useLocation();

const addTask = (name) => {
setTodos((prev) => [...prev, { id: Date.now(), text: name, completed: false }]);
};

const toggleTask = (id) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
prev.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo)
);
};

const deleteTask = (id) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};

const saveTask = (id) => {
const editTask = (id, newText) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, text: editingText } : todo
)
prev.map((todo) => todo.id === id ? { ...todo, text: newText } : todo)
);
setEditingId(null);
};

const currentPath = location.pathname;
const filteredTodos = todos.filter((todo) => {
if (filter === 'Active') return !todo.completed;
if (filter === 'Completed') return todo.completed;
if (currentPath === '/active') return !todo.completed;
if (currentPath === '/completed') return todo.completed;
return true;
});

const activeTasksCount = todos.filter((todo) => !todo.completed).length
const activeTasksCount = todos.filter((todo) => !todo.completed).length;

const todoListUI = (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todoListUI를 변수로 분리해 중복을 줄인 점이 좋았습니다.
다만 해당 렌더링 로직을 별도의 컴포넌트로 분리하면 역할이 더 명확해지고 재사용성도 높아질 것 같습니다!

<ul className="space-y-4">
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
toggleTask={toggleTask}
deleteTask={deleteTask}
editTask={editTask}
/>
))}
</ul>
);

return (
<div className="app-container">

<h1 className="main-title"><b>TodoMatic</b></h1>
<h2 style={{ width: '100%', textAlign: 'left' }}><b>What needs to be done?</b></h2>

<div className="todo-header" style={{ marginBottom: '10px' }}>
<Input
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
/>
<Button label="Add" onClick={handleAdd} />
</div>
<div className="min-h-screen bg-gray-100 py-12 px-4">
<div className="max-w-xl mx-auto p-8 bg-white rounded-lg shadow-sm border border-gray-200">

<h1 className="text-4xl font-extrabold text-center mb-2">TodoMatic</h1>
<h2 className="text-center text-gray-500 mb-6 text-lg">What needs to be done?</h2>

<div className="filters">
<Button label="Show all tasks" onClick={() => setFilter('All')} />
<Button label="Show active tasks" onClick={() => setFilter('Active')} />
<Button label="Show completed tasks" onClick={() => setFilter('Completed')} />
</div>
<Form addTask={addTask} />

<FilterButtons currentPath={currentPath} navigate={navigate} />

<h2 className="text-lg font-bold mb-4">
{activeTasksCount} tasks remaining
</h2>

<h2 id="list-heading" style={{ width: '100%', textAlign: 'left', marginTop: '20px' }}>
<b>{activeTasksCount} tasks remaining</b>
</h2>

<ul className="todo-list" style={{ paddingLeft: '20px' }}>
{filteredTodos.map((todo) => {
const taskName = todo.text;
const isEditing = editingId === todo.id;

return (
<li className="todo-item" key={todo.id} style={{ marginBottom: '15px' }}>

<div className="todo-content" style={{ display: 'flex', alignItems: 'center' }}>
<Checkbox
checked={todo.completed}
onChange={() => toggleTask(todo.id)}
/>
{isEditing ? (
<Input
value={editingText}
onChange={(e) => setEditingText(e.target.value)}
/>
) : (
<Text content={taskName} />
)}
</div>

<div className="todo-buttons" style={{ marginTop: '5px', marginLeft: '25px', display: 'flex', gap: '5px' }}>
{isEditing ? (
<Button label="Save" onClick={() => saveTask(todo.id)} />
) : (
<Button
label={`Edit ${taskName}`}
onClick={() => {
setEditingId(todo.id);
setEditingText(taskName);
}}
/>
)}
<Button
label={`Delete ${taskName}`}
onClick={() => deleteTask(todo.id)}
/>
</div>
</li>
);
})}
</ul>
<Routes>
<Route path="/" element={todoListUI} />
<Route path="/all" element={todoListUI} />
<Route path="/active" element={todoListUI} />
<Route path="/completed" element={todoListUI} />
</Routes>

</div>
</div>
);
}
Expand Down
21 changes: 19 additions & 2 deletions src/components/Button.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import React from 'react';

const Button = ({ label, onClick }) => {
const Button = ({ label, onClick, isActive }) => {
let style = "font-bold transition-colors ";

if (label === 'Add') {
style += "w-full border border-black bg-black text-white p-3 rounded-md text-lg mt-2";
} else if (label === 'Save') {
style += "flex-1 bg-black text-white py-2 rounded-md hover:bg-gray-800";
} else if (label === 'Delete') {
style += "flex-1 bg-[#ef4444] text-white py-2 rounded-md border border-[#ef4444] hover:bg-red-600";
} else if (label === 'Edit' || label === 'Cancel') {
style += "flex-1 bg-white border border-gray-300 text-gray-800 py-2 rounded-md hover:bg-gray-50";
} else if (label === 'All' || label === 'Active' || label === 'Completed') {
style += `flex-1 py-2 rounded-md border ${isActive
? 'bg-black text-white border-black'
: 'bg-white text-gray-600 border-gray-300 hover:bg-gray-50'
}`;
}

return (
<button className={`btn-${label}`} onClick={onClick}>
<button className={style} onClick={onClick}>
{label}
</button>
);
Expand Down
Loading