diff --git a/src/App.css b/src/App.css index 66f8f1f..1d3d268 100644 --- a/src/App.css +++ b/src/App.css @@ -3,18 +3,23 @@ body { font-family: Arial, sans-serif; /* 폰트 변경 */ display: flex; justify-content: center; - align-items: center; + align-items: flex-start; /* 상단 정렬 */ min-height: 100vh; margin: 0; background-color: #fff; + padding-top: 50px; /* 상단 여백 추가 */ } .app-container { max-width: 600px; width: 100%; padding: 20px; + /* 그림자에 대한 언급은 없으므로 제거하거나 유지 */ + /* box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); */ + background-color: #fff; /* 배경색 확실히 지정 */ } +/* 기존 요소들 스타일 */ h1 { font-size: 2.5rem; font-weight: bold; @@ -29,7 +34,7 @@ h2 { text-align: left; } -/* 입력창 */ +/* 입력창 (AddTodo) */ .input-container { margin-bottom: 10px; } @@ -40,7 +45,8 @@ h2 { font-size: 1rem; border: 2px solid #333; /* 입력창만 굵은 테두리 */ border-radius: 0; - box-sizing: border-box; + box-sizing: border-box; /* 패딩과 테두리가 width에 포함되도록 */ + outline: none; /* 포커스 시 기본 외곽선 제거 */ } /* Add 버튼 */ @@ -54,9 +60,11 @@ h2 { font-size: 1rem; background-color: #000; color: #fff; - border: none; + border: none; /* Add 버튼은 테두리 없음 */ border-radius: 0; cursor: pointer; + outline: none; + transition: background-color 0.2s ease; /* 호버 효과 부드럽게 */ } .add-button-container button:hover { @@ -80,9 +88,18 @@ h2 { border: 1px solid #333; /* 얇은 테두리 */ border-radius: 0; cursor: pointer; + outline: none; + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; } -.filter-buttons button:hover { +/* 활성 필터 버튼 스타일 */ +.filter-buttons button.active { + background-color: #000; /* 활성 버튼은 배경 검정 */ + color: #fff; /* 글자 흰색 */ + border-color: #000; +} + +.filter-buttons button:not(.active):hover { /* 활성 상태가 아닐 때만 호버 스타일 적용 */ background-color: #f0f0f0; } @@ -94,38 +111,53 @@ h2 { text-align: left; } -/* 작업 목록 */ +/* 작업 목록 컨테이너 */ .task-list { - margin-top: 10px; + /* margin-top: 10px; */ } +/* 각 할 일 항목 (기본 & 편집 공통 스타일) */ .task-item { - margin-bottom: 20px; + margin-bottom: 20px; /* 항목 하단 간격 */ + border-bottom: 1px solid #eee; /* 항목 구분선 */ + padding-bottom: 20px; /* 구분선과 내용 간 간격 */ } -.task-row { +.task-item:last-child { + border-bottom: none; /* 마지막 항목은 구분선 없음 */ + padding-bottom: 0; +} + + +/* 기본 모드 (.task-item 안에 있을 때) */ +.task-item .task-row { display: flex; align-items: center; - margin-bottom: 5px; + margin-bottom: 5px; /* 체크박스/이름과 버튼 간 간격 */ } -.task-row input[type="checkbox"] { +/* 체크박스 기본 스타일 */ +.task-item .task-row input[type="checkbox"] { width: 20px; height: 20px; margin-right: 10px; border: 2px solid #333; border-radius: 0; - appearance: none; + appearance: none; /* 기본 브라우저 스타일 제거 */ cursor: pointer; + flex-shrink: 0; /* 체크박스가 줄어들지 않도록 */ + position: relative; /* 체크마크 위치 기준 */ + outline: none; } -.task-row input[type="checkbox"]:checked { +/* 체크박스 체크된 상태 스타일 */ +.task-item .task-row input[type="checkbox"]:checked { background-color: #000; border-color: #000; - position: relative; } -.task-row input[type="checkbox"]:checked::after { +/* 체크박스 체크마크 */ +.task-item .task-row input[type="checkbox"]:checked::after { content: "✔"; color: #fff; font-size: 14px; @@ -135,36 +167,125 @@ h2 { transform: translate(-50%, -50%); } -.task-row .task-name { +/* 할 일 이름 */ +.task-item .task-row .task-name { font-size: 1rem; + flex-grow: 1; /* 남은 공간 차지 */ + word-break: break-word; /* 긴 단어 줄바꿈 */ +} + +/* 완료된 할 일 이름 스타일 */ +.task-item .task-row .task-name.completed { + text-decoration: line-through; + color: #888; /* 회색으로 변경 */ } -.task-actions { + +/* Edit/Delete 버튼 컨테이너 (기본 모드 & 편집 모드 공통) */ +.task-item .task-actions { display: flex; - gap: 10px; + gap: 10px; /* 버튼 간 간격 */ } -.task-actions button { - flex: 1; +/* Edit/Delete 버튼 기본 스타일 */ +.task-item .task-actions button { + flex: 1; /* 필터 버튼과 동일한 폭 */ padding: 5px 10px; font-size: 0.9rem; background-color: #fff; color: #000; - border: 1px solid #333; + border: 1px solid #333; /* 얇은 테두리 */ border-radius: 0; cursor: pointer; + outline: none; + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; } -.task-actions button:hover { +.task-item .task-actions button:hover { background-color: #f0f0f0; } -.task-actions .button-delete { - background-color: #ff0000; - color: #fff; - +/* Delete 버튼 스타일 */ +.task-item .task-actions button:last-child { + background-color: #ff0000; /* 빨간색 배경 */ + color: #fff; /* 흰색 글씨 */ + border-color: #ff0000; /* 배경과 동일한 색 테두리 */ +} + +.task-item .task-actions button:last-child:hover { + background-color: #e60000; /* 호버 시 약간 어두운 빨간색 */ + border-color: #e60000; +} + + +/* --- 👇 편집 모드 (Editing Mode) 스타일 --- */ + +/* 편집 모드일 때 할 일 항목 컨테이너 레이아웃 */ +.task-item.editing { + display: flex; /* Flexbox 사용 */ + flex-direction: column; /* 자식 요소를 세로로 쌓음 */ + align-items: flex-start; /* 왼쪽 정렬 */ +} + +/* 편집 모드일 때 기본 .task-row (체크박스 + 이름) 숨기기 */ +.task-item.editing .task-row { + display: none; +} + +/* 편집 모드 라벨 스타일 ("New name for...") */ +.task-item.editing .edit-label { + font-size: 1rem; /* task-name과 동일한 크기 */ + margin-bottom: 5px; /* 라벨과 입력창 간 간격 */ + font-weight: normal; /* 볼드 해제 (task-name은 볼드 아니었으므로) */ +} + +/* 편집 모드 입력창 스타일 */ +.task-item.editing input.edit-input { + width: 100%; /* 부모 컨테이너 폭에 맞춤 */ + padding: 8px; + font-size: 1rem; + border: 2px solid #333; + border-radius: 0; + box-sizing: border-box; + margin-bottom: 10px; /* 입력창과 버튼 간 간격 */ + outline: none; /* 포커스 시 기본 외곽선 제거 */ +} + +/* 편집 모드 버튼 컨테이너 스타일 (Cancel/Save) */ +.task-item.editing .task-actions { + display: flex; /* 버튼들은 Flex로 */ + gap: 10px; /* 버튼 간 간격 */ + width: 100%; /* 부모(task-item.editing) 너비에 맞춤 */ +} + +/* Cancel 버튼 스타일 */ +.task-item.editing .task-actions .cancel-button { /* 클래스 선택자 사용 */ + flex: 1; /* 동일 폭 */ + padding: 5px 10px; + font-size: 0.9rem; + background-color: #fff; /* 흰색 배경 */ + color: #000; /* 검은색 글씨 */ + border: 1px solid #333; /* 얇은 테두리 */ + border-radius: 0; +} +.task-item.editing .task-actions .cancel-button:hover { + background-color: #f0f0f0; +} + +/* Save 버튼 스타일 */ +.task-item.editing .task-actions .save-button { /* 클래스 선택자 사용 */ + flex: 1; /* 동일 폭 */ + padding: 5px 10px; + font-size: 0.9rem; + /* 👇 여기! Save 버튼 배경을 검정색으로 변경 */ + background-color: #000; /* 검은색 배경 */ + color: #fff; /* 글자색 흰색 유지 */ + border: none; /* 테두리 없음 유지 */ + /* 👆 여기까지 수정 */ + border-radius: 0; +} +/* Save 버튼 호버 스타일 */ +.task-item.editing .task-actions .save-button:hover { + background-color: #333; /* 호버 시 약간 밝은 검정색 */ } -.task-actions .button-delete:hover { - background-color: #e60000; -} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 755756d..4aa79b6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,42 +1,70 @@ - -import Header from "@/component/Header"; -import AddTodo from "@/component/AddTodo"; -import Category from "@/component/Category"; -import TodoList from "@/component/TodoList"; - +// src/App.jsx - 이전 답변에서 제공된 최종 코드 사용 +import React, { useState, useEffect } from "react"; +import Header from "./component/Header"; +import AddTodo from "./component/AddTodo"; +import Category from "./component/Category"; +import TodoList from "./component/TodoList"; import "./App.css"; +const LOCAL_STORAGE_KEY = "react-todo-app.tasks"; + function App() { - const [tasks, setTasks] = useState([ - { name: "Eat", completed: true }, - { name: "Sleep", completed: false }, - { name: "Repeat", completed: false }, - ]); + const [tasks, setTasks] = useState(() => { + try { + const storedTasks = localStorage.getItem(LOCAL_STORAGE_KEY); + return storedTasks ? JSON.parse(storedTasks) : []; + } catch (error) { + console.error("Failed to load tasks from localStorage:", error); + return []; + } + }); + const [inputValue, setInputValue] = useState(""); const [filter, setFilter] = useState("all"); + const [displayTaskCount, setDisplayTaskCount] = useState(tasks.length); + + useEffect(() => { + try { + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(tasks)); + } catch (error) { + console.error("Failed to save tasks to localStorage:", error); + } + }, [tasks]); + const addTask = () => { - if (inputValue) { - setTasks([...tasks, { name: inputValue, completed: false }]); + if (inputValue.trim()) { + const newTask = { + id: Date.now() + Math.random(), + name: inputValue.trim(), + completed: false, + }; + setTasks([...tasks, newTask]); + setDisplayTaskCount(prevCount => prevCount + 1); setInputValue(""); } }; - const toggleTask = (index) => { - const updatedTasks = [...tasks]; - updatedTasks[index].completed = !updatedTasks[index].completed; + const toggleTask = (id) => { + const updatedTasks = tasks.map(task => + task.id === id ? { ...task, completed: !task.completed } : task + ); setTasks(updatedTasks); }; - const deleteTask = (index) => { - const updatedTasks = tasks.filter((_, i) => i !== index); + const deleteTask = (id) => { + const updatedTasks = tasks.filter(task => task.id !== id); setTasks(updatedTasks); + setDisplayTaskCount(prevCount => prevCount - 1); }; - const editTask = (index, newName) => { - const updatedTasks = [...tasks]; - updatedTasks[index].name = newName; - setTasks(updatedTasks); + const editTask = (id, newName) => { + if (newName && newName.trim()) { + const updatedTasks = tasks.map(task => + task.id === id ? { ...task, name: newName.trim() } : task + ); + setTasks(updatedTasks); + } }; const filteredTasks = tasks.filter((task) => { @@ -60,6 +88,7 @@ function App() { toggleTask={toggleTask} editTask={editTask} deleteTask={deleteTask} + activeCount={displayTaskCount} /> ); diff --git a/src/component/AddTodo.jsx b/src/component/AddTodo.jsx index 4207b6b..cfd0715 100644 --- a/src/component/AddTodo.jsx +++ b/src/component/AddTodo.jsx @@ -10,7 +10,9 @@ function AddTodo({ inputValue, setInputValue, addTask }) { onChange={(e) => setInputValue(e.target.value)} /> - +
+ +
); } diff --git a/src/component/ButtonComponent.jsx b/src/component/ButtonComponent.jsx index 701db59..765a043 100644 --- a/src/component/ButtonComponent.jsx +++ b/src/component/ButtonComponent.jsx @@ -1,9 +1,10 @@ -function ButtonComponent({ label, onClick, type }) { - - const buttonClass = type === "add" ? "add-button-container" : ""; +import React from "react"; +// props 객체에서 className도 받아옵니다. +function ButtonComponent({ label, onClick, className }) { + // 받은 className을 ); diff --git a/src/component/Category.jsx b/src/component/Category.jsx index b0f252d..3ed738c 100644 --- a/src/component/Category.jsx +++ b/src/component/Category.jsx @@ -1,4 +1,3 @@ - import ButtonComponent from "./ButtonComponent"; function Category({ setFilter }) { diff --git a/src/component/CheckboxComponent.jsx b/src/component/CheckboxComponent.jsx index f41d65c..5473e91 100644 --- a/src/component/CheckboxComponent.jsx +++ b/src/component/CheckboxComponent.jsx @@ -1,4 +1,4 @@ - +import React from "react"; function CheckboxComponent({ isChecked, onChange }) { return ; diff --git a/src/component/Header.jsx b/src/component/Header.jsx index 03a3575..7d004a2 100644 --- a/src/component/Header.jsx +++ b/src/component/Header.jsx @@ -1,10 +1,12 @@ -import TextComponent from "./TextComponent"; - function Header() { return (
- - +

+ TodoMatic +

+

+ What needs to be done? +

); } diff --git a/src/component/InputComponent.jsx b/src/component/InputComponent.jsx index ad9d01f..6bfbe0d 100644 --- a/src/component/InputComponent.jsx +++ b/src/component/InputComponent.jsx @@ -1,5 +1,3 @@ - - function InputComponent({ value, onChange, placeholder }) { return ; } diff --git a/src/component/TextComponent.jsx b/src/component/TextComponent.jsx index ade087e..0381f37 100644 --- a/src/component/TextComponent.jsx +++ b/src/component/TextComponent.jsx @@ -1,11 +1,5 @@ -function TextComponent({ text, type }) { - - const style = { - fontSize: type === "title" ? "2rem" : "1.5rem", - fontWeight: type === "title" ? "bold" : "normal", - }; - - return {text}; +function TextComponent({ text }) { + return {text}; } export default TextComponent; \ No newline at end of file diff --git a/src/component/TodoComponent.jsx b/src/component/TodoComponent.jsx deleted file mode 100644 index f413526..0000000 --- a/src/component/TodoComponent.jsx +++ /dev/null @@ -1,31 +0,0 @@ -// src/components/Todo.jsx -import CheckboxComponent from "./CheckboxComponent"; -import ButtonComponent from "./ButtonComponent"; - -function Todo({ task, index, toggleTask, editTask, deleteTask }) { - return ( -
-
- toggleTask(index)} - /> - {task.name} -
-
- editTask(index, prompt("Edit task:", task.name))} - /> - deleteTask(index)} - /> -
-
- ); -} - -export default Todo; diff --git a/src/component/TodoList.jsx b/src/component/TodoList.jsx index 7341a74..ade116c 100644 --- a/src/component/TodoList.jsx +++ b/src/component/TodoList.jsx @@ -1,28 +1,107 @@ -// src/components/TodoList.jsx +import React, { useState } from "react"; // useState import import TextComponent from "./TextComponent"; -import Todo from "./Todo"; +import CheckboxComponent from "./CheckboxComponent"; +import ButtonComponent from "./ButtonComponent"; + +import "../App.css"; // App.css import + +function TodoList({ filteredTasks, toggleTask, editTask, deleteTask, activeCount }) { + // TodoList 컴포넌트 내에서 편집 중인 항목과 입력 값을 관리하는 상태 추가 + const [editingTaskId, setEditingTaskId] = useState(null); // null이면 편집 중인 항목 없음 + const [editingTextValue, setEditingTextValue] = useState(""); + + // Edit 버튼 클릭 시 호출될 함수 + const handleEditClick = (task) => { + setEditingTaskId(task.id); // 편집 모드로 전환, 해당 task의 ID 저장 + setEditingTextValue(task.name); // 입력 필드에 현재 task 이름 설정 + }; + + // Save 버튼 클릭 시 호출될 함수 + const handleSaveClick = (id) => { + const trimmedText = editingTextValue.trim(); // 앞뒤 공백 제거 + if (trimmedText) { // 빈 문자열이 아닐 때만 저장 + editTask(id, trimmedText); // App.jsx의 editTask 함수 호출 + setEditingTaskId(null); // 편집 모드 종료 + setEditingTextValue(""); // 입력 필드 초기화 + } else { + // 빈 문자열 입력 시 처리 (예: 원래 이름으로 되돌리고 모드 종료) + alert("Task name cannot be empty. Editing cancelled."); + setEditingTaskId(null); + setEditingTextValue(""); + } + }; + + // Cancel 버튼 클릭 시 호출될 함수 + const handleCancelClick = () => { + setEditingTaskId(null); // 편집 모드 종료 + setEditingTextValue(""); // 입력 필드 초기화 + }; -function TodoList({ filteredTasks, toggleTask, editTask, deleteTask }) { return (
- + {/* activeCount는 App.jsx에서 displayTaskCount 상태를 받아옴 */} +
- {filteredTasks.map((task, index) => ( - + {filteredTasks.map((task) => ( + // 각 task 항목에 대해 렌더링 +
+ {editingTaskId === task.id ? ( + // 👇 현재 task가 편집 중인 항목일 때 UI 렌더링 + <> + {/* "New name for [원래 이름]" 라벨 */} +
{/* 스타일을 위한 클래스 */} + +
+ + {/* 입력 필드 */} + setEditingTextValue(e.target.value)} + className="edit-input" // 스타일을 위한 클래스 + autoFocus // 편집 모드 진입 시 자동으로 포커스 + onKeyPress={(e) => { // Enter 키로 저장 + if (e.key === 'Enter') { + handleSaveClick(task.id); // 현재 task ID를 전달 + } + }} + /> + + {/* Cancel/Save 버튼 */} +
{/* 기존 task-actions 클래스 재사용 */} + {/* 버튼 순서: Cancel, Save */} + {/* 클래스 추가 */} + handleSaveClick(task.id)} className="save-button" /> {/* 현재 task ID를 전달 */} +
+ + ) : ( + // 👇 현재 task가 편집 중이 아닐 때 기본 UI 렌더링 + <> +
+ toggleTask(task.id)} // task.id 전달 + /> + {/* 완료된 할 일에 스타일 적용 */} + + +
+
+ {/* Edit 클릭 시 handleEditClick 호출 */} + handleEditClick(task)} /> {/* task 객체 전체 전달 */} + deleteTask(task.id)} /> {/* task.id 전달 */} +
+ + )} +
))}
); } -export default TodoList; - +export default TodoList; \ No newline at end of file diff --git a/src/contextAPI.jsx b/src/contextAPI.jsx new file mode 100644 index 0000000..f43547f --- /dev/null +++ b/src/contextAPI.jsx @@ -0,0 +1,86 @@ +import React, { createContext, useContext, useState } from 'react'; + +// 1. Context 생성 +const AuthContext = createContext(); + +// 2. AuthProvider 컴포넌트 +function AuthProvider({ children }) { + const [user, setUser] = useState(null); + + const login = (username) => { + setUser({ name: username }); + }; + + const logout = () => { + setUser(null); + }; + + return ( + + {children} + + ); +} + +// 3. Navbar 컴포넌트 +function Navbar() { + const { user, logout } = useContext(AuthContext); + + return ( +
+ {user ? ( + <> + 환영합니다, {user.name}님! + + + ) : ( + 로그인해주세요. + )} +
+ ); +} + +// 4. LoginForm 컴포넌트 +function LoginForm() { + const { user, login } = useContext(AuthContext); + const [inputValue, setInputValue] = useState(''); + + if (user) return null; + + return ( +
+ setInputValue(e.target.value)} + placeholder="사용자 이름을 입력하세요" + style={{ padding: '5px' }} + /> + +
+ ); +} + +// 5. App 컴포넌트 +function App() { + return ( + +
+ + +
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/contextAPI_lab.jsx b/src/contextAPI_lab.jsx new file mode 100644 index 0000000..f43547f --- /dev/null +++ b/src/contextAPI_lab.jsx @@ -0,0 +1,86 @@ +import React, { createContext, useContext, useState } from 'react'; + +// 1. Context 생성 +const AuthContext = createContext(); + +// 2. AuthProvider 컴포넌트 +function AuthProvider({ children }) { + const [user, setUser] = useState(null); + + const login = (username) => { + setUser({ name: username }); + }; + + const logout = () => { + setUser(null); + }; + + return ( + + {children} + + ); +} + +// 3. Navbar 컴포넌트 +function Navbar() { + const { user, logout } = useContext(AuthContext); + + return ( +
+ {user ? ( + <> + 환영합니다, {user.name}님! + + + ) : ( + 로그인해주세요. + )} +
+ ); +} + +// 4. LoginForm 컴포넌트 +function LoginForm() { + const { user, login } = useContext(AuthContext); + const [inputValue, setInputValue] = useState(''); + + if (user) return null; + + return ( +
+ setInputValue(e.target.value)} + placeholder="사용자 이름을 입력하세요" + style={{ padding: '5px' }} + /> + +
+ ); +} + +// 5. App 컴포넌트 +function App() { + return ( + +
+ + +
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/index.css b/src/index.css index e69de29..08a3ac9 100644 --- a/src/index.css +++ b/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/vite.config.js b/vite.config.js index 0891f5f..f661291 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,4 @@ - import react from "@vitejs/plugin-react"; +import react from "@vitejs/plugin-react"; import { resolve } from "node:path"; import { defineConfig } from "vite"; @@ -12,7 +12,8 @@ export default defineConfig({ }, }, server: { - + // 로컬호스트 변경 + host: "localhost", port: 3000, }, build: {