diff --git a/Mission-FE-Zero100 b/Mission-FE-Zero100 new file mode 160000 index 0000000..839460c --- /dev/null +++ b/Mission-FE-Zero100 @@ -0,0 +1 @@ +Subproject commit 839460c80639bc697cb6f3621879e9e8900adf57 diff --git a/package-lock.json b/package-lock.json index 99bd306..9646210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router-dom": "^7.4.1", + "react-router-dom": "^7.6.0", "styled-components": "^6.1.16" }, "devDependencies": { @@ -1377,12 +1377,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2605,15 +2599,13 @@ } }, "node_modules/react-router": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.1.tgz", - "integrity": "sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", + "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", "license": "MIT", "dependencies": { - "@types/cookie": "^0.6.0", "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0", - "turbo-stream": "2.4.0" + "set-cookie-parser": "^2.6.0" }, "engines": { "node": ">=20.0.0" @@ -2629,12 +2621,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.1.tgz", - "integrity": "sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz", + "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==", "license": "MIT", "dependencies": { - "react-router": "7.4.1" + "react-router": "7.6.0" }, "engines": { "node": ">=20.0.0" @@ -2879,12 +2871,6 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "license": "0BSD" }, - "node_modules/turbo-stream": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", - "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", - "license": "ISC" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 4003de3..d0aa047 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router-dom": "^7.4.1", + "react-router-dom": "^7.6.0", "styled-components": "^6.1.16" }, "devDependencies": { diff --git a/src/App.css b/src/App.css index 66f8f1f..29255d1 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,258 @@ 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-actions { +/* 완료된 할 일 이름 스타일 */ +.task-item .task-row .task-name.completed { + text-decoration: line-through; + color: #888; /* 회색으로 변경 */ +} + + +/* 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; /* 호버 시 약간 밝은 검정색 */ +} + +body { + font-family: sans-serif; /* 폰트는 적절히 수정하세요 */ + margin: 0; + background-color: #ffffff; /* 이미지 배경이 흰색이므로 흰색으로 설정 */ + display: flex; + justify-content: center; + padding-top: 20px; +} + +.app-container { + text-align: center; + width: 100%; + max-width: 450px; /* 이미지의 UI 너비에 맞게 조절 */ +} + +.app-title { + font-size: 2.5em; /* 이미지와 유사하게 크기 조절 */ + font-weight: bold; + margin-bottom: 40px; /* 버튼들과의 간격 */ + color: #000000; /* 검은색 글씨 */ +} + +.nav-buttons button { + background-color: #e7e7e7; /* 이미지의 밝은 회색 버튼 */ + color: #333; + border: none; + padding: 15px 30px; /* 버튼 크기 조절 */ + margin: 10px; + border-radius: 30px; /* 둥근 모서리 */ + font-size: 1.1em; + font-weight: bold; /* 글씨 굵게 */ + cursor: pointer; + transition: background-color 0.3s ease; + width: 80%; /* 버튼 너비 조절 */ + max-width: 300px; /* 최대 너비 */ +} + +.nav-buttons button:hover { + background-color: #dcdcdc; +} + +.form-container { + background-color: #f7f7f7; /* 이미지의 폼 배경색 */ + padding: 30px 40px; /* 패딩 조절 */ + border-radius: 8px; /* 약간의 둥근 모서리 */ + /* box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 이미지에는 그림자가 없어보이지만, 필요시 추가 */ + margin-top: 30px; + text-align: left; +} + +.form-container h2 { + font-size: 2.2em; /* 제목 크기 조절 */ + font-weight: bold; + color: #000000; + text-align: center; + margin-bottom: 30px; +} + +.form-group { + margin-bottom: 20px; + display: flex; + align-items: center; +} + +.form-group label { + font-weight: bold; + color: #333; /* 글자색 */ + margin-right: 15px; + width: 90px; /* 레이블 너비 고정 (비밀번호 글자 수에 맞춤) */ + text-align: left; /* 레이블 왼쪽 정렬 */ + font-size: 1em; +} + +.form-group input[type="text"], +.form-group input[type="password"] { + flex-grow: 1; + padding: 12px 15px; + border: 1px solid #cccccc; + border-radius: 5px; /* 인풋 둥근 모서리 */ + font-size: 1em; +} + +/* 로그인 페이지의 로그인 버튼 */ +.form-container button[type="submit"] { + background-color: #5a5a5a; /* 이미지의 어두운 회색 버튼 */ + color: white; + display: block; + width: 100%; + padding: 14px; + font-size: 1.1em; + font-weight: bold; + border: none; + border-radius: 5px; /* 버튼 모서리 */ + cursor: pointer; + transition: background-color 0.3s ease; + margin-top: 10px; /* 에러 메시지 위 공간 */ +} +.form-container button[type="submit"]:hover { + background-color: #4a4a4a; +} + +/* 로그인 폼 내의 회원가입 이동 텍스트 (버튼처럼 보이지 않게) */ +.login-form-signup-button { + background-color: transparent; + color: #555; /* 글자색 */ + border: none; + padding: 0; /* 패딩 제거 */ + font-size: 0.95em; /* 글자 크기 */ + cursor: pointer; + text-decoration: none; /* 밑줄 제거, 필요시 추가 */ + display: block; + text-align: center; /* 중앙 정렬 */ + margin-top: 25px; /* 로그인 버튼과의 간격 */ +} + +.login-form-signup-button:hover { + color: #000; /* 호버 시 색상 변경 */ + text-decoration: underline; /* 호버 시 밑줄 */ +} + + +.error-message { + color: red; + font-size: 1.2em; /* 점 크기를 위해 폰트 사이즈 조절 */ + text-align: right; + /* height: 20px; 레이아웃을 위해 필요하다면 설정 */ + /* padding-right: 0; 로그인 폼 오른쪽 끝에 빨간 점을 위한 위치 조정 */ + /* 이미지는 입력 필드 오른쪽에 점이 있으므로, 이 스타일은 form-group 내에 위치하는게 좋을 수 있음 */ + /* 지금은 로그인 버튼 위에 표시되도록 설정되어 있음 */ + margin-top: 5px; + margin-bottom: 10px; /* 로그인 버튼과의 간격 */ } -.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..73fe676 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,67 +1,29 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import Header from "@/component/Header"; -import AddTodo from "@/component/AddTodo"; -import Category from "@/component/Category"; -import TodoList from "@/component/TodoList"; +// 페이지 컴포넌트 import +import HomePage from './pages/HomePage'; // 이전 단계에서 생성 +import LoginPage from './pages/LoginPage'; // 이전 단계에서 생성 +import SignupPage from './pages/SignupPage'; // 이전 단계에서 생성 +import TodoPage from './pages/TodoPage'; // 방금 수정한 TodoPage -import "./App.css"; +import './App.css'; // 전역 스타일 및 로그인/회원가입 UI 스타일 포함 function App() { - const [tasks, setTasks] = useState([ - { name: "Eat", completed: true }, - { name: "Sleep", completed: false }, - { name: "Repeat", completed: false }, - ]); - const [inputValue, setInputValue] = useState(""); - const [filter, setFilter] = useState("all"); - - const addTask = () => { - if (inputValue) { - setTasks([...tasks, { name: inputValue, completed: false }]); - setInputValue(""); - } - }; - - const toggleTask = (index) => { - const updatedTasks = [...tasks]; - updatedTasks[index].completed = !updatedTasks[index].completed; - setTasks(updatedTasks); - }; - - const deleteTask = (index) => { - const updatedTasks = tasks.filter((_, i) => i !== index); - setTasks(updatedTasks); - }; - - const editTask = (index, newName) => { - const updatedTasks = [...tasks]; - updatedTasks[index].name = newName; - setTasks(updatedTasks); - }; - - const filteredTasks = tasks.filter((task) => { - if (filter === "all") return true; - if (filter === "active") return !task.completed; - if (filter === "completed") return task.completed; - return true; - }); - return ( -
-
- - - -
+ +
{/* 전체 앱을 감싸는 컨테이너 */} +

OO's TODO

{/* 모든 페이지 상단에 표시될 제목 */} + + } /> + } /> + } /> + } /> {/* Todo 페이지 라우트 */} + {/* 일치하는 라우트가 없을 경우 루트 페이지로 리다이렉트 (선택 사항) */} + } /> + +
+
); } diff --git a/src/Mission-FE-Zero100 b/src/Mission-FE-Zero100 new file mode 160000 index 0000000..839460c --- /dev/null +++ b/src/Mission-FE-Zero100 @@ -0,0 +1 @@ +Subproject commit 839460c80639bc697cb6f3621879e9e8900adf57 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..bcb646d 100644 --- a/src/component/ButtonComponent.jsx +++ b/src/component/ButtonComponent.jsx @@ -1,10 +1,23 @@ -function ButtonComponent({ label, onClick, type }) { - - const buttonClass = type === "add" ? "add-button-container" : ""; +// src/component/ButtonComponent.jsx +import React from "react"; +function ButtonComponent({ + children, // 버튼 내부에 표시될 내용 (텍스트, 아이콘 등) + onClick, // 클릭 시 실행될 함수 + className = "", // CSS 클래스 (기본값 빈 문자열) + type = "button", // 버튼 타입 (기본값 "button") + disabled = false, // 비활성화 여부 (기본값 false) + ...restProps // 그 외 HTML button 속성들 (예: aria-label) +}) { return ( - ); } 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..a555b8e 100644 --- a/src/component/CheckboxComponent.jsx +++ b/src/component/CheckboxComponent.jsx @@ -1,7 +1,7 @@ - +import React from "react"; function CheckboxComponent({ isChecked, onChange }) { return ; } -export default CheckboxComponent; \ No newline at end of file +export default CheckboxComponent; diff --git a/src/component/Header.jsx b/src/component/Header.jsx index 03a3575..1d3ffc5 100644 --- a/src/component/Header.jsx +++ b/src/component/Header.jsx @@ -1,12 +1,15 @@ -import TextComponent from "./TextComponent"; - function Header() { return (
- - +

+ TodoMatic +

+

+ What needs to be done? +

); } -export default Header; \ No newline at end of file +export default Header; +java \ No newline at end of file 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 index f413526..e06dd27 100644 --- a/src/component/TodoComponent.jsx +++ b/src/component/TodoComponent.jsx @@ -1,31 +1,106 @@ -// src/components/Todo.jsx -import CheckboxComponent from "./CheckboxComponent"; -import ButtonComponent from "./ButtonComponent"; +// src/component/TodoComponent.jsx +import React, { useState } from 'react'; +import CheckboxComponent from './CheckboxComponent'; // CheckboxComponent가 있다고 가정 +import ButtonComponent from './ButtonComponent'; // ButtonComponent 사용 +import TextComponent from './TextComponent'; // TextComponent가 있다고 가정 (선택 사항) + +// CSS 클래스명을 위한 별도 파일 또는 App.css에 정의된 클래스 사용 +// import './TodoComponent.css'; // 만약 TodoComponent.css 파일이 있다면 + +function TodoComponent({ task, toggleTask, deleteTask, editTask }) { + const [isEditing, setIsEditing] = useState(false); + const [editedName, setEditedName] = useState(task.name); + + const handleEdit = () => { + console.log("TodoComponent: handleEdit for task:", task); + setIsEditing(true); + setEditedName(task.name); // 수정 시작 시 현재 이름으로 입력 필드 초기화 + }; + + const handleSave = () => { + console.log("TodoComponent: handleSave for task id:", task.id, "new name:", editedName); + if (editedName.trim()) { + editTask(task.id, editedName.trim()); // App.jsx의 editTask 호출 + setIsEditing(false); + } else { + alert("Task name cannot be empty."); + // 선택: 빈 값일 경우 원래 이름으로 되돌리거나, 수정 모드 유지 등 + // setEditedName(task.name); + // setIsEditing(false); + } + }; + + const handleCancel = () => { + console.log("TodoComponent: handleCancel for task:", task); + setIsEditing(false); + setEditedName(task.name); // 취소 시 원래 이름으로 복원 + }; + + const handleNameChange = (e) => { + setEditedName(e.target.value); + }; + + const handleKeyPress = (e) => { + if (e.key === 'Enter') { + handleSave(); + } + }; -function Todo({ task, index, toggleTask, editTask, deleteTask }) { return ( -
-
- toggleTask(index)} - /> - {task.name} -
-
- editTask(index, prompt("Edit task:", task.name))} - /> - deleteTask(index)} - /> -
+
+ {isEditing ? ( + // --- 수정 모드 UI --- + <> +
+ {/* TextComponent가 있다면 사용, 없다면 일반 span/p 태그 사용 */} + {TextComponent ? : New name for {task.name}} +
+ +
+ + Cancel + + + Save + +
+ + ) : ( + // --- 일반 모드 UI --- + <> +
+ {/* CheckboxComponent가 있다고 가정 */} + toggleTask(task.id)} // App.jsx의 toggleTask 호출 + /> + !isEditing && toggleTask(task.id)} // 텍스트 클릭으로도 토글 (수정 중 아닐 때만) + > + {/* TextComponent가 있다면 사용, 없다면 그냥 텍스트 표시 */} + {TextComponent ? : task.name} + +
+
+ + Edit + + deleteTask(task.id)} className="delete-button"> + Delete + +
+ + )}
); } -export default Todo; +export default TodoComponent; \ No newline at end of file diff --git a/src/component/TodoList.jsx b/src/component/TodoList.jsx index 7341a74..a009fe9 100644 --- a/src/component/TodoList.jsx +++ b/src/component/TodoList.jsx @@ -1,22 +1,47 @@ -// src/components/TodoList.jsx -import TextComponent from "./TextComponent"; -import Todo from "./Todo"; +// src/component/TodoList.jsx +import React from 'react'; // useState는 TodoComponent로 이동했으므로 여기선 필요 없을 수 있음 +import TodoComponent from './TodoComponent'; // 방금 만든 TodoComponent를 import +import TextComponent from './TextComponent'; + +import '../App.css'; + +function TodoList({ filteredTasks, toggleTask, editTask, deleteTask, activeCount }) { + // TodoList 내의 편집 관련 상태는 TodoComponent로 이동했습니다. + // const [editingTaskId, setEditingTaskId] = useState(null); + // const [editingTextValue, setEditingTextValue] = useState(""); + + // 핸들러 함수들도 TodoComponent로 이동했습니다. + // const handleEditClick = (task) => { ... }; + // const handleSaveClick = (id) => { ... }; + // const handleCancelClick = () => { ... }; + + // 만약 filteredTasks가 비어있을 때 메시지를 표시하고 싶다면 + if (!filteredTasks || filteredTasks.length === 0) { + return ( +
+
+ +
+
+ ); + } -function TodoList({ filteredTasks, toggleTask, editTask, deleteTask }) { return (
- +
- {filteredTasks.map((task, index) => ( - ( + // 각 task에 대해 TodoComponent를 렌더링하고 필요한 props 전달 + ))}
@@ -24,5 +49,4 @@ function TodoList({ filteredTasks, toggleTask, editTask, deleteTask }) { ); } -export default TodoList; - +export default TodoList; \ 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/src/pages/HomePage.jsx b/src/pages/HomePage.jsx new file mode 100644 index 0000000..13c4dfe --- /dev/null +++ b/src/pages/HomePage.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +function HomePage() { + const navigate = useNavigate(); + + return ( +
+ + +
+ ); +} + +export default HomePage; \ No newline at end of file diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx new file mode 100644 index 0000000..b256de3 --- /dev/null +++ b/src/pages/LoginPage.jsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +function LoginPage() { + const [id, setId] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const handleSubmit = (e) => { + e.preventDefault(); + setError(''); + console.log('Login attempt:', { id, password }); + if (id === 'abcde' && password === '1234') { // 임시 로그인 성공 조건 + alert('로그인 성공!'); + navigate('/todo'); + } else { + setError('•'); + } + }; + + return ( +
+

로그인

+
+
+ + setId(e.target.value)} + placeholder="abcde" + required + /> +
+
+ + setPassword(e.target.value)} + placeholder="••••" + required + /> +
+ {error &&
{error}
} + +
+ +
+ ); +} + +export default LoginPage; \ No newline at end of file diff --git a/src/pages/SignupPage.jsx b/src/pages/SignupPage.jsx new file mode 100644 index 0000000..b4486af --- /dev/null +++ b/src/pages/SignupPage.jsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +function SignupPage() { + const [name, setName] = useState(''); + const [id, setId] = useState(''); + const [password, setPassword] = useState(''); + const navigate = useNavigate(); + + const handleSubmit = (e) => { + e.preventDefault(); + console.log('Signup attempt:', { name, id, password }); + alert('회원가입 요청이 전송되었습니다.'); + navigate('/login'); + }; + + return ( +
+

회원가입

+
+
+ + setName(e.target.value)} + placeholder="김철수" + required + /> +
+
+ + setId(e.target.value)} + placeholder="chulsu" + required + /> +
+
+ + setPassword(e.target.value)} + placeholder="••••" + required + /> +
+ +
+
+ ); +} + +export default SignupPage; \ No newline at end of file diff --git a/src/pages/TodoPage.jsx b/src/pages/TodoPage.jsx new file mode 100644 index 0000000..b138f9d --- /dev/null +++ b/src/pages/TodoPage.jsx @@ -0,0 +1,97 @@ +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(() => { + 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.trim()) { + const newTask = { + id: Date.now() + Math.random(), + name: inputValue.trim(), + completed: false, + }; + setTasks([...tasks, newTask]); + setDisplayTaskCount(prevCount => prevCount + 1); + setInputValue(""); + } + }; + + const toggleTask = (id) => { + const updatedTasks = tasks.map(task => + task.id === id ? { ...task, completed: !task.completed } : task + ); + setTasks(updatedTasks); + }; + + const deleteTask = (id) => { + console.log("App.jsx: deleteTask, id:", id); + const updatedTasks = tasks.filter(task => task.id !== id); + setTasks(updatedTasks); + setDisplayTaskCount(prevCount => prevCount - 1); + }; + + 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) => { + if (filter === "all") return true; + if (filter === "active") return !task.completed; + if (filter === "completed") return task.completed; + return true; + }); + + return ( +
+
+ + + +
+ ); +} + +export default App; \ No newline at end of file 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: {