diff --git a/package-lock.json b/package-lock.json index 3c80b3e3..06a4aba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,9 @@ "name": "15-sprint-mission", "version": "0.0.0", "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", "react-router-dom": "^7.5.0" }, "devDependencies": { @@ -1353,12 +1354,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", @@ -1935,6 +1930,21 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2084,6 +2094,15 @@ "node": ">=0.8.19" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2118,7 +2137,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2228,6 +2246,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2381,6 +2411,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -2431,24 +2474,48 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "license": "Apache-2.0", + "dependencies": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-refresh": { @@ -2462,15 +2529,13 @@ } }, "node_modules/react-router": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz", - "integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==", + "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" @@ -2486,12 +2551,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.0.tgz", - "integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==", + "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.5.0" + "react-router": "7.6.0" }, "engines": { "node": ">=20.0.0" @@ -2552,10 +2617,13 @@ } }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } }, "node_modules/semver": { "version": "6.3.1", @@ -2573,6 +2641,12 @@ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2632,11 +2706,22 @@ "node": ">=8" } }, - "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/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } }, "node_modules/type-check": { "version": "0.4.0", @@ -2693,15 +2778,18 @@ } }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" diff --git a/package.json b/package.json index bbb09b73..5ecaeaf4 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "preview": "vite preview" }, "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", "react-router-dom": "^7.5.0" }, "devDependencies": { diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 00000000..4d5dfaa5 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,33 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { HelmetProvider } from 'react-helmet-async'; +import Index from './pages/Index.jsx'; +import Items from './pages/Items.jsx'; +import AddItem from './pages/AddItem.jsx'; +import Auth from './pages/Auth.jsx'; +import Login from './pages/Login.jsx'; +import Signup from './pages/Signup.jsx'; +import './reset.css'; +import './color.css'; + +function App() { + return ( + + + + } /> + + } /> + + } /> + + }> + } /> + } /> + + + + + ); +} + +export default App; diff --git a/src/assets/ic_X.svg b/src/assets/ic_X.svg new file mode 100644 index 00000000..f6674f7f --- /dev/null +++ b/src/assets/ic_X.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/ic_plus.svg b/src/assets/ic_plus.svg new file mode 100644 index 00000000..5bb9abf5 --- /dev/null +++ b/src/assets/ic_plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/color.css b/src/color.css index 1d55bfe6..9b27e3ab 100644 --- a/src/color.css +++ b/src/color.css @@ -1,5 +1,7 @@ :root { - --blue: #3692FF; + --blue100: #3692FF; + --blue200: #1967D6; + --blue300: #1251AA; --red: #F74747; --gray900: #111827; --gray800: #1F2937; diff --git a/src/components/AuthInput.css b/src/components/AuthInput.css new file mode 100644 index 00000000..4b6822fb --- /dev/null +++ b/src/components/AuthInput.css @@ -0,0 +1,40 @@ +.auth-input-box * { + border: none; +} + +.auth-input-box input { + margin-bottom: 24px; +} + +.auth-input-box input.wrong { + margin-bottom: 8px; + border: solid 1px var(--red); +} + +.auth-input-box input.correct { + border: solid 1px var(--blue100); +} + +.auth-input-box .wrong-message { + margin: 8px 16px 18px; + color: var(--red); + font-weight: 600; + font-size: 14px; + line-height: 24px; +} + +.auth-input-box.password { + position: relative; +} + +.auth-input-box.password .eye-btn { + width: 24px; + padding: 0; + position: absolute; + top: 58px; + right: 24px; +} + +.auth-input-box.password .eye-btn img { + width: 100%; +} \ No newline at end of file diff --git a/src/components/AuthInput.jsx b/src/components/AuthInput.jsx new file mode 100644 index 00000000..954e1222 --- /dev/null +++ b/src/components/AuthInput.jsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import Input from "./Input"; +import icEyeVisible from "../assets/ic_eye_visible.svg"; +import icEyeInvisible from "../assets/ic_eye_invisible.svg"; +import "./AuthInput.css"; + +function PasswordInput ({ name, className, onFocusout, valid, wrongMessage, ...props }) { + const [isVisible, setIsVisible] = useState(false); + const onClick = () => { + setIsVisible((prev) => !prev); + } + const passwordMatch = () => { + try { + const password = document.querySelector("input#password"); + const passwordCheck = document.querySelector("input#password-check"); + if (password.value === passwordCheck.value) { + passwordCheck.classList.add("correct"); + passwordCheck.classList.remove("wrong"); + passwordCheck.nextElementSibling.nextElementSibling.textContent = null; + } else if (passwordCheck.value) { + passwordCheck.classList.add("wrong"); + passwordCheck.classList.remove("correct"); + passwordCheck.nextElementSibling.nextElementSibling.textContent = "비밀번호가 일치하지 않습니다."; + } + } catch (e) {} + } + + return ( + + +
{wrongMessage}
+ + ); +} + +export default function AuthInput ({ label, name, type="text", placeholder='', emptyWrongMessage='', invalidWrongMessage='' }) { + const [valid, setValid] = useState(''); + const [wrongMessage, setWrongMessage] = useState(null); + + const onFocusout = (e) => { + if (!e.target.value) { + setValid("wrong"); + setWrongMessage(emptyWrongMessage); + } else if (!e.target.validity.valid) { + setValid("wrong"); + setWrongMessage(invalidWrongMessage); + } else { + setValid("correct"); + setWrongMessage(null); + } + } + + if (type === "password") { + return (); + } + return ( +
{wrongMessage}
+ ); +} \ No newline at end of file diff --git a/src/components/Button.jsx b/src/components/Button.jsx new file mode 100644 index 00000000..917d8585 --- /dev/null +++ b/src/components/Button.jsx @@ -0,0 +1,19 @@ +import { useNavigate } from "react-router-dom"; +import styles from "./Button.module.css"; + +//styleType은 small, medium, large로 나뉜다 +function Button ({ styleType="small", className, onClick, to, children, ...props }) { + const navigate = useNavigate(); + const handleClick = (e) => { + if (onClick) onClick(e); + if (to) setTimeout(() => navigate(to), 0); + } + + return ( + + ); +} + +export default Button; \ No newline at end of file diff --git a/src/components/Button.module.css b/src/components/Button.module.css new file mode 100644 index 00000000..85d59458 --- /dev/null +++ b/src/components/Button.module.css @@ -0,0 +1,39 @@ +.btn { + padding: 0; + border: none; + background-color: var(--blue100); + color: var(--gray100); + font-weight: 600; + text-align: center; + align-content: center; +} + +.btn:hover { + background-color: var(--blue200); +} + +.btn:active { + background-color: var(--blue300); +} + +.btn:disabled { + background-color: var(--gray400); +} + +.btn.small { + border-radius: 8px; + font-size: 16px; + line-height: 26px; +} + +.btn.medium { + border-radius: 9999px; + font-size: 18px; + line-height: 26px; +} + +.btn.large { + border-radius: 9999px; + font-size: 20px; + line-height: 32px; +} \ No newline at end of file diff --git a/src/components/Dropdown.jsx b/src/components/Dropdown.jsx index e369af27..74297635 100644 --- a/src/components/Dropdown.jsx +++ b/src/components/Dropdown.jsx @@ -1,26 +1,36 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import icArrowDown from '../assets/ic_arrow_down.svg'; import icSort from '../assets/ic_sort.svg'; import styles from './Dropdown.module.css'; -const option = { favorite: "좋아요순", recent: "최신순" } +const option = { recent: "최신순", favorite: "좋아요순", } function Dropdown({ state, setState, mode }) { const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) setIsOpen(false); + }; + document.addEventListener("click", handleClickOutside); + return () => document.removeEventListener("click", handleClickOutside);; + }, []); return ( ); diff --git a/src/components/Dropdown.module.css b/src/components/Dropdown.module.css index 7a903704..98117e5b 100644 --- a/src/components/Dropdown.module.css +++ b/src/components/Dropdown.module.css @@ -29,19 +29,23 @@ border: 1px solid var(--gray200); border-radius: 12px; background-color: white; - color: var(--gray800); - font-weight: 400; - font-size: 16px; - line-height: 26px; } -.dropdownList li { +.dropdownList li input { + display: block; cursor: pointer; - height: 42px; - padding-top: 9px; + width: 100%; + height: 41px; + padding: 8px 0 6px; + border: none; border-bottom: 1px solid var(--gray200); + background-color: transparent; + color: var(--gray800); + font-weight: 400; + font-size: 16px; + line-height: 26px; } -.dropdownList li:last-child { +.dropdownList li:last-child input { border: none; } \ No newline at end of file diff --git a/src/components/Input.jsx b/src/components/Input.jsx index 448d85c7..e0e836b2 100644 --- a/src/components/Input.jsx +++ b/src/components/Input.jsx @@ -1,88 +1,13 @@ -import { useState } from "react"; -import icEyeVisible from "../assets/ic_eye_visible.svg"; -import icEyeInvisible from "../assets/ic_eye_invisible.svg"; +import styles from "./Input.module.css"; -function NormalInput ({ name, type, placeholder, onFocusout, valid, wrongMessage }) { - return (<> - -
{wrongMessage}
- ) -} - -function PasswordInput ({ name, placeholder, onFocusout, valid, wrongMessage }) { - const [isVisible, setIsVisible] = useState(false); - const onClick = () => { - setIsVisible((prev) => !prev); - } - const passwordMatch = () => { - try { - const password = document.querySelector("input#password"); - const passwordCheck = document.querySelector("input#password-check"); - if (password.value === passwordCheck.value) { - passwordCheck.classList.add("correct"); - passwordCheck.classList.remove("wrong"); - passwordCheck.nextElementSibling.textContent = null; - } else if (passwordCheck.value) { - passwordCheck.classList.add("wrong"); - passwordCheck.classList.remove("correct"); - passwordCheck.nextElementSibling.textContent = "비밀번호가 일치하지 않습니다."; - } - } catch (e) {} - } - - if (name === "password-check") { - return (
- -
- -
); - } - return (
- -
{wrongMessage}
- +function Input ({ label, name, className, inputClassName, type, children, ...props }) { + return (
+ + {type === "textarea" + ?