diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..c16edae --- /dev/null +++ b/.eslintrc.js @@ -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 충돌 방지 (반드시 마지막에) +]; \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..296fff4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"] +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/eslint.config.js @@ -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_]' }], + }, + }, +]) diff --git a/index.html b/index.html index 8715026..2d9c8d4 100644 --- a/index.html +++ b/index.html @@ -4,10 +4,11 @@ + leets
- + \ No newline at end of file diff --git a/package.json b/package.json index 36a05e0..3529d8e 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" } -} +} \ No newline at end of file diff --git a/src/App.css b/src/App.css index 9c75aa0..a461c50 100644 --- a/src/App.css +++ b/src/App.css @@ -1,11 +1 @@ -.app-container { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 20px; -} - -.main-title { - width: 100%; - text-align: left; -} \ No newline at end of file +@import "tailwindcss"; \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 1cb9f64..d35e6d7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,38 +1,26 @@ 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) ); }; @@ -40,91 +28,58 @@ function App() { 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 = ( + + ); return ( -
- -

TodoMatic

-

What needs to be done?

- -
- setNewTask(e.target.value)} - /> -
+
+
+ +

TodoMatic

+

What needs to be done?

-
-
+
+ + + +

+ {activeTasksCount} tasks remaining +

-

- {activeTasksCount} tasks remaining -

- -
    - {filteredTodos.map((todo) => { - const taskName = todo.text; - const isEditing = editingId === todo.id; - - return ( -
  • - -
    - toggleTask(todo.id)} - /> - {isEditing ? ( - setEditingText(e.target.value)} - /> - ) : ( - - )} -
    - -
    - {isEditing ? ( -
    -
  • - ); - })} -
+ + + + + + + +
); } diff --git a/src/components/Button.jsx b/src/components/Button.jsx index a96c7b8..46e83f0 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -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 ( - ); diff --git a/src/components/Input.jsx b/src/components/Input.jsx index 6367bac..46e83f0 100644 --- a/src/components/Input.jsx +++ b/src/components/Input.jsx @@ -1,12 +1,28 @@ -const Input = ({ value, onChange }) => { +import React from 'react'; + +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 ( - + ); }; -export default Input; \ No newline at end of file +export default Button; \ No newline at end of file diff --git a/src/components/Text.jsx b/src/components/Text.jsx index 8d4e2df..8728bb5 100644 --- a/src/components/Text.jsx +++ b/src/components/Text.jsx @@ -1,10 +1,11 @@ import React from 'react'; -const Text = ({ content }) => { +const Text = ({ children, className }) => { return ( - - {content} + + {children} ); }; + export default Text; \ No newline at end of file diff --git a/src/features/FilterButtons.jsx b/src/features/FilterButtons.jsx new file mode 100644 index 0000000..6b260c5 --- /dev/null +++ b/src/features/FilterButtons.jsx @@ -0,0 +1,13 @@ +import Button from '../components/Button'; + +const FilterButtons = ({ currentPath, navigate }) => { + return ( +
+
+ ); +}; + +export default FilterButtons; \ No newline at end of file diff --git a/src/features/Form.jsx b/src/features/Form.jsx new file mode 100644 index 0000000..8e7e226 --- /dev/null +++ b/src/features/Form.jsx @@ -0,0 +1,27 @@ +import { useState } from 'react'; +import Input from '../components/Input'; +import Button from '../components/Button'; + +const Form = ({ addTask }) => { + const [newTask, setNewTask] = useState(''); + + const handleAdd = () => { + const trimmed = newTask.trim(); + if (!trimmed) return; + addTask(trimmed); + setNewTask(''); + }; + + return ( +
+ setNewTask(e.target.value)} + /> +
+ ); +}; + +export default Form; \ No newline at end of file diff --git a/src/features/TodoItem.jsx b/src/features/TodoItem.jsx new file mode 100644 index 0000000..10b73e8 --- /dev/null +++ b/src/features/TodoItem.jsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import Checkbox from '../components/Checkbox'; +import Text from '../components/Text'; +import Input from '../components/Input'; +import Button from '../components/Button'; + +const TodoItem = ({ todo, toggleTask, deleteTask, editTask }) => { + const [isEditing, setIsEditing] = useState(false); + const [editingText, setEditingText] = useState(todo.text); + + const handleSave = () => { + const trimmed = editingText.trim(); + if (trimmed) { + editTask(todo.id, trimmed); + } else { + setEditingText(todo.text); + } + setIsEditing(false); + }; + + const handleCancel = () => { + setIsEditing(false); + setEditingText(todo.text); + }; + + return ( +
  • +
    + toggleTask(todo.id)} /> + {isEditing ? ( +
    + setEditingText(e.target.value)} /> +
    + ) : ( + + {todo.text} + + )} +
    + +
    + {isEditing ? ( + <> +
    +
  • + ); +}; + +export default TodoItem; \ No newline at end of file diff --git a/src/index.css b/src/index.css index e69de29..a461c50 100644 --- a/src/index.css +++ b/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..ca89fd5 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,10 +1,13 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.jsx' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import './index.css'; -createRoot(document.getElementById('root')).render( - - - , -) +ReactDOM.createRoot(document.getElementById('root')).render( + + {/* 2. 감싸기 */} + + + , +); \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index d1203cd..af0a621 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,11 +1,12 @@ import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite' import react, { reactCompilerPreset } from '@vitejs/plugin-react' import babel from '@rolldown/plugin-babel' -// https://vite.dev/config/ export default defineConfig({ plugins: [ - react(), - babel({ presets: [reactCompilerPreset()] }) + tailwindcss(), + react(), + babel({ presets: [reactCompilerPreset()] }) ], -}) +}) \ No newline at end of file