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 ( -