From 3b78d11138d72729c89c29bb20db1f8ad77b8767 Mon Sep 17 00:00:00 2001 From: s202055564 Date: Tue, 1 Jul 2025 17:00:20 +0900 Subject: [PATCH 1/3] =?UTF-8?q?1=EB=8B=A8=EA=B3=84=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1단계 내용 가져오기 --- .prettierrc | 8 + README.md | 26 +- index.html | 2 +- package-lock.json | 516 +++++++++++++++++++++++++++++++-- package.json | 11 +- src/App.css | 42 --- src/App.tsx | 50 ++-- src/component/Banner.tsx | 44 +++ src/component/FriendChoise.tsx | 61 ++++ src/component/GiftRanking.tsx | 273 +++++++++++++++++ src/component/GiftTheme.tsx | 65 +++++ src/component/Navbar.tsx | 91 ++++++ src/index.css | 68 ----- src/mock/Gift.ts | 17 ++ src/mock/GiftCategory.ts | 92 ++++++ src/page/Login.tsx | 101 +++++++ src/page/Main.tsx | 28 ++ src/page/Notfound.tsx | 86 ++++++ src/styles/global.tsx | 69 +++++ src/styles/reset.ts | 9 + src/styles/theme.ts | 193 ++++++++++++ tsconfig.app.json | 9 +- tsconfig.json | 10 +- vite.config.ts | 7 +- 24 files changed, 1699 insertions(+), 179 deletions(-) create mode 100644 .prettierrc create mode 100644 src/component/Banner.tsx create mode 100644 src/component/FriendChoise.tsx create mode 100644 src/component/GiftRanking.tsx create mode 100644 src/component/GiftTheme.tsx create mode 100644 src/component/Navbar.tsx create mode 100644 src/mock/Gift.ts create mode 100644 src/mock/GiftCategory.ts create mode 100644 src/page/Login.tsx create mode 100644 src/page/Main.tsx create mode 100644 src/page/Notfound.tsx create mode 100644 src/styles/global.tsx create mode 100644 src/styles/reset.ts create mode 100644 src/styles/theme.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..ddb778b3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 80, + "trailingComma": "es5", + "tabWidth": 2, + "endOfLine": "lf" +} \ No newline at end of file diff --git a/README.md b/README.md index d710c238..28bb19e5 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# react-gift-order \ No newline at end of file +# react-gift-login + +선물하기 서비스의 로그인/홈 화면을 구성하는 React 프로젝트입니다. + +## 프로젝트 설명 + +이 프로젝트는 Vite + React + TypeScript 환경에서 로그인/홈 화면을 구현하고, 코드 스타일 및 프로젝트 구조를 정리하는 과제를 수행하기 위해 만들어졌습니다. + +## 구현 기능 목록 + +- [ ] 프로젝트 초기 세팅 + - [ ] Vite + React + TypeScript 세팅 확인 + - [ ] 불필요한 파일/코드 제거 + - [ ] 절대경로(alias) 설정 + - [ ] Prettier 설치 및 설정 + - [ ] Emotion 설치 및 reset css 적용 + - [ ] Pretendard 폰트 적용 +- [ ] 공통 레이아웃 구성 + - [ ] 페이지 전환 구조 설계 (React Router 등) +- [ ] 로그인 화면 + - [ ] 이메일/비밀번호 입력 폼 + - [ ] 로그인 버튼 + - [ ] 로그인 유효성 검사 +- [ ] 홈 화면 + - [ ] 간단한 사용자 환영 메시지 또는 카드 렌더링 diff --git a/index.html b/index.html index e4b78eae..c246a0cb 100644 --- a/index.html +++ b/index.html @@ -2,8 +2,8 @@ - + Vite + React + TS diff --git a/package-lock.json b/package-lock.json index 92fa8e98..d8822c65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,17 @@ "name": "react-gift-login", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + + "@emotion/styled": "^11.14.1", + "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { "@eslint/js": "^9.25.0", + "@types/node": "^24.0.4", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", @@ -20,12 +26,10 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", + "prettier": "^3.6.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5" - }, - "engines": { - "node": ">=22.0.0" } }, "node_modules/@ampproject/remapping": { @@ -46,7 +50,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -102,7 +105,6 @@ "version": "7.27.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.27.5", @@ -136,7 +138,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -178,7 +179,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -188,7 +188,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -222,7 +221,6 @@ "version": "7.27.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -266,11 +264,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -285,7 +291,6 @@ "version": "7.27.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -304,7 +309,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -314,7 +318,6 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -324,6 +327,160 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", @@ -986,7 +1143,6 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -1001,7 +1157,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1011,7 +1166,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1021,14 +1175,12 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1419,6 +1571,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", @@ -1793,6 +1961,21 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1861,7 +2044,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1939,6 +2121,42 @@ "dev": true, "license": "MIT" }, + + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1958,14 +2176,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1993,6 +2209,15 @@ "dev": true, "license": "ISC" }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -2048,7 +2273,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2322,6 +2546,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2375,6 +2605,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2428,6 +2667,27 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2442,7 +2702,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2465,6 +2724,27 @@ "node": ">=0.8.19" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2509,7 +2789,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": { @@ -2529,7 +2808,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2545,6 +2823,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2596,6 +2880,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2670,7 +2960,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2760,7 +3049,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -2769,6 +3057,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2789,11 +3095,25 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2848,6 +3168,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.0.tgz", + "integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2900,6 +3236,12 @@ "react": "^19.1.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2910,11 +3252,70 @@ "node": ">=0.10.0" } }, + + "node_modules/react-router": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz", + "integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz", + "integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==", + "license": "MIT", + "dependencies": { + "react-router": "7.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3011,6 +3412,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3034,6 +3441,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3057,6 +3473,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3070,6 +3492,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -3191,6 +3625,13 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -3368,6 +3809,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index f0a1981f..ee0a0422 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,6 @@ "private": true, "version": "0.0.0", "type": "module", - "engines": { - "node": ">=22.0.0" - }, "scripts": { "dev": "vite", "build": "tsc -b && vite build", @@ -13,11 +10,16 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { "@eslint/js": "^9.25.0", + "@types/node": "^24.0.4", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", @@ -25,6 +27,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", + "prettier": "^3.6.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5" diff --git a/src/App.css b/src/App.css index b9d355df..e69de29b 100644 --- a/src/App.css +++ b/src/App.css @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx index 3d7ded3f..4c9dd7c3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,35 +1,27 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import GlobalStyle from '@/styles/global' +import {ThemeProvider} from "@emotion/react"; +import theme from './styles/theme' +import Main from './page/Main'; +import Navbar from './component/Navbar'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Login from './page/Login'; +import Notfound from './page/Notfound'; function App() { - const [count, setCount] = useState(0) - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- + + + + + + } /> + } /> + } /> + + + + ) } -export default App +export default App \ No newline at end of file diff --git a/src/component/Banner.tsx b/src/component/Banner.tsx new file mode 100644 index 00000000..2d333494 --- /dev/null +++ b/src/component/Banner.tsx @@ -0,0 +1,44 @@ +import theme from '@/styles/theme'; +import styled from '@emotion/styled'; + +const BannerSection = styled.section` + padding: 0px 16px; +`; + +const BannerTextWrapper = styled.div` + width: 100%; + border-radius: 1rem; + background-color: ${theme.colors.yellow600}; + padding: 16px; + display: flex; + flex-direction: column; +`; + +const BannerSub = styled.p` + font-size: 0.75rem; + font-weight: 400; + line-height: 1rem; + color: ${theme.colors.text_sub}; + margin: 0px; + text-align: left; +`; +const BannerMain = styled.p` + font-size: 0.875rem; + font-weight: 700; + line-height: 1.1875rem; + color: ${theme.colors.text_default}; + margin: 0px; + text-align: left; +`; +const Banner = () => { + return ( + + + 카카오테크 캠퍼스 3기여러분 + 프론트엔드 2단계 과제 화이팅! 🎉 + + + ); +}; + +export default Banner \ No newline at end of file diff --git a/src/component/FriendChoise.tsx b/src/component/FriendChoise.tsx new file mode 100644 index 00000000..3ffbbd6f --- /dev/null +++ b/src/component/FriendChoise.tsx @@ -0,0 +1,61 @@ +import theme from '@/styles/theme' +import styled from '@emotion/styled' +import React from 'react' + +const MySection = styled.section` + width: 100%; + padding: 16px 12px; + background-color: rgb(243, 244, 245); +` + +const MyButtom = styled.button` + width: 100%; + background-color: rgb(255, 255, 255); + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + padding: 16px; + border-radius: 18px; + cursor: pointer; +` + +const MyDiv = styled.div` + width: 2.625rem; + height: 2.625rem; + border-radius: 16px; + background-color: ${theme.colors.yellow600}; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +` + +const FriendChoise = () => { + return ( + + + + + +

선물할 친구를 선택해 주세요.

+
+
+ ) +} + +export default FriendChoise \ No newline at end of file diff --git a/src/component/GiftRanking.tsx b/src/component/GiftRanking.tsx new file mode 100644 index 00000000..28bcd5e9 --- /dev/null +++ b/src/component/GiftRanking.tsx @@ -0,0 +1,273 @@ +import { Gift } from '@/mock/Gift'; +import styled from '@emotion/styled'; +import { useState } from 'react'; + + + +const GIFTLENGTH = 6; + +const enum PeopleType{ + ALL = 'ALL', + FEMALE = 'FEMALE', + MALE = 'MALE', + TEEN = 'TEEN', +} + + +const enum WishType{ + WANT = 'WANT', + MANY_GIFT = 'MANY_GIFT', + MANY_WISH = 'MANY_WISH', +} + +const GiftRanKingSection = styled.div` + padding: 0px 16px; + width: 100%; + +` + +const BlankSpace = styled.div` + width: 100%; + height: 16px; + background-color: transparent; +` + +const Title = styled.h3` + font-size: 1.25rem; + font-weight: 700; + line-height: 1.6875rem; + color: rgb(42, 48, 56); + margin: 0px; + width: 100%; + text-align: left; +` + +const CategoryGroup = styled.div` + border-radius: 1rem; + background-color: rgb(255, 255, 255); + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +` + +const PeopleGroup = styled.div` + width: 100%; + display: flex; + -webkit-box-pack: justify; + justify-content: space-between; + gap: 8px; +` + +const WishGroup = styled.div` + width: 100%; + display: flex; + justify-content: space-between; + border: 1px solid rgba(70, 132, 233, 0.1); + background-color: rgb(239, 246, 255); + border-radius: 0.5rem; + padding: 12px 16px; +` + +interface FilterButtonProps { + active: boolean; +} + +const FilterButton = styled.button` + width: 3.7rem; + height: 3.7rem; + padding: 10px 16px; + border: none; + border-radius: 20px; + font-size: 10px; + cursor: pointer; + background-color: ${({ active }) => (active ? 'rgb(33, 124, 249)' : 'rgb(239, 246, 255)')}; + color: ${({ active }) => (active ? 'white' : '#333')}; + transition: background-color 0.2s; +` + +const FilterButton2 = styled.button` + padding: 10px 16px; + font-size: 12px; + cursor: pointer; + background-color: rgb(239, 246, 255); + color: ${({ active }) => (active ? 'rgb(33, 124, 249)' : 'rgb(133, 184, 253);')}; + transition: background-color 0.2s; +` + +const Label = styled.p` + margin: 4px 0 0; + font-size: 10px; + text-align: center; + white-space: nowrap; +` + +const IconWrapper = styled.div` + font-size: 12px; + text-align: center; +` + +const ProductDiv = styled.div` + width: 100%; +` + +const ProductGrid = styled.div` + width: 100%; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px 8px; +` + +const ProductCard = styled.div` + text-align: center; + overflow: hidden; + border-radius: 8px; +` + +const ProductImage = styled.img` + width: 100%; + aspect-ratio: 1; + object-fit: cover; + object-position: center; + border-radius: 4px; +` + +const BrandImage = styled.img` + width: 32px; + height: 32px; + margin: 4px auto; +` + +const Price = styled.p` + font-weight: bold; +` + +const LoadMoreButtonDiv = styled.button` + width: 100%; + display: flex; + -webkit-box-pack: center; + justify-content: center; + background-color: white; +` +const LoadMoreButton = styled.button` + max-width: 30rem; + width: 100%; + padding: 12px; + border-radius: 4px; + border: 1px solid rgb(220, 222, 227); +` + + +const GiftRanking = () => { + + const [isExpanded, setIsExpanded] = useState(false); + + + + const [peopleType, setPeopleType] = useState(PeopleType.ALL); + const [wishType, setWishType] = useState(WishType.WANT); + + const handlePeopleClick = (type: PeopleType) => { + setPeopleType(type); + }; + + const handleWishClick = (type: WishType) => { + setWishType(type); + }; + + + + + const GiftList = Array.from({ length: 12 }, (_, i) => ({ + ...Gift, + id: i + 1, + })); + + + const visibleCount = isExpanded ? GiftList.length : GIFTLENGTH; + + const shownProducts = GiftList.slice(0, visibleCount); + + + return ( + + + 실시간 급상승 선물랭킹 + + + + handlePeopleClick(PeopleType.ALL)}> + ALL + + + handlePeopleClick(PeopleType.FEMALE)}> + 👩🏻 + + + handlePeopleClick(PeopleType.MALE)}> + 👨🏻 + + + handlePeopleClick(PeopleType.TEEN)}> + 👦🏻 + + + + + + + handleWishClick(WishType.WANT)}> + 받고 싶어한 + + handleWishClick(WishType.MANY_GIFT)}> + 많이 선물한 + + handleWishClick(WishType.MANY_WISH)}> + 위시로 받은 + + + + + + + {shownProducts.map((item) => ( + + + +

+ {item.name} +

+ {item.price.sellingPrice.toLocaleString()} 원 +
+ ))} +
+
+ + + + {GiftList.length > GIFTLENGTH && ( + setIsExpanded((prev) => !prev)}> + {isExpanded ? '접기' : '더보기'} + + )} + + + + +
+ + + ) +} + + +export default GiftRanking diff --git a/src/component/GiftTheme.tsx b/src/component/GiftTheme.tsx new file mode 100644 index 00000000..b6e0037b --- /dev/null +++ b/src/component/GiftTheme.tsx @@ -0,0 +1,65 @@ +import { GiftCategory } from '@/mock/GiftCategory'; +import styled from '@emotion/styled'; +import React from 'react' + +const GiftThemeSection = styled.section` + padding: 1rem; +`; + +const Title = styled.h3` + padding: 0px 8px 20px; +`; + +const ThemeGrid = styled.div` + width: 100%; + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 20px 4px; +`; + +const ThemeItem = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.25rem; + cursor: pointer; +`; + +const ThemeImage = styled.img` + max-width: 3.125rem; + max-height: 3.125rem; + width: 100%; + border-radius: 18px; + object-fit: cover; + overflow: hidden; +`; + +const ThemeLabel = styled.p` + font-size: 0.75rem; + font-weight: 400; + line-height: 1rem; + color: rgb(42, 48, 56); + margin: 0px; + text-align: left; +`; + +const GiftTheme = () => { + return ( + + 선물 테마 + + {GiftCategory.map(({ themeId, name, image }) => ( + + + {name} + + ))} + + + ) +} + +export default GiftTheme \ No newline at end of file diff --git a/src/component/Navbar.tsx b/src/component/Navbar.tsx new file mode 100644 index 00000000..a384039f --- /dev/null +++ b/src/component/Navbar.tsx @@ -0,0 +1,91 @@ +import styled from '@emotion/styled'; +import { useNavigate } from 'react-router-dom'; + +const NavContaier = styled.div` + position: fixed; + top: 0px; + left: 0px; + right: 0px; + margin: 0px auto; + max-width: 720px; + background-color: rgb(255, 255, 255); + z-index: 1000; +` + +const MyNav =styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + height: 2.75rem; + padding-left: 8px; + padding-right: 8px; + background-color: rgb(255, 255, 255); + gap: 4px; +` +const LogoImg =styled.img` + height: 2.75rem; +` +const MySvg = styled.svg` + width: 100; + height: 100; + fill: none; + stroke: black; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +` +const MyButton = styled.button` + background: none; +` + + + +const Navbar = () => { + + + const navigate = useNavigate(); + + return ( + + +
+ navigate(-1)} aria-label="뒤로 가기"> + + +
+
+ + + +
+
+ navigate('/Login')}> + + +
+
+
+ ); +}; + +export default Navbar; diff --git a/src/index.css b/src/index.css index 08a3ac9e..e69de29b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,68 +0,0 @@ -: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/mock/Gift.ts b/src/mock/Gift.ts new file mode 100644 index 00000000..61400d4d --- /dev/null +++ b/src/mock/Gift.ts @@ -0,0 +1,17 @@ +export const Gift = { + id: 123, + name: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', + imageURL: + 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', + price: { + basicPrice: 29000, + discountRate: 0, + sellingPrice: 29000, + }, + brandInfo: { + id: 2088, + name: 'BBQ', + imageURL: + 'https://st.kakaocdn.net/product/gift/gift_brand/20220216170226_38ba26d8eedf450683200d6730757204.png', + }, +}; diff --git a/src/mock/GiftCategory.ts b/src/mock/GiftCategory.ts new file mode 100644 index 00000000..d712d2ca --- /dev/null +++ b/src/mock/GiftCategory.ts @@ -0,0 +1,92 @@ +export const GiftCategory = [ + { + themeId: 3715, + name: '생일', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F371520241125_LQBMT.png', + }, + { + themeId: 3714, + name: '맛있는선물', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F371420250401_CYHOR.png', + }, + { + themeId: 3713, + name: '직장동료', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F371320250107_QWGZN.png', + }, + { + themeId: 3712, + name: '연인', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F371220250107_YMYGC.png', + }, + { + themeId: 3993, + name: 'FOR ME', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F399320250519_CMTFF.png', + }, + { + themeId: 3710, + name: '가벼운선물', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F371020250102_QSNFV.png', + }, + { + themeId: 3782, + name: '스몰럭셔리', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F378220250214_OHAQK.png', + }, + { + themeId: 3877, + name: '명품선물', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F387720250324_SDCJQ.png', + }, + { + themeId: 3707, + name: '출산・돌', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F370720241228_QFZPM.png', + }, + { + themeId: 3697, + name: '결혼・집들이', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F369720250126_OGWLG.png', + }, + { + themeId: 3704, + name: '시원한선물', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F370420250324_WDMHS.png', + }, + { + themeId: 3705, + name: '합격・응원', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F370520250127_NLVFN.png', + }, + { + themeId: 3706, + name: '건강・케어', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F370620250415_HENTO.png', + }, + { + themeId: 3703, + name: '교환권', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F370320250331_NPPCU.png', + }, + { + themeId: 3702, + name: '웃긴선물', + image: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F370220241228_UPSAE.png', + }, +]; diff --git a/src/page/Login.tsx b/src/page/Login.tsx new file mode 100644 index 00000000..f8263683 --- /dev/null +++ b/src/page/Login.tsx @@ -0,0 +1,101 @@ +import styled from '@emotion/styled'; +import { useNavigate } from 'react-router-dom'; + +const MyDiv = styled.div` + max-width: 720px; + width: 100%; + min-height: 100vh; + height: 100%; + background-color: rgb(255, 255, 255); + padding-top: 2.75rem; +`; + +const LoginMain = styled.main` + width: 100%; + height: calc(-2.75rem + 100vh); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const KakaoLogo = styled.img` + width: 5.5rem; + color: rgb(42, 48, 56); +`; +const LoginSection = styled.section` + width: 100%; + max-width: 26.25rem; + padding: 16px; +`; + +const InputSection = styled.input` + width: 100%; + box-sizing: border-box; + color: rgb(42, 48, 56); + transition: border-color 200ms; + border-style: solid; + min-height: 2.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.375rem; + padding: 8px 0px; + border-width: 0px 0px 1px; + border-color: rgb(220, 222, 227); +`; + +const LoginButton = styled.button` + width: 100%; + height: 2.75rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.1875rem; + color: rgb(42, 48, 56); + background-color: rgb(254, 229, 0); + border-radius: 4px; + border: none; + cursor: pointer; + transition: background-color 200ms; +`; + +const EmptyDiv1 = styled.div` + width: 100%; + height: 16px; + background-color: transparent; +`; + +const EmptyDiv2 = styled.div` + width: 100%; + height: 48px; + background-color: transparent; +`; + +const Login = () => { + + const navigate = useNavigate(); + + return ( + + + + + +
+ +
+ +
+ +
+ + navigate('/')}>로그인 +
+
+
+ ); +}; + +export default Login; diff --git a/src/page/Main.tsx b/src/page/Main.tsx new file mode 100644 index 00000000..d892d607 --- /dev/null +++ b/src/page/Main.tsx @@ -0,0 +1,28 @@ +import Banner from '@/component/Banner' +import FriendChoise from '@/component/FriendChoise' +import GiftRanking from '@/component/GiftRanking' +import GiftTheme from '@/component/GiftTheme' + +import styled from '@emotion/styled' + +const MyDiv = styled.div` + max-width: 720px; + width: 100%; + min-height: 100vh; + height: 100%; + background-color: rgb(255, 255, 255); + padding-top: 2.75rem; +` + +const main = () => { + return ( + + + + + + + ) +} + +export default main diff --git a/src/page/Notfound.tsx b/src/page/Notfound.tsx new file mode 100644 index 00000000..e238fe23 --- /dev/null +++ b/src/page/Notfound.tsx @@ -0,0 +1,86 @@ +import theme from '@/styles/theme' +import styled from '@emotion/styled' +import { useNavigate } from 'react-router-dom' + + +const MYDiv = styled.div` + max-width: 720px; + width: 100%; + min-height: 100vh; + height: 100%; + background-color: rgb(255, 255, 255); + padding-top: 2.75rem; +` +const NotFoundmain = styled.main` + width: 100%; + height: calc(-2.75rem + 100vh); + background-color: rgb(243, 244, 245); + display: flex; + flex-direction: column; + -webkit-box-align: center; + align-items: center; + padding: 5rem 1.25rem; +` + +const NotFoundImg = styled.img` + width: 150px; + height: 150px; + object-fit: cover; +` + +const Myh3 = styled.h3` + font-size: 1.25rem; + font-weight: 700; + line-height: 1.6875rem; + color: ${theme.colors.text_default}; + margin: 0px; + text-align: left; + padding-top: 28px; + +` + +const MyP = styled.p` + font-size: 1rem; + font-weight: 400; + line-height: 1.375rem; + color: ${theme.colors.text_sub}; + margin: 0px; + text-align: left; + padding: 8px 0px 48px 0px; +` + +const HomeButton = styled.button` + width: 160px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background-color: ${theme.colors.kakaoYellow}; + color: ${theme.colors.text_default}; + font-size: 1rem; + font-weight: 400; + line-height: 1.375rem; + cursor: pointer; +` + + + +const Notfound = () => { + + const navigate = useNavigate(); + return ( + + + + 잘못된 접근입니다. + 찾으시는 페이지가 존재하지 않습니다. + navigate('/')}>홈으로 + + + ) +} + +export default Notfound diff --git a/src/styles/global.tsx b/src/styles/global.tsx new file mode 100644 index 00000000..62f25dbc --- /dev/null +++ b/src/styles/global.tsx @@ -0,0 +1,69 @@ + + +import { Global, css } from '@emotion/react' +import reset from './reset' + +export const globalStyles = css` + ${reset}; + + body { + background: #fff; + font-family: sans-serif; + color: #222; + } + + #root { + width: 100%; + min-height: 100vh; + height: 100%; + display: flex +; + flex-direction: column; + align-items: center; + justify-content: flex-start; + background-color: rgb(247, 248, 249); + } + + img, picture, video, canvas, svg { + display: block; + height: auto; + } + + + *, *:before, *:after { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + box-sizing: border-box; + } + + @font-face { + font-family: 'Ownglyph_ParkDaHyun'; + src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/2411-3@1.0/Ownglyph_ParkDaHyun.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + } + + @font-face { + font-family: 'Pretendard-Regular'; + src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff'); + font-weight: 400; + font-style: normal; + } + + + html, body { + font-family: 'Pretendard-Regular', sans-serif; + } + + + +`; + +const GlobalStyle = () => ; + +export default GlobalStyle; + diff --git a/src/styles/reset.ts b/src/styles/reset.ts new file mode 100644 index 00000000..809352e2 --- /dev/null +++ b/src/styles/reset.ts @@ -0,0 +1,9 @@ +// src/styles/reset.ts +import { css } from '@emotion/react' + +const reset = css` + /* Reset CSS */ + +` + +export default reset diff --git a/src/styles/theme.ts b/src/styles/theme.ts new file mode 100644 index 00000000..5818e9ef --- /dev/null +++ b/src/styles/theme.ts @@ -0,0 +1,193 @@ + +const theme = { + colors: { + gray00 : '#ffffff',// 흰색 + gray100 : '#f7f8f9', //가장 밝은 회색 + gray200 : '#f3f4f5', //매우 밝은 회색 + gray300 : '#eeeff1', //밝은 회색 + gray400 : '#dcdee3', //연한 회색 + gray500 : '#d1d3d8', //중간 밝은 회색 + gray600 : '#b0b3ba', //중간 회색 + gray700 : '#868b94', //진한 회색 + gray800 : '#555d6d', //매우 진한 회색 + gray900 : '#2a3038', //거의 검은색 + gray1000 : '#1a1c20',// 검은색 + + //Yellow 계열 (카카오 브랜드 컬러) + yellow00 : ' #fffef9', //가장 밝은 노란색 + yellow100 : ' #fffce5', //매우 밝은 노란색 + yellow200 : ' #fff8b7', //밝은 노란색 + yellow300 : ' #fff38a', //연한 노란색 + yellow400 : ' #ffef5c', //중간 밝은 노란색 + yellow500 : ' #ffea2e', //중간 노란색 + yellow600 : ' #fee500', //카카오 메인 노란색 + yellow700 : ' #d5c000', //진한 노란색 + yellow800 : ' #ac9b00', //매우 진한 노란색 + yellow900 : ' #847700', //어두운 노란색 + yellow1000 :' #5b5200', //가장 어두운 노란색 + //Brown 계열 + brown00 : ' #fff9f4', //가장 밝은 갈색 + brown100 : ' #ffeedc', //매우 밝은 갈색 + brown200 : ' #ffe2c4', //밝은 갈색 + brown300 : ' #f9d0a8', //연한 갈색 + brown400 : ' #edbc8a', //중간 밝은 갈색 + brown500 : ' #cb9a69', //중간 갈색 + brown600 : ' #a97b4d', //진한 갈색 + brown700 : ' #875e35', //매우 진한 갈색 + brown800 : ' #654321', //카카오 브라운 + brown900 : ' #432a12', //어두운 갈색 + brown1000 : ' #2d1b08', //가장 어두운 갈색 + //Blue 계열 + blue00 : ' #f8faff', // 가장 밝은 파란색 + blue100 : ' #eff6ff', // 매우 밝은 파란색 + blue200 : ' #e2edfc', // 밝은 파란색 + blue300 : ' #cbdffa', // 연한 파란색 + blue400 : ' #aacefd', // 중간 밝은 파란색 + blue500 : ' #85b8fd', // 중간 파란색 + blue600 : ' #5e98fe', // 진한 파란색 + blue700 : ' #217cf9', // 매우 진한 파란색 + blue800 : ' #135fcd', // 어두운 파란색 + blue900 : ' #0b4596', // 매우 어두운 파란색 + blue1000 : ' #032451', // 가장 어두운 파란색 + //Red 계열 + red00 : ' #fffafa', // 가장 밝은 빨간색 + red100 : ' #fdf0f0', // 매우 밝은 빨간색 + red200 : ' #fde7e7', // 밝은 빨간색 + red300 : ' #fed4d2', // 연한 빨간색 + red400 : ' #feb7b3', // 중간 밝은 빨간색 + red500 : ' #fe928d', // 중간 빨간색 + red600 : ' #fc6a66', // 진한 빨간색 + red700 : ' #fa342c', // 매우 진한 빨간색 + red800 : ' #ca1d13', // 어두운 빨간색 + red900 : ' #921708', // 매우 어두운 빨간색 + red1000 : ' #4a1209', // 가장 어두운 빨간색 + //시맨틱 컬러 (Semantic Colors) + + kakaoYellow : ' #fee500', // 카카오 메인 노란색 + kakaoYellowHover : ' #ffea2e', // 카카오 노란색 호버 상태 + kakaoYellowActive : ' #d5c000', // 카카오 노란색 활성 상태 + kakaoYellowPressed : ' #d5c000', // 카카오 노란색 눌림 상태 + kakaoBrown : ' #654321', // 카카오 갈색 + kakaoBrownPressed : ' #432a12', // 카카오 갈색 눌림 상태 + //배경 컬러 + back_default : ' #ffffff', // 기본 배경색 + back_disabled : ' #f3f4f5', // 비활성 배경색 + fill : ' #f7f8f9', // 채움 배경색 + //텍스트 컬러 + text_default : ' #2a3038', // 기본 텍스트 색상 + text_sub : ' #b0b3ba', // 보조 텍스트 색상 + text_disabled : ' #dcdee3', // 비활성 텍스트 색상 + placeholder : ' #b0b3ba', // 플레이스홀더 텍스트 색상 + //테두리 컬러 + border_default : ' #dcdee3', // 기본 테두리 색상 + border_disabled : ' #eeeff1', // 비활성 테두리 색상 + //상태 컬러 + critical : ' #fa342c', // 경고/에러 색상 + criticalBackground : ' #fdf0f0', // 경고/에러 배경 색상 + info : ' #217cf9', // 정보 색상 + infoBackground : ' #eff6ff', // 정보 배경 색상 + }, + + title1Bold: { + fontSize: '1.25rem', // 20px + fontWeight: 700, + lineHeight: '1.6875rem', // 27px + }, + + title1Regular: { + fontSize: '1.25rem', + fontWeight: 400, + lineHeight: '1.6875rem', + }, + title2Bold: { + fontSize: '1rem', // 16px + fontWeight: 700, + lineHeight: '1.5rem', // 24px + }, + title2Regular: { + fontSize: '1rem', + fontWeight: 400, + lineHeight: '1.5rem', + }, + subtitle1Bold: { + fontSize: '1rem', + fontWeight: 700, + lineHeight: '1.375rem', // 22px + }, + subtitle1Regular: { + fontSize: '1rem', + fontWeight: 400, + lineHeight: '1.5rem', + }, + subtitle2Bold: { + fontSize: '0.875rem', // 14px + fontWeight: 700, + lineHeight: '1.1875rem', // 19px + }, + subtitle2Regular: { + fontSize: '0.875rem', + fontWeight: 400, + lineHeight: '1.1875rem', + }, + body1Bold: { + fontSize: '1rem', + fontWeight: 700, + lineHeight: '1.375rem', + }, + body1Regular: { + fontSize: '1rem', + fontWeight: 400, + lineHeight: '1.375rem', + }, + body2Bold: { + fontSize: '0.875rem', + fontWeight: 700, + lineHeight: '1.1875rem', + }, + body2Regular: { + fontSize: '0.875rem', + fontWeight: 400, + lineHeight: '1.1875rem', + }, + label1Bold: { + fontSize: '0.875rem', + fontWeight: 700, + lineHeight: '1.1875rem', + }, + label1Regular: { + fontSize: '0.875rem', + fontWeight: 400, + lineHeight: '1.1875rem', + }, + label2Bold: { + fontSize: '0.75rem', // 12px + fontWeight: 700, + lineHeight: '1rem', // 16px + }, + label2Regular: { + fontSize: '0.75rem', + fontWeight: 400, + lineHeight: '1rem', + }, + spacing: { + spacing0: '0px', + spacing1: '4px', + spacing2: '8px', + spacing3: '12px', + spacing4: '16px', + spacing5: '20px', + spacing6: '24px', + spacing7: '28px', + spacing8: '32px', + spacing9: '36px', + spacing10: '40px', + spacing11: '44px', + spacing12: '48px', + spacing13: '52px', + spacing14: '56px', + spacing15: '60px', + spacing16: '64px', + }, +} + +export default theme diff --git a/tsconfig.app.json b/tsconfig.app.json index c9ccbd4c..46d7ea7b 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -19,9 +19,14 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, + "erasableSyntaxOnly": false, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } }, "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index 1ffef600..9f49755f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,15 @@ { + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "jsx": "react-jsx", + "jsxImportSource": "@emotion/react" + }, "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] -} +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b9..25d10a79 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,12 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import path from 'path' -// https://vite.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') + } + } }) From fa70f76b8dfef3f3f2f620e8b55029d78fd5ba13 Mon Sep 17 00:00:00 2001 From: s202055564 Date: Wed, 2 Jul 2025 19:53:18 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 커스텀 훅 Login.tsx 추가 이를 사용해서 로그인 에러 로직 구현 --- src/hook/useInput.ts | 29 ++++++++++++++++++++ src/page/Login.tsx | 64 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/hook/useInput.ts diff --git a/src/hook/useInput.ts b/src/hook/useInput.ts new file mode 100644 index 00000000..eadad422 --- /dev/null +++ b/src/hook/useInput.ts @@ -0,0 +1,29 @@ +import { useState } from 'react'; + +type ValidatorFn = (value: string) => string | null; + +function useInput(initialValue: string, validator: ValidatorFn) { + const [value, setValue] = useState(initialValue); + const [error, setError] = useState(null); + const [touched, setTouched] = useState(false); + + const onChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + if (touched) { + const errorMsg = validator(e.target.value); + setError(errorMsg); + } + }; + + const onBlur = () => { + setTouched(true); + const errorMsg = validator(value); + setError(errorMsg); + }; + + const isValid = !error && touched; + + return { value, onChange, onBlur, error, isValid }; +} + +export default useInput; diff --git a/src/page/Login.tsx b/src/page/Login.tsx index f8263683..b3819af7 100644 --- a/src/page/Login.tsx +++ b/src/page/Login.tsx @@ -1,3 +1,5 @@ +import useInput from '@/hook/useInput'; +import theme from '@/styles/theme'; import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; @@ -29,7 +31,7 @@ const LoginSection = styled.section` padding: 16px; `; -const InputSection = styled.input` +const InputSection = styled.input<{ hasError?: boolean }>` width: 100%; box-sizing: border-box; color: rgb(42, 48, 56); @@ -41,20 +43,21 @@ const InputSection = styled.input` line-height: 1.375rem; padding: 8px 0px; border-width: 0px 0px 1px; - border-color: rgb(220, 222, 227); + border-color: ${({ hasError }) => (hasError ? 'red' : 'rgb(220, 222, 227)')}; + `; -const LoginButton = styled.button` +const LoginButton = styled.button<{ hasError?: boolean }>` width: 100%; height: 2.75rem; font-size: 0.875rem; font-weight: 400; line-height: 1.1875rem; color: rgb(42, 48, 56); - background-color: rgb(254, 229, 0); + background-color: ${theme.colors.kakaoYellow}; border-radius: 4px; border: none; - cursor: pointer; + cursor: ${({hasError}) => (hasError? 'pointer' : 'not-allowed' ) }; transition: background-color 200ms; `; @@ -70,9 +73,35 @@ const EmptyDiv2 = styled.div` background-color: transparent; `; +const ErrorMessage = styled.div` + color: red; + font-size: 0.75rem; + margin-top: 4px; +`; + +const validateEmail = (value: string): string | null => { + if (!value) return 'ID를 입력해주세요.'; + // 간단한 이메일 형식 체크 정규식 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) return 'ID는 이메일 형식으로 입력해주세요.'; + return null; +}; + +// 비밀번호 유효성 검사 함수 +const validatePassword = (value: string): string | null => { + if (!value) return 'PW를 입력해주세요.'; + if (value.length < 8) return 'PW는 최소 8글자 이상이어야 합니다.'; + return null; +}; + const Login = () => { const navigate = useNavigate(); + const id = useInput('', validateEmail); + const pw = useInput('', validatePassword); + + // 두 조건 모두 충족해야 로그인 버튼 활성화 + const canSubmit = id.isValid && pw.isValid; return ( @@ -84,14 +113,33 @@ const Login = () => {
- + + {id.error && {id.error}}
- + + {pw.error && {pw.error}}
- navigate('/')}>로그인 + canSubmit && navigate('/')} + hasError={!!pw.error || !! id.error} + >로그인
From 512a2ebf9b1dae5ead9762640f86145c412d63d8 Mon Sep 17 00:00:00 2001 From: s202055564 Date: Thu, 3 Jul 2025 16:20:16 +0900 Subject: [PATCH 3/3] =?UTF-8?q?1=EB=8B=A8=EA=B3=84=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컴포넌트 분리 -Login.styled.ts 추가 -utils 폴더와 파일 추가 -초기값 설정 함수 분리 등 리펙토링 --- src/hook/useInput.ts | 2 +- src/page/Login.styled.ts | 85 ++++++++++++++++++++++++++++ src/page/Login.tsx | 108 +++--------------------------------- src/utils/validateInput.tsx | 12 ++++ 4 files changed, 107 insertions(+), 100 deletions(-) create mode 100644 src/page/Login.styled.ts create mode 100644 src/utils/validateInput.tsx diff --git a/src/hook/useInput.ts b/src/hook/useInput.ts index eadad422..9b99ee6b 100644 --- a/src/hook/useInput.ts +++ b/src/hook/useInput.ts @@ -2,7 +2,7 @@ import { useState } from 'react'; type ValidatorFn = (value: string) => string | null; -function useInput(initialValue: string, validator: ValidatorFn) { +function useInput(validator: ValidatorFn, initialValue ='') { const [value, setValue] = useState(initialValue); const [error, setError] = useState(null); const [touched, setTouched] = useState(false); diff --git a/src/page/Login.styled.ts b/src/page/Login.styled.ts new file mode 100644 index 00000000..4b471edb --- /dev/null +++ b/src/page/Login.styled.ts @@ -0,0 +1,85 @@ + +import theme from '@/styles/theme'; +import styled from '@emotion/styled'; + +export const MyDiv = styled.div` + max-width: 720px; + width: 100%; + min-height: 100vh; + height: 100%; + background-color: rgb(255, 255, 255); + padding-top: 2.75rem; +`; + +export const LoginMain = styled.main` + width: 100%; + height: calc(-2.75rem + 100vh); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +export const KakaoLogo = styled.img` + width: 5.5rem; + color: rgb(42, 48, 56); +`; +export const LoginSection = styled.section` + width: 100%; + max-width: 26.25rem; + padding: 16px; +`; + +export const InputSection = styled.input<{ hasError?: boolean }>` + width: 100%; + box-sizing: border-box; + color: rgb(42, 48, 56); + transition: border-color 200ms; + border-style: solid; + min-height: 2.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.375rem; + padding: 8px 0px; + border-width: 0px 0px 1px; + border-color: ${({ hasError }) => (hasError ? 'red' : 'rgb(220, 222, 227)')}; + &:focus { + outline: none; + border-color: ${({ hasError }) => hasError ? 'red' : 'rgb(42, 48, 56)'}; + } + +`; + +export const LoginButton = styled.button<{ notVaild?: boolean }>` + width: 100%; + height: 2.75rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.1875rem; + color: rgb(42, 48, 56); + background-color: ${theme.colors.kakaoYellow}; + border-radius: 4px; + border: none; + cursor: ${({notVaild}) => (notVaild? 'not-allowed' :'pointer' ) }; + opacity: ${({notVaild}) => (notVaild? '0.5' :'1' ) }; + transition: background-color 200ms; +`; + +export const EmptyDiv16h= styled.div` + width: 100%; + height: 16px; + background-color: transparent; +`; + +export const EmptyDiv48h = styled.div` + width: 100%; + height: 48px; + background-color: transparent; +`; + +export const ErrorMessage = styled.div` + color: red; + font-size: 0.75rem; + margin-top: 4px; +`; + diff --git a/src/page/Login.tsx b/src/page/Login.tsx index b3819af7..d0f19fd3 100644 --- a/src/page/Login.tsx +++ b/src/page/Login.tsx @@ -1,107 +1,17 @@ import useInput from '@/hook/useInput'; -import theme from '@/styles/theme'; -import styled from '@emotion/styled'; +import { validateEmail, validatePassword } from '@/utils/validateInput'; import { useNavigate } from 'react-router-dom'; +import { EmptyDiv16h, EmptyDiv48h, ErrorMessage, InputSection, KakaoLogo, LoginButton, LoginMain, LoginSection, MyDiv } from './Login.styled'; -const MyDiv = styled.div` - max-width: 720px; - width: 100%; - min-height: 100vh; - height: 100%; - background-color: rgb(255, 255, 255); - padding-top: 2.75rem; -`; - -const LoginMain = styled.main` - width: 100%; - height: calc(-2.75rem + 100vh); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -`; - -const KakaoLogo = styled.img` - width: 5.5rem; - color: rgb(42, 48, 56); -`; -const LoginSection = styled.section` - width: 100%; - max-width: 26.25rem; - padding: 16px; -`; - -const InputSection = styled.input<{ hasError?: boolean }>` - width: 100%; - box-sizing: border-box; - color: rgb(42, 48, 56); - transition: border-color 200ms; - border-style: solid; - min-height: 2.75rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.375rem; - padding: 8px 0px; - border-width: 0px 0px 1px; - border-color: ${({ hasError }) => (hasError ? 'red' : 'rgb(220, 222, 227)')}; - -`; - -const LoginButton = styled.button<{ hasError?: boolean }>` - width: 100%; - height: 2.75rem; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.1875rem; - color: rgb(42, 48, 56); - background-color: ${theme.colors.kakaoYellow}; - border-radius: 4px; - border: none; - cursor: ${({hasError}) => (hasError? 'pointer' : 'not-allowed' ) }; - transition: background-color 200ms; -`; - -const EmptyDiv1 = styled.div` - width: 100%; - height: 16px; - background-color: transparent; -`; - -const EmptyDiv2 = styled.div` - width: 100%; - height: 48px; - background-color: transparent; -`; - -const ErrorMessage = styled.div` - color: red; - font-size: 0.75rem; - margin-top: 4px; -`; - -const validateEmail = (value: string): string | null => { - if (!value) return 'ID를 입력해주세요.'; - // 간단한 이메일 형식 체크 정규식 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(value)) return 'ID는 이메일 형식으로 입력해주세요.'; - return null; -}; - -// 비밀번호 유효성 검사 함수 -const validatePassword = (value: string): string | null => { - if (!value) return 'PW를 입력해주세요.'; - if (value.length < 8) return 'PW는 최소 8글자 이상이어야 합니다.'; - return null; -}; const Login = () => { const navigate = useNavigate(); - const id = useInput('', validateEmail); - const pw = useInput('', validatePassword); + const id = useInput(validateEmail); + const pw = useInput(validatePassword); - // 두 조건 모두 충족해야 로그인 버튼 활성화 const canSubmit = id.isValid && pw.isValid; + const handleLoginClick = () =>{canSubmit && navigate('/')}; return ( @@ -122,7 +32,7 @@ const Login = () => { /> {id.error && {id.error}} - +
{ /> {pw.error && {pw.error}}
- + canSubmit && navigate('/')} - hasError={!!pw.error || !! id.error} + onClick={handleLoginClick} + notVaild={!canSubmit} >로그인 diff --git a/src/utils/validateInput.tsx b/src/utils/validateInput.tsx new file mode 100644 index 00000000..9ded11e0 --- /dev/null +++ b/src/utils/validateInput.tsx @@ -0,0 +1,12 @@ +export const validateEmail = (value: string): string | null => { + if (!value) return 'ID를 입력해주세요.'; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) return 'ID는 이메일 형식으로 입력해주세요.'; + return null; +}; + +export const validatePassword = (value: string): string | null => { + if (!value) return 'PW를 입력해주세요.'; + if (value.length < 8) return 'PW는 최소 8글자 이상이어야 합니다.'; + return null; +};