From 93b6e1279b74f5c732d217680f98034f81fe3f6b Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:25:39 +0900 Subject: [PATCH 01/47] =?UTF-8?q?=F0=9F=92=84[Design]=20=EC=A0=84=EC=97=AD?= =?UTF-8?q?=20=EB=B0=B0=EA=B2=BD=20=EC=83=89=EC=83=81=20=EB=B0=8F=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=84=A4=EC=A0=95=20#13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 70cc712d..c02ee756 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -14,9 +14,13 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - {children} + + + +
+ {children} +
+
); From e5ff7e3253b351eeae41bc3371f16b7ce894363a Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:26:20 +0900 Subject: [PATCH 02/47] =?UTF-8?q?=F0=9F=93=A6[Chore]=20discord=20webhook?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦[Chore] discord webhook 연결 * 📦[Chore] 웹훅 테스트 메시지 수정 * 📦[Chore]: 메시지 수정 --- .github/workflows/ci-develop.yml | 31 +++++++++++++++++++++++++++ .github/workflows/main-deploy.yml | 35 +++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-develop.yml b/.github/workflows/ci-develop.yml index 548bb7e7..e693a929 100644 --- a/.github/workflows/ci-develop.yml +++ b/.github/workflows/ci-develop.yml @@ -38,3 +38,34 @@ jobs: - name: Build Storybook run: npm run build-storybook + + notify-discord: + needs: ci-develop + runs-on: ubuntu-latest + if: success() + steps: + - name: Send Discord Notification + env: + WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + DATA: > + { + "embeds": [ + { + "title": "🎉 develop 브랜치 알림", + "description": "${{ github.event_name == 'pull_request' && '새로운 PR이 성공적으로 등록되었습니다!' || 'Push가 성공적으로 완료되었습니다!' }}", + "url": "${{ github.event.pull_request.html_url || github.event.head_commit.url }}", + "color": 5814783, + "fields": [ + { + "name": "Repository", + "value": "${{ github.repository }}" + } + ] + } + ] + } + run: | + curl -X POST \ + -H "Content-Type: application/json" \ + -d "$DATA" \ + $WEBHOOK_URL diff --git a/.github/workflows/main-deploy.yml b/.github/workflows/main-deploy.yml index 9359507a..0d2b828b 100644 --- a/.github/workflows/main-deploy.yml +++ b/.github/workflows/main-deploy.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x] # Node.js 18.x와 20.x 버전으로 테스트 + node-version: [18.x, 20.x] steps: - name: Checkout code uses: actions/checkout@v3 @@ -22,10 +22,41 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} # 매트릭스를 사용하여 버전 설정 + node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm install - name: Build Next.js app run: npm run build + + notify-discord: + needs: deploy-production + runs-on: ubuntu-latest + if: success() + steps: + - name: Send Discord Notification + env: + WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + DATA: > + { + "embeds": [ + { + "title": "🚀 main 브랜치 알림", + "description": "${{ github.event_name == 'pull_request' && '새로운 PR이 성공적으로 등록되었습니다!' || 'Push가 성공적으로 완료되었습니다!' }}", + "url": "${{ github.event.pull_request.html_url || github.event.head_commit.url }}", + "color": 5814783, + "fields": [ + { + "name": "Repository", + "value": "${{ github.repository }}" + } + ] + } + ] + } + run: | + curl -X POST \ + -H "Content-Type: application/json" \ + -d "$DATA" \ + $WEBHOOK_URL From 22615d7cbe94361cc617b273aa66f85ab48d3d33 Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:39:42 +0900 Subject: [PATCH 03/47] =?UTF-8?q?=E2=9C=A8=20[Feature]=20participantcounte?= =?UTF-8?q?r=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A7=8C=EB=93=A4?= =?UTF-8?q?=EA=B8=B0=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨[Feat] 기본 tsx, test, storybook 코드 작성 * 🐛[Fix] jest test error fix * 📦[Chore] typscript-eslint version up * 📦[Chore] storybook chromaic action 조건 수전 --- .github/workflows/chromatic.yml | 8 +- jest.config.js | 25 +- package-lock.json | 677 ++++++++++++++++-- package.json | 2 + .../ParticipantCounter.stories.tsx | 49 ++ .../ParticipantCounter.test.tsx | 39 + .../ParticipantCounter/ParticipantCounter.tsx | 39 + tsconfig.json | 25 +- 8 files changed, 793 insertions(+), 71 deletions(-) create mode 100644 src/components/ParticipantCounter/ParticipantCounter.stories.tsx create mode 100644 src/components/ParticipantCounter/ParticipantCounter.test.tsx create mode 100644 src/components/ParticipantCounter/ParticipantCounter.tsx diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 8eaf3b2b..b4956881 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -2,7 +2,13 @@ name: 'Chromatic Deployment' # Event for the workflow -on: push +on: + push: + branches: + - develop + pull_request: + branches: + - develop # List of jobs jobs: diff --git a/jest.config.js b/jest.config.js index 1b1168b9..679e6597 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,18 @@ -module.exports = { - testEnvironment: "jest-environment-jsdom", - transform: { - "^.+.tsx?$": "ts-jest", - }, - moduleNameMapper: { - "^@/(.*)$": "/$1", - }, +const nextJest = require('next/jest'); + +/** @type {import('jest').Config} */ +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.js and .env files in your test environment + dir: './', +}); + +// Add any custom config to be passed to Jest +const config = { + coverageProvider: 'v8', + testEnvironment: 'jsdom', + // Add more setup options before each test is run + // setupFilesAfterEnv: ['/jest.setup.ts'], }; + +// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async +module.exports = createJestConfig(config); diff --git a/package-lock.json b/package-lock.json index a5a85c96..5e866d87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,8 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", "autoprefixer": "^10.4.20", "chromatic": "^11.19.0", "eslint": "^8.57.1", @@ -5571,21 +5573,401 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/scope-manager": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", - "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5593,29 +5975,19 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", - "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", - "debug": "^4.3.4" + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5633,15 +6005,15 @@ } } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", - "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0" + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5651,16 +6023,34 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", - "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", "dev": true, "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { @@ -5670,6 +6060,31 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" }, @@ -5679,6 +6094,93 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/types": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", @@ -17581,6 +18083,97 @@ } } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", + "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/type-utils": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", + "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", + "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 06ec9e28..a2a3c253 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", "autoprefixer": "^10.4.20", "chromatic": "^11.19.0", "eslint": "^8.57.1", diff --git a/src/components/ParticipantCounter/ParticipantCounter.stories.tsx b/src/components/ParticipantCounter/ParticipantCounter.stories.tsx new file mode 100644 index 00000000..18ee12dd --- /dev/null +++ b/src/components/ParticipantCounter/ParticipantCounter.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ParticipantCounter from './ParticipantCounter'; + +const meta = { + title: 'Components/ParticipantCounter', + component: ParticipantCounter, + parameters: { + componentSubtitle: '참가자 수를 표시하는 컴포넌트', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + current: 5, + max: 20, + }, +}; + +export const Full: Story = { + args: { + current: 20, + max: 20, + }, + parameters: { + docs: { + description: { + story: '정원이 다 찼을 때는 아이콘과 텍스트 색상이 변경됩니다.', + }, + }, + }, +}; + +export const Overflow: Story = { + args: { + current: 25, + max: 20, + }, + render: (args) => ( +
+

+ * 참가자 수가 최대값을 초과하면 최대값으로 표시됩니다 (25 → 20) +

+ +
+ ), +}; diff --git a/src/components/ParticipantCounter/ParticipantCounter.test.tsx b/src/components/ParticipantCounter/ParticipantCounter.test.tsx new file mode 100644 index 00000000..ecb71ae1 --- /dev/null +++ b/src/components/ParticipantCounter/ParticipantCounter.test.tsx @@ -0,0 +1,39 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import ParticipantCounter, { PARTICIPANT_COLORS } from './ParticipantCounter'; + +describe('ParticipantCounter', () => { + it('참가자 수가 올바르게 표시되는지 확인', () => { + render(); + expect(screen.getByRole('participant-count')).toHaveTextContent('5/20'); + }); + + it('정원이 다 찼을 때 아이콘과 텍스트 색상이 변경되는지 확인', () => { + render(); + const icon = screen.getByRole('participant-icon'); + const count = screen.getByRole('participant-count'); + expect(icon).toHaveClass(PARTICIPANT_COLORS.full); + expect(count).toHaveClass(PARTICIPANT_COLORS.full); + }); + + it('정원이 차지 않았을 때 아이콘과 텍스트 기본 색상 확인', () => { + render(); + const icon = screen.getByRole('participant-icon'); + const count = screen.getByRole('participant-count'); + expect(icon).toHaveClass(PARTICIPANT_COLORS.default); + expect(count).toHaveClass(PARTICIPANT_COLORS.default); + }); + + it('참가자 수가 최대값을 초과할 경우 최대값으로 표시', () => { + render(); + expect(screen.getByRole('participant-count')).toHaveTextContent('20/20'); + }); + + it('정원이 초과되었을 때도 아이콘과 텍스트 색상이 full 색상인지 확인', () => { + render(); + const icon = screen.getByRole('participant-icon'); + const count = screen.getByRole('participant-count'); + expect(icon).toHaveClass(PARTICIPANT_COLORS.full); + expect(count).toHaveClass(PARTICIPANT_COLORS.full); + }); +}); diff --git a/src/components/ParticipantCounter/ParticipantCounter.tsx b/src/components/ParticipantCounter/ParticipantCounter.tsx new file mode 100644 index 00000000..84f07560 --- /dev/null +++ b/src/components/ParticipantCounter/ParticipantCounter.tsx @@ -0,0 +1,39 @@ +export const PARTICIPANT_COLORS = { + default: 'text-gray-700', + full: 'text-orange-500', +} as const; + +interface ParticipantCounterProps { + current: number; + max: number; +} + +function ParticipantCounter({ current, max }: ParticipantCounterProps) { + const displayCount = current > max ? max : current; + const isFull = current >= max; + const colorStyle = isFull + ? PARTICIPANT_COLORS.full + : PARTICIPANT_COLORS.default; + + return ( +
+ + + + {`${displayCount}/${max}`} +
+ ); +} + +export default ParticipantCounter; diff --git a/tsconfig.json b/tsconfig.json index c8f844c0..0d8430ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,21 +19,10 @@ } ], "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ], - "types": [ - "jest" - ] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"], + "types": ["jest"] } From 6a6e6dc1866352f3b9f07dea53f4c3707669999c Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:34:31 +0900 Subject: [PATCH 04/47] =?UTF-8?q?=E2=9C=A8[Feature]=20ConfirmedLabel=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EB=B0=9C=20(#2?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦[Chore] 폴더명 수정 * ✅[Test] test 코드 작성 * ✨[Feat] 컴포넌트 개발 * 💄[Design] 스토리북 코드 작성 --- public/icons/IcCheck.tsx | 38 +++++++++++++++++++ public/icons/index.ts | 2 +- .../ConfirmedLabel.stories.tsx | 25 ++++++++++++ .../confirmed-label/ConfirmedLabel.test.tsx | 20 ++++++++++ .../confirmed-label/ConfirmedLabel.tsx | 24 ++++++++++++ .../ParticipantCounter.stories.tsx | 0 .../ParticipantCounter.test.tsx | 0 .../ParticipantCounter.tsx | 0 8 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 public/icons/IcCheck.tsx create mode 100644 src/components/confirmed-label/ConfirmedLabel.stories.tsx create mode 100644 src/components/confirmed-label/ConfirmedLabel.test.tsx create mode 100644 src/components/confirmed-label/ConfirmedLabel.tsx rename src/components/{ParticipantCounter => participant-counter}/ParticipantCounter.stories.tsx (100%) rename src/components/{ParticipantCounter => participant-counter}/ParticipantCounter.test.tsx (100%) rename src/components/{ParticipantCounter => participant-counter}/ParticipantCounter.tsx (100%) diff --git a/public/icons/IcCheck.tsx b/public/icons/IcCheck.tsx new file mode 100644 index 00000000..78a4b9cb --- /dev/null +++ b/public/icons/IcCheck.tsx @@ -0,0 +1,38 @@ +import { SVGProps } from 'react'; + +interface IcCheckProps extends SVGProps { + width?: number; + height?: number; + circleColor?: string; + strokeColor?: string; +} + +function IcCheck({ + width = 24, + height = 24, + circleColor = '#F97316', + strokeColor = 'white', + ...props +}: IcCheckProps) { + return ( + + + + + ); +} + +export default IcCheck; diff --git a/public/icons/index.ts b/public/icons/index.ts index cb0ff5c3..4faa7804 100644 --- a/public/icons/index.ts +++ b/public/icons/index.ts @@ -1 +1 @@ -export {}; +export { default as IcCheck } from './IcCheck'; diff --git a/src/components/confirmed-label/ConfirmedLabel.stories.tsx b/src/components/confirmed-label/ConfirmedLabel.stories.tsx new file mode 100644 index 00000000..47d5b893 --- /dev/null +++ b/src/components/confirmed-label/ConfirmedLabel.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ConfirmedLabel from './ConfirmedLabel'; + +const meta = { + title: 'Components/ConfirmedLabel', + component: ConfirmedLabel, + parameters: { + componentSubtitle: '스터디 개설확정 상태를 표시하는 컴포넌트', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + visible: true, + }, +}; + +export const Hidden: Story = { + args: { + visible: false, + }, +}; diff --git a/src/components/confirmed-label/ConfirmedLabel.test.tsx b/src/components/confirmed-label/ConfirmedLabel.test.tsx new file mode 100644 index 00000000..c256d226 --- /dev/null +++ b/src/components/confirmed-label/ConfirmedLabel.test.tsx @@ -0,0 +1,20 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import ConfirmedLabel from './ConfirmedLabel'; + +describe('ConfirmedLabel', () => { + it('컴포넌트가 올바르게 렌더링되는지 확인', () => { + render(); + expect(screen.getByRole('confirmed-text')).toHaveTextContent('개설확정'); + }); + + it('visible이 false일 때 컴포넌트가 렌더링되지 않는지 확인', () => { + render(); + expect(screen.queryByRole('confirmed-text')).not.toBeInTheDocument(); + }); + + it('visible prop이 없을 때 기본적으로 보이는지 확인', () => { + render(); + expect(screen.getByRole('confirmed-text')).toBeInTheDocument(); + }); +}); diff --git a/src/components/confirmed-label/ConfirmedLabel.tsx b/src/components/confirmed-label/ConfirmedLabel.tsx new file mode 100644 index 00000000..c6e7aeca --- /dev/null +++ b/src/components/confirmed-label/ConfirmedLabel.tsx @@ -0,0 +1,24 @@ +import { IcCheck } from '../../../public/icons'; + +interface ConfirmedLabelProps { + visible?: boolean; +} + +function ConfirmedLabel({ visible = true }: ConfirmedLabelProps) { + if (!visible) return null; + + return ( +
+ + + 개설확정 + +
+ ); +} + +export default ConfirmedLabel; diff --git a/src/components/ParticipantCounter/ParticipantCounter.stories.tsx b/src/components/participant-counter/ParticipantCounter.stories.tsx similarity index 100% rename from src/components/ParticipantCounter/ParticipantCounter.stories.tsx rename to src/components/participant-counter/ParticipantCounter.stories.tsx diff --git a/src/components/ParticipantCounter/ParticipantCounter.test.tsx b/src/components/participant-counter/ParticipantCounter.test.tsx similarity index 100% rename from src/components/ParticipantCounter/ParticipantCounter.test.tsx rename to src/components/participant-counter/ParticipantCounter.test.tsx diff --git a/src/components/ParticipantCounter/ParticipantCounter.tsx b/src/components/participant-counter/ParticipantCounter.tsx similarity index 100% rename from src/components/ParticipantCounter/ParticipantCounter.tsx rename to src/components/participant-counter/ParticipantCounter.tsx From 3bc804ef0585c335531ce5ee578c1d1838067647 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:37:15 +0900 Subject: [PATCH 05/47] =?UTF-8?q?=E2=9C=A8[Feat]=20=08=ED=97=A4=EB=8D=94?= =?UTF-8?q?=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84#9=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨[Feat] 네비게이션 바 UI, 메뉴 클릭시 해당 페이지로 라우팅 기능 구현 #9 * 💄[Design] 전역 padding 적용 body에서 main으로 수정 #9 * ♻️[Refactor] HeaderBar에서 NavButton 분리 #9 * ✅[Test] 헤더 네비게이션 바 테스트 #12 * ✅[Test] 헤더바 스토리북 코드 작성 #12 --- src/app/layout.tsx | 6 ++- src/components/header/HeaderBar.stories.tsx | 17 ++++++ src/components/header/HeaderBar.test.tsx | 60 +++++++++++++++++++++ src/components/header/HeaderBar.tsx | 33 ++++++++++++ src/components/header/NavButton.tsx | 25 +++++++++ src/components/header/index.ts | 1 - 6 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 src/components/header/HeaderBar.stories.tsx create mode 100644 src/components/header/HeaderBar.test.tsx create mode 100644 src/components/header/HeaderBar.tsx create mode 100644 src/components/header/NavButton.tsx delete mode 100644 src/components/header/index.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c02ee756..caa0873d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next'; import ReactQueryProviders from '@/lib/utils/reactQueryProvider'; +import HeaderBar from '@/components/header/HeaderBar'; import '@/styles/globals.css'; @@ -15,9 +16,10 @@ export default function RootLayout({ }>) { return ( - + -
+ +
{children}
diff --git a/src/components/header/HeaderBar.stories.tsx b/src/components/header/HeaderBar.stories.tsx new file mode 100644 index 00000000..4afdb28a --- /dev/null +++ b/src/components/header/HeaderBar.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import HeaderBar from './HeaderBar'; + +const meta: Meta = { + title: 'Components/HeaderBar', + component: HeaderBar, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; diff --git a/src/components/header/HeaderBar.test.tsx b/src/components/header/HeaderBar.test.tsx new file mode 100644 index 00000000..3d4a7519 --- /dev/null +++ b/src/components/header/HeaderBar.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from '@testing-library/react'; +import HeaderBar from './HeaderBar'; +import '@testing-library/jest-dom'; + +const navigationLinks = [ + { name: '홈' }, + { name: '책 모임' }, + { name: '책 교환' }, + { name: '찜 목록' }, + { name: '로그인' }, +]; + +describe('HeaderBar 컴포넌트 테스트', () => { + beforeEach(() => { + render(); + }); + + describe('네비게이션 링크', () => { + it('모든 네비게이션 링크가 올바르게 렌더링되어야 한다', () => { + navigationLinks.forEach((link) => { + const linkElement = screen.getByRole('link', { name: link.name }); + expect(linkElement).toBeInTheDocument(); + }); + }); + + it('홈 링크에 올바른 스타일이 적용되어야 한다', () => { + const homeLink = screen.getByRole('link', { name: '홈' }); + expect(homeLink).toHaveClass('hover:scale-105 md:text-base'); + }); + }); +}); + +jest.mock('next/navigation', () => ({ + usePathname: () => '/', +})); + +describe('HeaderBar 컴포넌트 테스트', () => { + beforeEach(() => { + render(); + }); + + it('책 모임 링크가 올바른 href를 가져야 한다', () => { + const bookclubLink = screen.getByRole('link', { name: '책 모임' }); + expect(bookclubLink).toHaveAttribute('href', '/bookclub'); + }); +}); + +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), + usePathname: jest.fn(), +})); + +describe('HeaderBar 네비게이션 테스트', () => { + it('각 네비게이션 버튼 클릭시 올바른 href 속성을 가져야 한다', () => { + render(); + + const homeLink = screen.getByRole('link', { name: '홈' }); + expect(homeLink).toHaveAttribute('href', '/'); + }); +}); diff --git a/src/components/header/HeaderBar.tsx b/src/components/header/HeaderBar.tsx new file mode 100644 index 00000000..80f92f49 --- /dev/null +++ b/src/components/header/HeaderBar.tsx @@ -0,0 +1,33 @@ +'use client'; + +import React from 'react'; +import NavButton from './NavButton'; + +function HeaderBar() { + const navItems = [ + { href: '/', label: '홈' }, + { href: '/bookclub', label: '책 모임' }, + { href: '/exchange', label: '책 교환' }, + { href: '/wish', label: '찜 목록' }, + ]; + + return ( + + ); +} + +export default HeaderBar; diff --git a/src/components/header/NavButton.tsx b/src/components/header/NavButton.tsx new file mode 100644 index 00000000..863facd9 --- /dev/null +++ b/src/components/header/NavButton.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +interface NavButtonProps { + href: string; + children: React.ReactNode; +} + +function NavButton({ href, children }: NavButtonProps) { + const pathname = usePathname(); + + return ( + + {children} + + ); +} + +export default NavButton; diff --git a/src/components/header/index.ts b/src/components/header/index.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/components/header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; From 1ed9aab3089f776bcfb289a8464d2084f586c849 Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:10:58 +0900 Subject: [PATCH 06/47] =?UTF-8?q?=E2=9C=A8=20[Feature]=20progressBar=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EB=B0=9C=20(#3?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✅[Test] 테스트 코드 작성 * ✨[Feat] 컴포넌트, 스토리북 개발 --- .../progress-bar/ProgressBar.stories.tsx | 55 +++++++++++++++++++ .../progress-bar/ProgressBar.test.tsx | 42 ++++++++++++++ src/components/progress-bar/ProgressBar.tsx | 44 +++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/components/progress-bar/ProgressBar.stories.tsx create mode 100644 src/components/progress-bar/ProgressBar.test.tsx create mode 100644 src/components/progress-bar/ProgressBar.tsx diff --git a/src/components/progress-bar/ProgressBar.stories.tsx b/src/components/progress-bar/ProgressBar.stories.tsx new file mode 100644 index 00000000..bbf3f1ee --- /dev/null +++ b/src/components/progress-bar/ProgressBar.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ProgressBar from './ProgressBar'; + +const meta = { + title: 'Components/ProgressBar', + component: ProgressBar, + parameters: { + componentSubtitle: '진행률을 시각적으로 표시하는 프로그레스바 컴포넌트', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + current: 5, + max: 20, + }, +}; + +export const CustomSize: Story = { + args: { + current: 10, + max: 20, + height: 8, + borderRadius: 8, + }, +}; + +export const Full: Story = { + args: { + current: 20, + max: 20, + }, + parameters: { + docs: { + description: { + story: '정원이 다 찼을 때는 프로그레스바 색상이 변경됩니다.', + }, + }, + }, +}; + +export const WithContainer: Story = { + args: { + current: 15, + max: 20, + }, + render: (args) => ( +
+ +
+ ), +}; diff --git a/src/components/progress-bar/ProgressBar.test.tsx b/src/components/progress-bar/ProgressBar.test.tsx new file mode 100644 index 00000000..6b94e028 --- /dev/null +++ b/src/components/progress-bar/ProgressBar.test.tsx @@ -0,0 +1,42 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import ProgressBar, { PROGRESS_COLORS } from './ProgressBar'; + +describe('ProgressBar', () => { + it('프로그레스바의 현재값과 최대값이 올바르게 전달되는지 확인', () => { + render(); + const progressbar = screen.getByRole('progressbar'); + expect(progressbar).toHaveAttribute('aria-valuenow', '5'); + expect(progressbar).toHaveAttribute('aria-valuemax', '20'); + }); + + it('정원이 다 찼을 때 색상이 변경되는지 확인', () => { + render(); + const fillBar = screen.getByRole('progressbar').children[0]; + expect(fillBar).toHaveClass(PROGRESS_COLORS.full); + }); + + it('기본 높이와 radius가 적용되는지 확인', () => { + render(); + const progressbar = screen.getByRole('progressbar'); + expect(progressbar).toHaveStyle({ height: '4px', borderRadius: '6px' }); + }); + + it('커스텀 높이와 radius가 적용되는지 확인', () => { + render(); + const progressbar = screen.getByRole('progressbar'); + expect(progressbar).toHaveStyle({ height: '8px', borderRadius: '8px' }); + }); + + it('프로그레스바의 너비가 계산된 퍼센티지로 채워지는지 확인', () => { + render(); + const fillBar = screen.getByRole('progressbar').children[0]; + expect(fillBar).toHaveStyle({ width: '25%' }); // 5/20 * 100 = 25% + }); + + it('current가 max보다 클 때 너비가 100%를 초과하지 않는지 확인', () => { + render(); + const fillBar = screen.getByRole('progressbar').children[0]; + expect(fillBar).toHaveStyle({ width: '100%' }); + }); +}); diff --git a/src/components/progress-bar/ProgressBar.tsx b/src/components/progress-bar/ProgressBar.tsx new file mode 100644 index 00000000..8c17a8d4 --- /dev/null +++ b/src/components/progress-bar/ProgressBar.tsx @@ -0,0 +1,44 @@ +export const PROGRESS_COLORS = { + background: 'bg-orange-50', + default: 'bg-orange-600', + full: 'bg-orange-400', +} as const; + +interface ProgressBarProps { + current: number; + max: number; + height?: number; + borderRadius?: number; +} + +function ProgressBar({ + current, + max, + height = 4, + borderRadius = 6, +}: ProgressBarProps) { + const percentage = Math.min((current / max) * 100, 100); + const isFull = current >= max; + const fillColor = isFull ? PROGRESS_COLORS.full : PROGRESS_COLORS.default; + + return ( +
+
+
+ ); +} + +export default ProgressBar; From a82d4817be9646e7754243c081b76750cf0b01d2 Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:12:56 +0900 Subject: [PATCH 07/47] =?UTF-8?q?=E2=9C=A8=20[Feat]=20TextChip=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EB=B0=9C=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✅[Test] 테스트 코드 작성 * ✨[Feat] 컴포넌트 개발 * 💄[Design] 스토리북 작성 --- src/components/text-chip/TextChip.stories.tsx | 37 +++++++++++++++++++ src/components/text-chip/TextChip.test.tsx | 23 ++++++++++++ src/components/text-chip/TextChip.tsx | 24 ++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/components/text-chip/TextChip.stories.tsx create mode 100644 src/components/text-chip/TextChip.test.tsx create mode 100644 src/components/text-chip/TextChip.tsx diff --git a/src/components/text-chip/TextChip.stories.tsx b/src/components/text-chip/TextChip.stories.tsx new file mode 100644 index 00000000..13aee917 --- /dev/null +++ b/src/components/text-chip/TextChip.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TextChip } from './TextChip'; + +const meta = { + title: 'Components/TextChip', + component: TextChip, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + text: '기본', + isDueSoon: false, + }, +}; + +export const DueSoon: Story = { + args: { + text: '마감임박', + isDueSoon: true, + }, +}; + +export const MultipleChips: Story = { + args: { + text: '', + isDueSoon: false, + }, + render: () => ( +
+ + +
+ ), +}; diff --git a/src/components/text-chip/TextChip.test.tsx b/src/components/text-chip/TextChip.test.tsx new file mode 100644 index 00000000..353455b5 --- /dev/null +++ b/src/components/text-chip/TextChip.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { COLORS, TextChip } from './TextChip'; + +describe('TextChip', () => { + it('텍스트가 올바르게 렌더링되어야 한다', () => { + render(); + const chip = screen.getByRole('text-chip'); + expect(chip).toBeInTheDocument(); + }); + + it('기본 상태일 때 디폴트 색상과 기본 배경색이 적용되어야 한다', () => { + render(); + const chip = screen.getByRole('text-chip'); + expect(chip).toHaveClass(COLORS.default, COLORS.background); + }); + + it('isDueSoon이 true일 때 텍스트 색상이 변경되어야 한다.', () => { + render(); + const chip = screen.getByRole('text-chip'); + expect(chip).toHaveClass(COLORS.dueSoon, COLORS.background); + }); +}); diff --git a/src/components/text-chip/TextChip.tsx b/src/components/text-chip/TextChip.tsx new file mode 100644 index 00000000..4bddf36c --- /dev/null +++ b/src/components/text-chip/TextChip.tsx @@ -0,0 +1,24 @@ +export const COLORS = { + background: 'bg-gray-900', + default: 'text-white', + dueSoon: 'text-orange-600', +} as const; + +interface TextChipProps { + text: string; + isDueSoon?: boolean; +} + +export function TextChip({ text, isDueSoon = false }: TextChipProps) { + return ( +
+ {text} +
+ ); +} From 43e1a4ba91817b4e2120b5d72f61bf7c0ef487aa Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:18:09 +0900 Subject: [PATCH 08/47] =?UTF-8?q?=F0=9F=92=84[Design]=20rootlayout=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=20padding=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index caa0873d..b6668862 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,7 +19,7 @@ export default function RootLayout({ -
+
{children}
From d83c0f4e1e0763e8d05bdf27f716d57abdf2b0b8 Mon Sep 17 00:00:00 2001 From: Minkyung Kim <97824352+wynter24@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:39:19 +0900 Subject: [PATCH 09/47] =?UTF-8?q?=E2=9C=A8[Feat]=20=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20button=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#11=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✅[Test] 버튼 테스트 코드 작성 #11 * 📦[Chore] jest.config 수정 * 📦[Chore] tailwind-merge 설치 * ✨[Feat] 버튼 컴포넌트 생성 및 초기 디자인 설정 * 💄[Design] 버튼 기본 스타일 cursor-pointer로 설정 * ✅[Test] 스타일 테스트 추가 * ♻️[Refactor] Home 페이지 button 컴포넌트에 props 추가 * ♻️[Refactor] 타입 선언 type에서 interface로 변경 #11 * 💄[Design] 스토리북 코드 작성 #11 * 💄[Design] 최소 너비 설정 #11 * 📦[Chore] tailwind content 수정 #11 * 📦[Chore] tailwind content 수정 #11 * ♻️[Refactor] button props 변경 및 해당 스타일 설정 #11 * ♻️[Refactor] button props 변경 #11 * ✅[Test] 스타일 관련 테스트 코드 삭제 #11 * 💄[Design] 스토리북 args 수정 #11 * 💬[Comment] test 코드 주석 정리 #11 * ♻️[Refactor] 사이즈, 배경, 색상을 객체 리터럴로 변경 및 variant 추가 #11 * 📝[Docs] 스토리북 버튼 텍스트 및 이름 수정 #11 * ♻️[Refactor] 중복 색상 삭제 및 variantColor에 gray 추가 #11 * 🎨[Style] 상수 이름 영문 대문자 스네이크 표기법으로 변경 #11 * 🎨[Style] 깃 컨벤션 - 함수 표현식에서 함수 선언식으로 수정 #11 * 🔥[Remove] 초기 설정에 bookclub에 작성했던 button 삭제 #11 * ♻️[Refactor] 배경스타일 및 색상 variant 변경 #11 * 📝[Docs] 변경된 Button props명으로 스토리북 args 수정 #11 * ✅[Test] 변경된 props에 따른 테스트 코드 수정 #11 --- package-lock.json | 10 +++++ package.json | 1 + src/app/bookclub/page.tsx | 8 +--- src/components/button/Button.stories.ts | 8 +++- src/components/button/Button.test.tsx | 37 ++++++++++++++++++- src/components/button/Button.tsx | 49 +++++++++++++++++++++++-- tailwind.config.ts | 7 +--- 7 files changed, 101 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e866d87..d9563012 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "next": "15.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.5", "zustand": "^5.0.1" }, "devDependencies": { @@ -17442,6 +17443,15 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "license": "MIT" }, + "node_modules/tailwind-merge": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", + "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.15", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz", diff --git a/package.json b/package.json index a2a3c253..9ff8b551 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "next": "15.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.5", "zustand": "^5.0.1" }, "devDependencies": { diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 30cbc4fa..7bcd29e7 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,9 +1,3 @@ -import Button from '@/components/button/Button'; - export default function Home() { - return ( -
-
- ); + return
; } diff --git a/src/components/button/Button.stories.ts b/src/components/button/Button.stories.ts index 1a27d296..4cc2810b 100644 --- a/src/components/button/Button.stories.ts +++ b/src/components/button/Button.stories.ts @@ -12,8 +12,12 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Primary: Story = { +export const DefaultButton: Story = { args: { - // 버튼 props + text: '기본 버튼', + size: 'large', + fillType: 'solid', + themeColor: 'orange-600', + disabled: false, }, }; diff --git a/src/components/button/Button.test.tsx b/src/components/button/Button.test.tsx index 41fca76a..aca48259 100644 --- a/src/components/button/Button.test.tsx +++ b/src/components/button/Button.test.tsx @@ -1,3 +1,38 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import Button from './Button'; + describe('Button', () => { - it('버튼이 정상적으로 렌더링됩니다', () => {}); + it('버튼이 정상적으로 렌더링', () => { + render( + + ); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 79a60d2b..325a942c 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,12 +1,7 @@ import type { Config } from 'tailwindcss'; export default { - content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', - './src/*/.{html,js}', - ], + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], theme: { extend: { colors: { From c08af6f06e26113e3e25dc716c86228845919bcc Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:15:38 +0900 Subject: [PATCH 10/47] =?UTF-8?q?=F0=9F=93=A6[Chore]=20ci=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EB=B2=94=EC=9C=84=20=EB=B0=8F=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/chromatic.yml | 3 +- .github/workflows/{ci-develop.yml => ci.yml} | 34 ++++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) rename .github/workflows/{ci-develop.yml => ci.yml} (54%) diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index b4956881..10a5504a 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -8,7 +8,8 @@ on: - develop pull_request: branches: - - develop + - '**' + - '!main' # List of jobs jobs: diff --git a/.github/workflows/ci-develop.yml b/.github/workflows/ci.yml similarity index 54% rename from .github/workflows/ci-develop.yml rename to .github/workflows/ci.yml index e693a929..c239d990 100644 --- a/.github/workflows/ci-develop.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,17 @@ -name: CI for develop +name: CI on: pull_request: branches: - - develop + - '**' + - '!main' push: branches: - develop jobs: - ci-develop: - name: Lint, Test, and Build (develop) + ci: + name: Lint, Test, and Build runs-on: ubuntu-latest strategy: matrix: @@ -40,7 +41,7 @@ jobs: run: npm run build-storybook notify-discord: - needs: ci-develop + needs: ci runs-on: ubuntu-latest if: success() steps: @@ -51,14 +52,33 @@ jobs: { "embeds": [ { - "title": "🎉 develop 브랜치 알림", - "description": "${{ github.event_name == 'pull_request' && '새로운 PR이 성공적으로 등록되었습니다!' || 'Push가 성공적으로 완료되었습니다!' }}", + "title": "${{ + format('{0}', + (github.base_ref == 'develop' || github.ref_name == 'develop') + && '🚀 Develop 브랜치 CI 알림' + || '🎉 새 작업 CI 알림' + ) + }}", + "description": "${{ + format('{0}', + github.event_name == 'pull_request' + && (github.base_ref == 'develop' + && format('Develop 브랜치로 새로운 PR이 등록되었습니다! ({0} → develop)', github.head_ref) + || format('새로운 PR이 등록되었습니다! ({0} → {1})', github.head_ref, github.base_ref) + ) + || 'Develop 브랜치에 Push가 완료되었습니다!' + ) + }}", "url": "${{ github.event.pull_request.html_url || github.event.head_commit.url }}", "color": 5814783, "fields": [ { "name": "Repository", "value": "${{ github.repository }}" + }, + { + "name": "Branch", + "value": "${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}" } ] } From 6ec79247e90e225dc64194c1828bb29d942535b9 Mon Sep 17 00:00:00 2001 From: Minkyung Kim <97824352+wynter24@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:42:31 +0900 Subject: [PATCH 11/47] =?UTF-8?q?=E2=9C=A8[Feat]=20WrittenReview,=20Rating?= =?UTF-8?q?Display=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=20#38=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨[Feat] 평점 표시용 RatingIcon 컴포넌트 추가 #38 * 🔥[Remove] 초시 설정때 생성된 불필요한 Button2 폴더 삭제 #38 * ♻️[Refactor] RatingIcon heartColor에서 checked로 prop 변경 #38 * ✨[Feat] 평점 공통컴포넌트 생성 #38 * ✨[Feat] RatingDisplay 스토리북 작성 #38 * ✨[Feat] WrittenReview 공통 컴포넌트 생성 #38 * ✨[Feat] WrittenReview 스토리북 작성 #38 * 🐛[Fix] WrittenReview 이미지 에러 처리 #38 * ✅[Test] WrittenReview 테스트 코드 작성 #38 * 🚚[Rename] rating에서 rating-display로 폴더명 수정 #38 * 💄[Design] 텍스트 dp font-medium 속성 추가 #38 --- public/icons/RatingIcon.tsx | 32 ++++++++++ public/icons/index.ts | 1 + src/components/button2/Button2.stories.ts | 21 ------- src/components/button2/Button2.tsx | 7 --- .../rating-display/RatingDisplay.stories.tsx | 16 +++++ .../rating-display/RatingDisplay.tsx | 23 ++++++++ .../written-review/WrittenReview.stories.tsx | 22 +++++++ .../written-review/WrittenReview.test.tsx | 55 +++++++++++++++++ .../written-review/WrittenReview.tsx | 59 +++++++++++++++++++ 9 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 public/icons/RatingIcon.tsx delete mode 100644 src/components/button2/Button2.stories.ts delete mode 100644 src/components/button2/Button2.tsx create mode 100644 src/components/rating-display/RatingDisplay.stories.tsx create mode 100644 src/components/rating-display/RatingDisplay.tsx create mode 100644 src/components/written-review/WrittenReview.stories.tsx create mode 100644 src/components/written-review/WrittenReview.test.tsx create mode 100644 src/components/written-review/WrittenReview.tsx diff --git a/public/icons/RatingIcon.tsx b/public/icons/RatingIcon.tsx new file mode 100644 index 00000000..4582487b --- /dev/null +++ b/public/icons/RatingIcon.tsx @@ -0,0 +1,32 @@ +import { SVGProps } from 'react'; + +interface RatingIconProps extends SVGProps { + width?: number; + height?: number; + checked: boolean; +} + +function RatingIcon({ + width = 24, + height = 24, + checked = false, + ...props +}: RatingIconProps) { + const heartColor = checked ? '#EA580C' : '#D1D5DB'; + return ( + + + + ); +} +export default RatingIcon; diff --git a/public/icons/index.ts b/public/icons/index.ts index 4faa7804..26a5c656 100644 --- a/public/icons/index.ts +++ b/public/icons/index.ts @@ -1 +1,2 @@ export { default as IcCheck } from './IcCheck'; +export { default as RatingIcon } from './RatingIcon'; diff --git a/src/components/button2/Button2.stories.ts b/src/components/button2/Button2.stories.ts deleted file mode 100644 index 50061e2a..00000000 --- a/src/components/button2/Button2.stories.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import Button2 from './Button2'; - -const meta: Meta = { - title: 'Components/Button', - component: Button2, - parameters: { - layout: 'centered', - }, -}; - -export default meta; -type Story = StoryObj; - -export const Primary2: Story = { - args: { - // 버튼 props - }, -}; - -// 버튼 컴포넌트 스토리 추가 diff --git a/src/components/button2/Button2.tsx b/src/components/button2/Button2.tsx deleted file mode 100644 index d02aaa5d..00000000 --- a/src/components/button2/Button2.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const Button2 = () => { - return
hi
; -}; - -export default Button2; diff --git a/src/components/rating-display/RatingDisplay.stories.tsx b/src/components/rating-display/RatingDisplay.stories.tsx new file mode 100644 index 00000000..839e53f3 --- /dev/null +++ b/src/components/rating-display/RatingDisplay.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import RatingDisplay from './RatingDisplay'; + +const meta = { + title: 'Components/RatingDisplay', + component: RatingDisplay, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const FourStarRating: Story = { + args: { + ratingCount: 4, + }, +}; diff --git a/src/components/rating-display/RatingDisplay.tsx b/src/components/rating-display/RatingDisplay.tsx new file mode 100644 index 00000000..4c17483b --- /dev/null +++ b/src/components/rating-display/RatingDisplay.tsx @@ -0,0 +1,23 @@ +import RatingIcon from '../../../public/icons/RatingIcon'; + +export default function RatingDisplay({ + ratingCount, +}: { + ratingCount: number; +}) { + const normalizedRating = Math.min(ratingCount, 5); + + return ( +
+ {Array.from({ length: normalizedRating }).map((_, index) => ( +
+ ); +} diff --git a/src/components/written-review/WrittenReview.stories.tsx b/src/components/written-review/WrittenReview.stories.tsx new file mode 100644 index 00000000..3fa22bfb --- /dev/null +++ b/src/components/written-review/WrittenReview.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import WrittenReview from './WrittenReview'; + +const meta = { + title: 'Components/WrittenReview', + component: WrittenReview, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const CreatedReview: Story = { + args: { + ratingCount: 4, + comment: + '따듯하게 느껴지는 공간이에요 :) 평소에 달램 이용해보고 싶었는데 이렇게 같이 달램 생기니까 너무 좋아요! 프로그램이 더 많이 늘어났으면 좋겠어요.', + profileImage: + 'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg', + userName: '럽윈즈올', + createdAt: '2024.01.25', + }, +}; diff --git a/src/components/written-review/WrittenReview.test.tsx b/src/components/written-review/WrittenReview.test.tsx new file mode 100644 index 00000000..35a40d3e --- /dev/null +++ b/src/components/written-review/WrittenReview.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import WrittenReview from './WrittenReview'; +import { act } from 'react'; + +describe('WrittenReview Component', () => { + const defaultProfileImage = + 'https://cdn.pixabay.com/photo/2018/02/12/10/45/heart-3147976_1280.jpg'; + const validProfileImage = + 'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg'; + const brokenProfileImage = 'https://broken-url.com/image.jpg'; + + it('유효한 이미지 경우 정상적으로 렌더링', () => { + render( + , + ); + const imgElement = screen.getByAltText("Test User's profile picture"); + + expect(imgElement.getAttribute('src')).toContain( + encodeURIComponent(validProfileImage), + ); + }); + + it('유효하지 않은 이미지 경우 기본 이미지로 대체', async () => { + render( + , + ); + + const imgElement = screen.getByAltText("Test User's profile picture"); + + expect(imgElement.getAttribute('src')).toContain( + encodeURIComponent(brokenProfileImage), + ); + + await act(async () => { + imgElement.dispatchEvent(new Event('error')); + }); + + expect(imgElement.getAttribute('src')).toContain( + encodeURIComponent(defaultProfileImage), + ); + }); +}); diff --git a/src/components/written-review/WrittenReview.tsx b/src/components/written-review/WrittenReview.tsx new file mode 100644 index 00000000..df4d53ec --- /dev/null +++ b/src/components/written-review/WrittenReview.tsx @@ -0,0 +1,59 @@ +'use client'; + +import Image from 'next/image'; +import RatingDisplay from '../rating-display/RatingDisplay'; +import { useState, useEffect } from 'react'; + +// 디자인 확정시, 기본 이미지 변경 +const defaultProfileImage = + 'https://cdn.pixabay.com/photo/2018/02/12/10/45/heart-3147976_1280.jpg'; + +interface WrittenReviewProps { + ratingCount: number; + comment: string; + profileImage?: string; + userName: string; + createdAt: string; +} + +export default function WrittenReview({ + ratingCount, + comment, + profileImage, + userName, + createdAt, +}: WrittenReviewProps) { + const [imgSrc, setImgSrc] = useState(profileImage || defaultProfileImage); + + useEffect(() => { + setImgSrc(profileImage || defaultProfileImage); + }, [profileImage]); + + const handleImageError = () => { + setImgSrc(defaultProfileImage); + }; + + return ( +
+ +

+ {comment} +

+
+ {`${userName}'s +

+ {userName} +

+

{createdAt}

+
+
+
+ ); +} From aca55840d533729ab271620af26a49ef86ecdeea Mon Sep 17 00:00:00 2001 From: Minkyung Kim <97824352+wynter24@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:02:24 +0900 Subject: [PATCH 12/47] =?UTF-8?q?=F0=9F=93=A6[Chore]=20=EC=A4=84=20?= =?UTF-8?q?=EB=B0=94=EA=BF=88=20=EC=8A=A4=ED=83=80=EC=9D=BC(LF)=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20.gitattributes=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#70=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..5fd9f3b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.ts text eol=lf +*.tsx text eol=lf \ No newline at end of file From e48632bc7044eae49a1409ddbd96a17d68d7011f Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:55:54 +0900 Subject: [PATCH 13/47] =?UTF-8?q?=E2=9C=A8[Feat]=20Color=20system=20?= =?UTF-8?q?=EB=B0=8F=20breakpoint,=20storybook=20preview=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨[Feat] color 시스템 도입 * ✨[Feat] 반응형 screen 및 storybook viewport 추가 --- .storybook/preview.ts | 25 +++++++++++++++++++++++++ tailwind.config.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 6aa6b1f8..4580843a 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -9,6 +9,31 @@ const preview: Preview = { date: /Date$/i, }, }, + viewport: { + viewports: { + mobile: { + name: 'Mobile', + styles: { + width: '375px', + height: '1168px', + }, + }, + tablet: { + name: 'Tablet', + styles: { + width: '744px', + height: '1343px', + }, + }, + desktop: { + name: 'Desktop', + styles: { + width: '1920px', + height: '1080px', + }, + }, + }, + }, }, }; diff --git a/tailwind.config.ts b/tailwind.config.ts index 325a942c..83f3f52d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,14 +1,45 @@ import type { Config } from 'tailwindcss'; -export default { +const config: Config = { content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], theme: { + screens: { + sm: '375px', + md: '744px', + lg: '1920px', + }, extend: { colors: { background: 'var(--background)', foreground: 'var(--foreground)', + gray: { + light: '#fefefe', + 'light-hover': '#fdfdfd', + 'light-active': '#fafbfb', + normal: '#f0f1f3', + 'normal-hover': '#d8d9db', + 'normal-active': '#c0c1c2', + dark: '#b4b5b6', + 'dark-hover': '#909192', + 'dark-active': '#6c6c6d', + darker: '#545455', + }, + green: { + light: '#e6f6f4', + 'light-hover': '#d9f2ef', + 'light-active': '#b0e4dd', + normal: '#00a991', + 'normal-hover': '#009883', + 'normal-active': '#008774', + dark: '#007f6d', + 'dark-hover': '#006557', + 'dark-active': '#004c41', + darker: '#003b33', + }, }, }, }, plugins: [], -} satisfies Config; +}; + +export default config; From a66ea628a3881ad38afe9f4aeca377244d9a402b Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:57:55 +0900 Subject: [PATCH 14/47] =?UTF-8?q?=E2=9C=A8[Feat]=20Avatar,=20AvatarGroup?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨[Feat] Avatar 컴포넌트 생성 * ✅[Test] avatar test 코드 작성 * ✅[Test] avatar storybook 코드 작성 * ♻️[Refactor] avatar 컴포넌트 코드 수정 * ✨[Feat] avatargroup 컴포넌트 개발 * ♻️[Refactor] avatarsize 상수 분리 * ✅[Test] avatargroup storybook 코드 작성 * ✨[Feat] avatar onClick 추가 * ✅[Test] avatar test 코드 수정 * ✅[Test] avatargroup test 코드 작성 * 🎨[Style] 따옴표 코드 포맷팅 변경 --- next.config.ts | 2 +- .../avatar-group/AvatarGroup.stories.tsx | 65 +++++++++++++++++ .../avatar-group/AvatarGroup.test.tsx | 70 +++++++++++++++++++ src/components/avatar-group/AvatarGroup.tsx | 43 ++++++++++++ src/components/avatar/Avatar.stories.tsx | 46 ++++++++++++ src/components/avatar/Avatar.test.tsx | 21 ++++++ src/components/avatar/Avatar.tsx | 24 +++++++ src/constants/avatar.ts | 7 ++ src/constants/index.ts | 1 + 9 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/components/avatar-group/AvatarGroup.stories.tsx create mode 100644 src/components/avatar-group/AvatarGroup.test.tsx create mode 100644 src/components/avatar-group/AvatarGroup.tsx create mode 100644 src/components/avatar/Avatar.stories.tsx create mode 100644 src/components/avatar/Avatar.test.tsx create mode 100644 src/components/avatar/Avatar.tsx create mode 100644 src/constants/avatar.ts create mode 100644 src/constants/index.ts diff --git a/next.config.ts b/next.config.ts index e9ffa308..5e891cf0 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ diff --git a/src/components/avatar-group/AvatarGroup.stories.tsx b/src/components/avatar-group/AvatarGroup.stories.tsx new file mode 100644 index 00000000..d21d5e45 --- /dev/null +++ b/src/components/avatar-group/AvatarGroup.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import AvatarGroup from './AvatarGroup'; +import Avatar from '@/components/avatar/Avatar'; + +const meta = { + title: 'Components/AvatarGroup', + component: AvatarGroup, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const renderAvatars = (count: number) => + Array(count) + .fill(null) + .map((_, i) => ( + + )); + +export const Small: Story = { + render: () => ( + + {renderAvatars(16)} + + ), +}; + +export const Medium: Story = { + render: () => ( + + {renderAvatars(16)} + + ), +}; + +export const Large: Story = { + render: () => ( + + {renderAvatars(16)} + + ), +}; + +export const ShowMoreAvatars: Story = { + render: () => ( + + {renderAvatars(16)} + + ), +}; + +export const FewAvatars: Story = { + render: () => ( + + {renderAvatars(3)} + + ), +}; diff --git a/src/components/avatar-group/AvatarGroup.test.tsx b/src/components/avatar-group/AvatarGroup.test.tsx new file mode 100644 index 00000000..fc4199e1 --- /dev/null +++ b/src/components/avatar-group/AvatarGroup.test.tsx @@ -0,0 +1,70 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import AvatarGroup from './AvatarGroup'; +import Avatar from '../avatar/Avatar'; + +const createMockAvatars = (count: number) => { + return Array.from({ length: count }, (_, index) => ( + + )); +}; + +describe('AvatarGroup', () => { + it('maxCount만큼의 아바타를 렌더링해야 한다', () => { + const maxCount = 3; + const totalAvatars = 5; + + render( + + {createMockAvatars(totalAvatars)} + , + ); + + const avatarImages = screen.getAllByRole('img'); + expect(avatarImages).toHaveLength(maxCount); + }); + + it('남은 아바타 개수를 표시해야 한다', () => { + const maxCount = 2; + const totalAvatars = 5; + const remainingCount = totalAvatars - maxCount; + + render( + + {createMockAvatars(totalAvatars)} + , + ); + + const remainingCountElement = screen.getByText(/^\+\d+$/); + expect(remainingCountElement).toBeInTheDocument(); + expect(remainingCountElement.textContent).toBe(`+${remainingCount}`); + }); + + it('maxCount보다 적은 아바타가 있으면 남은 개수를 표시하지 않아야 한다', () => { + const maxCount = 4; + const totalAvatars = 3; + + render( + + {createMockAvatars(totalAvatars)} + , + ); + + const remainingCountElement = screen.queryByText(/^\+\d+$/); + expect(remainingCountElement).not.toBeInTheDocument(); + }); + + it('기본 maxCount는 4여야 한다', () => { + const totalAvatars = 6; + const defaultMaxCount = 4; + + render({createMockAvatars(totalAvatars)}); + + const avatarImages = screen.getAllByRole('img'); + expect(avatarImages).toHaveLength(defaultMaxCount); + }); +}); diff --git a/src/components/avatar-group/AvatarGroup.tsx b/src/components/avatar-group/AvatarGroup.tsx new file mode 100644 index 00000000..0b1ebfc2 --- /dev/null +++ b/src/components/avatar-group/AvatarGroup.tsx @@ -0,0 +1,43 @@ +import Avatar from '@/components/avatar/Avatar'; +import { AVATAR_SIZE, AvatarSize } from '@/constants'; +import { Children, ReactElement, cloneElement } from 'react'; + +interface AvatarGroupProps { + children: ReactElement | ReactElement[]; + maxCount?: number; + size?: AvatarSize; +} + +function AvatarGroup({ + children, + maxCount = 4, + size = 'sm', +}: AvatarGroupProps) { + const avatars = Children.toArray(children).slice(0, maxCount); + const remainingCount = Children.count(children) - maxCount; + + return ( +
+
+ {avatars.map((avatar, index) => + cloneElement(avatar as ReactElement, { + key: index, + size, + }), + )} + + {remainingCount > 0 && ( +
+ + +{remainingCount} + +
+ )} +
+
+ ); +} + +export default AvatarGroup; diff --git a/src/components/avatar/Avatar.stories.tsx b/src/components/avatar/Avatar.stories.tsx new file mode 100644 index 00000000..8735bff5 --- /dev/null +++ b/src/components/avatar/Avatar.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Avatar from './Avatar'; + +const meta = { + title: 'Components/Avatar', + component: Avatar, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Small: Story = { + args: { + src: 'https://picsum.photos/200', + alt: 'Avatar image', + size: 'sm', + }, +}; + +export const Medium: Story = { + args: { + src: 'https://picsum.photos/200', + alt: 'Avatar image', + size: 'md', + }, +}; + +export const Large: Story = { + args: { + src: 'https://picsum.photos/200', + alt: 'Avatar image', + size: 'lg', + }, +}; + +export const Clickable: Story = { + args: { + src: 'https://picsum.photos/200', + alt: 'Clickable Avatar', + size: 'md', + onClick: () => alert('Avatar clicked!'), + }, +}; diff --git a/src/components/avatar/Avatar.test.tsx b/src/components/avatar/Avatar.test.tsx new file mode 100644 index 00000000..9d7543fc --- /dev/null +++ b/src/components/avatar/Avatar.test.tsx @@ -0,0 +1,21 @@ +import { render, screen } from '@testing-library/react'; +import Avatar from './Avatar'; +import userEvent from '@testing-library/user-event'; + +describe('Avatar 컴포넌트', () => { + const defaultProps = { + src: '/test-image.jpg', + alt: '테스트 이미지', + }; + + it('onClick 이벤트가 정상적으로 동작해야 한다', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + render(); + + const avatarContainer = screen.getByRole('img').parentElement; + await user.click(avatarContainer!); + + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/avatar/Avatar.tsx b/src/components/avatar/Avatar.tsx new file mode 100644 index 00000000..f445c9a6 --- /dev/null +++ b/src/components/avatar/Avatar.tsx @@ -0,0 +1,24 @@ +import { AVATAR_SIZE, AvatarSize } from '@/constants'; +import Image from 'next/image'; +import { HTMLAttributes } from 'react'; + +interface AvatarProps extends HTMLAttributes { + src: string; + alt: string; + size?: AvatarSize; + onClick?: () => void; +} + +function Avatar({ src, alt, size = 'sm', onClick, ...props }: AvatarProps) { + return ( +
+ {alt} +
+ ); +} + +export default Avatar; diff --git a/src/constants/avatar.ts b/src/constants/avatar.ts new file mode 100644 index 00000000..ddbbc6f6 --- /dev/null +++ b/src/constants/avatar.ts @@ -0,0 +1,7 @@ +export const AVATAR_SIZE = { + sm: 'h-[29px] w-[29px]', + md: 'h-[40px] w-[40px]', + lg: 'h-[56px] w-[56px]', +} as const; + +export type AvatarSize = keyof typeof AVATAR_SIZE; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 00000000..a7424ca3 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1 @@ +export * from './avatar'; From 68de6ce7f5693898a5f751ab7ce96210a3f0df35 Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:11:04 +0900 Subject: [PATCH 15/47] =?UTF-8?q?=E2=9C=A8[Feat]=20CardList=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EB=B0=9C=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨[Feat] 아이콘 컴포넌트 생성 * ✨[Feat] 컴포넌트 모바일 디자인 개발 * ✅ [Test] 스토리북 및 preview 추가 #20 * 💄 [Design] padding 변경 #20 * ♻️[Refactor] tailwind config 수정 * ♻️[Refactor] textchip props 이름 변경 * 💄[Design] 반응형 적용, break point 적용 * ✨[Feat] 찜 아이콘 props 및 clickevent props 추가 * 💄[Design] boxsizing 추가 * ✨[Feat] 마감 지우기 아이콘 개발 * 💄[Design] 마감 오버레이 적용 * ✅[Test] 스토리북 여러 환경 테스트 * ♻️[Refactor] props 이름 변경 * ✅[Test] test 코드 작성 * ♻️[Refactor] 레이아웃 코드 수정, 주석 변경 * ✅[Test] 오버레이 테스트 코드 변경 * ✅[Test] 스토리북 코드 변경 * 💄[Design] text css 조정 * 🚚[Rename] 컴포넌트 이름 변경 * ✨[Feat] 아이콘 컴포넌트 생성 * ✨[Feat] 컴포넌트 모바일 디자인 개발 * ✅ [Test] 스토리북 및 preview 추가 #20 * 💄 [Design] padding 변경 #20 * ♻️[Refactor] textchip props 이름 변경 * 💄[Design] 반응형 적용, break point 적용 * ✨[Feat] 찜 아이콘 props 및 clickevent props 추가 * 💄[Design] boxsizing 추가 * ✨[Feat] 마감 지우기 아이콘 개발 * 💄[Design] 마감 오버레이 적용 * ✅[Test] 스토리북 여러 환경 테스트 * ♻️[Refactor] props 이름 변경 * ✅[Test] test 코드 작성 * ♻️[Refactor] 레이아웃 코드 수정, 주석 변경 * ✅[Test] 오버레이 테스트 코드 변경 * ✅[Test] 스토리북 코드 변경 * 💄[Design] text css 조정 * 🚚[Rename] 컴포넌트 이름 변경 * 🔥[Remove] 불필요 구문 제거 --- public/icons/HeartIcon.tsx | 42 +++++ public/icons/RightArrow.tsx | 35 ++++ public/icons/WaveIcon.tsx | 52 ++++++ public/icons/index.ts | 3 + src/components/card/Card.stories.tsx | 125 +++++++++++++++ src/components/card/Card.test.tsx | 94 +++++++++++ src/components/card/Card.tsx | 149 ++++++++++++++++++ .../ParticipantCounter.tsx | 2 +- src/components/text-chip/TextChip.stories.tsx | 12 +- src/components/text-chip/TextChip.test.tsx | 6 +- src/components/text-chip/TextChip.tsx | 8 +- 11 files changed, 514 insertions(+), 14 deletions(-) create mode 100644 public/icons/HeartIcon.tsx create mode 100644 public/icons/RightArrow.tsx create mode 100644 public/icons/WaveIcon.tsx create mode 100644 src/components/card/Card.stories.tsx create mode 100644 src/components/card/Card.test.tsx create mode 100644 src/components/card/Card.tsx diff --git a/public/icons/HeartIcon.tsx b/public/icons/HeartIcon.tsx new file mode 100644 index 00000000..74984836 --- /dev/null +++ b/public/icons/HeartIcon.tsx @@ -0,0 +1,42 @@ +import { SVGProps } from 'react'; + +interface HeartIconProps extends SVGProps { + width?: number; + height?: number; + isActive?: boolean; +} + +function HeartIcon({ + width = 48, + height = 48, + isActive = false, + ...props +}: HeartIconProps) { + return ( + + + + + ); +} + +export default HeartIcon; diff --git a/public/icons/RightArrow.tsx b/public/icons/RightArrow.tsx new file mode 100644 index 00000000..9f79fb23 --- /dev/null +++ b/public/icons/RightArrow.tsx @@ -0,0 +1,35 @@ +import { SVGProps } from 'react'; + +interface RightArrowProps extends SVGProps { + width?: number; + height?: number; + strokeColor?: string; +} + +function RightArrow({ + width = 18, + height = 18, + strokeColor = '#EA580C', + ...props +}: RightArrowProps) { + return ( + + + + ); +} + +export default RightArrow; diff --git a/public/icons/WaveIcon.tsx b/public/icons/WaveIcon.tsx new file mode 100644 index 00000000..4bf5c6b6 --- /dev/null +++ b/public/icons/WaveIcon.tsx @@ -0,0 +1,52 @@ +import { SVGProps } from 'react'; + +interface WaveIconProps extends SVGProps { + width?: number; + height?: number; +} + +function WaveIcon({ width = 20, height = 24, ...props }: WaveIconProps) { + return ( + + + + + + + + ); +} + +export default WaveIcon; diff --git a/public/icons/index.ts b/public/icons/index.ts index 26a5c656..5e20c43f 100644 --- a/public/icons/index.ts +++ b/public/icons/index.ts @@ -1,2 +1,5 @@ export { default as IcCheck } from './IcCheck'; export { default as RatingIcon } from './RatingIcon'; +export { default as HeartIcon } from './HeartIcon'; +export { default as RightArrow } from './RightArrow'; +export { default as WaveIcon } from './WaveIcon'; diff --git a/src/components/card/Card.stories.tsx b/src/components/card/Card.stories.tsx new file mode 100644 index 00000000..7109e781 --- /dev/null +++ b/src/components/card/Card.stories.tsx @@ -0,0 +1,125 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Card from './Card'; + +const meta = { + title: 'Components/Card', + component: Card, + parameters: { + layout: 'centered', + }, + argTypes: { + onClick: { action: 'clicked' }, + onLikeToggleClick: { action: 'like toggled' }, + onJoinClick: { action: 'join clicked' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const defaultArgs = { + title: '테스트 모임', + location: '서울 강남구', + date: '2024.04.01', + time: '19:00', + currentParticipants: 5, + maxParticipants: 10, + imageUrl: 'https://picsum.photos/200/300', +}; + +// 뷰포트별 데코레이터 설정 +const viewportDecorators = { + mobile: { + viewport: { defaultViewport: 'mobile' }, + width: 'w-[330px]', + }, + tablet: { + viewport: { defaultViewport: 'tablet' }, + width: 'w-[700px]', + }, + desktop: { + viewport: { defaultViewport: 'desktop' }, + width: 'w-[1020px]', + }, +}; + +// 모바일 스토리 +export const Mobile_Default: Story = { + args: defaultArgs, + parameters: { + viewport: viewportDecorators.mobile.viewport, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export const Mobile_Ended: Story = { + args: { + ...defaultArgs, + isEnded: true, + isLiked: true, + }, + parameters: { + viewport: viewportDecorators.mobile.viewport, + }, + decorators: Mobile_Default.decorators, +}; + +// 태블릿 스토리 +export const Tablet_Default: Story = { + args: defaultArgs, + parameters: { + viewport: viewportDecorators.tablet.viewport, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export const Tablet_Ended: Story = { + args: { + ...defaultArgs, + isEnded: true, + isLiked: true, + }, + parameters: { + viewport: viewportDecorators.tablet.viewport, + }, + decorators: Tablet_Default.decorators, +}; + +// 데스크톱 스토리 +export const Desktop_Default: Story = { + args: defaultArgs, + parameters: { + viewport: viewportDecorators.desktop.viewport, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export const Desktop_Ended: Story = { + args: { + ...defaultArgs, + isEnded: true, + isLiked: true, + }, + parameters: { + viewport: viewportDecorators.desktop.viewport, + }, + decorators: Desktop_Default.decorators, +}; diff --git a/src/components/card/Card.test.tsx b/src/components/card/Card.test.tsx new file mode 100644 index 00000000..710e181d --- /dev/null +++ b/src/components/card/Card.test.tsx @@ -0,0 +1,94 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Card from './Card'; +import '@testing-library/jest-dom'; + +const defaultProps = { + title: '테스트 모임', + location: '서울 강남구', + date: '2024.04.01', + time: '19:00', + currentParticipants: 5, + maxParticipants: 10, + imageUrl: '/test-image.jpg', +}; + +describe('Card', () => { + it('기본 정보가 올바르게 렌더링되어야 함', () => { + render(); + + expect(screen.getByText('테스트 모임')).toBeInTheDocument(); + expect(screen.getByText('서울 강남구')).toBeInTheDocument(); + expect(screen.getByText('2024.04.01')).toBeInTheDocument(); + expect(screen.getByText('19:00')).toBeInTheDocument(); + }); + + it('클릭 이벤트가 발생하면 onClick 핸들러가 호출되어야 함', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + render(); + + const article = screen.getByRole('article'); + await user.click(article); + + expect(handleClick).toHaveBeenCalled(); + }); + + it('좋아요 버튼 클릭 시 이벤트 전파가 중단되고 onLikeToggleClick이 호출되어야 함', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + const handleLikeToggleClick = jest.fn(); + + render( + , + ); + + const likeButton = screen.getByRole('button', { name: /좋아요/i }); + await user.click(likeButton); + + expect(handleLikeToggleClick).toHaveBeenCalled(); + expect(handleClick).not.toHaveBeenCalled(); + }); + + it('참여하기 버튼 클릭 시 이벤트 전파가 중단되고 onJoinClick이 호출되어야 함', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + const handleJoinClick = jest.fn(); + + render( + , + ); + + const joinButton = screen.getByRole('button', { name: /join now/i }); + await user.click(joinButton); + + expect(handleJoinClick).toHaveBeenCalled(); + expect(handleClick).not.toHaveBeenCalled(); + }); + + it('마감된 상태일 때 오버레이가 표시되어야 함', () => { + render(); + + expect(screen.getByText(/마감된 챌린지에요.*다음 기회에 만나요/i)); + }); + + it('확정된 상태일 때 확정 라벨이 표시되어야 함', () => { + render(); + + expect(screen.getByText(/확정/)).toBeInTheDocument(); + }); + + it('참가자 수가 올바르게 표시되어야 함', () => { + render(); + + expect(screen.getByText('5/10')).toBeInTheDocument(); + }); +}); diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx new file mode 100644 index 00000000..f0caad17 --- /dev/null +++ b/src/components/card/Card.tsx @@ -0,0 +1,149 @@ +import Image from 'next/image'; +import { TextChip } from '../text-chip/TextChip'; +import ParticipantCounter from '../participant-counter/ParticipantCounter'; +import ConfirmedLabel from '../confirmed-label/ConfirmedLabel'; +import ProgressBar from '../progress-bar/ProgressBar'; +import { HeartIcon, RightArrow, WaveIcon } from '../../../public/icons'; + +interface CardProps { + title: string; + location: string; + date: string; + time: string; + currentParticipants: number; + maxParticipants: number; + isConfirmed?: boolean; + isLiked?: boolean; + isEnded?: boolean; + imageUrl: string; + onClick?: () => void; + onLikeToggleClick?: () => void; + onJoinClick?: () => void; +} + +function Card({ + title, + location, + date, + time, + currentParticipants, + maxParticipants, + isConfirmed = false, + isLiked = true, + isEnded = false, + imageUrl, + onClick, + onLikeToggleClick, + onJoinClick, +}: CardProps) { + return ( +
+
+ {title} +
+ +
+ {/* 상단 섹션: 제목/위치, 찜하기 아이콘, 날짜/시간 칩 */} +
+
+
+

{title}

+ | + + {location} + +
+
+ + +
+
+ +
+ + {/* 하단 섹션: 인원, progressBar, 참가 버튼 */} +
+
+
+ + {isConfirmed && } +
+ +
+ +
+ +
+
+
+ + {/* 마감 오버레이 */} + {isEnded && ( + <> +
+
+

+ {'마감된 챌린지에요,\n다음 기회에 만나요 🙏'} +

+ {isLiked && ( + + )} +
+
+ + {/* 태블릿, 데스크톱 레이아웃 우측 상단 찜하기 아이콘 */} + {isLiked && ( +
+ +
+ )} + + )} +
+ ); +} + +export default Card; diff --git a/src/components/participant-counter/ParticipantCounter.tsx b/src/components/participant-counter/ParticipantCounter.tsx index 84f07560..3b5b236c 100644 --- a/src/components/participant-counter/ParticipantCounter.tsx +++ b/src/components/participant-counter/ParticipantCounter.tsx @@ -30,7 +30,7 @@ function ParticipantCounter({ current, max }: ParticipantCounterProps) { {`${displayCount}/${max}`}
); diff --git a/src/components/text-chip/TextChip.stories.tsx b/src/components/text-chip/TextChip.stories.tsx index 13aee917..9e8e835c 100644 --- a/src/components/text-chip/TextChip.stories.tsx +++ b/src/components/text-chip/TextChip.stories.tsx @@ -12,26 +12,26 @@ type Story = StoryObj; export const Default: Story = { args: { text: '기본', - isDueSoon: false, + isTime: false, }, }; -export const DueSoon: Story = { +export const Time: Story = { args: { - text: '마감임박', - isDueSoon: true, + text: '17:30', + isTime: true, }, }; export const MultipleChips: Story = { args: { text: '', - isDueSoon: false, + isTime: false, }, render: () => (
- +
), }; diff --git a/src/components/text-chip/TextChip.test.tsx b/src/components/text-chip/TextChip.test.tsx index 353455b5..1de7fed8 100644 --- a/src/components/text-chip/TextChip.test.tsx +++ b/src/components/text-chip/TextChip.test.tsx @@ -15,9 +15,9 @@ describe('TextChip', () => { expect(chip).toHaveClass(COLORS.default, COLORS.background); }); - it('isDueSoon이 true일 때 텍스트 색상이 변경되어야 한다.', () => { - render(); + it('isTime이 true일 때 텍스트 색상이 변경되어야 한다.', () => { + render(); const chip = screen.getByRole('text-chip'); - expect(chip).toHaveClass(COLORS.dueSoon, COLORS.background); + expect(chip).toHaveClass(COLORS.isTime, COLORS.background); }); }); diff --git a/src/components/text-chip/TextChip.tsx b/src/components/text-chip/TextChip.tsx index 4bddf36c..7bdade10 100644 --- a/src/components/text-chip/TextChip.tsx +++ b/src/components/text-chip/TextChip.tsx @@ -1,21 +1,21 @@ export const COLORS = { background: 'bg-gray-900', default: 'text-white', - dueSoon: 'text-orange-600', + isTime: 'text-orange-600', } as const; interface TextChipProps { text: string; - isDueSoon?: boolean; + isTime?: boolean; } -export function TextChip({ text, isDueSoon = false }: TextChipProps) { +export function TextChip({ text, isTime = false }: TextChipProps) { return (
{text} From f7740af8b9f31b972bdf64f30f5c3f9b083508db Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:47:48 +0900 Subject: [PATCH 16/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20tailwind?= =?UTF-8?q?=20config=20=EC=88=98=EC=A0=95=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tailwind.config.ts | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/tailwind.config.ts b/tailwind.config.ts index 83f3f52d..ec01778d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -13,27 +13,28 @@ const config: Config = { background: 'var(--background)', foreground: 'var(--foreground)', gray: { - light: '#fefefe', - 'light-hover': '#fdfdfd', - 'light-active': '#fafbfb', - normal: '#f0f1f3', - 'normal-hover': '#d8d9db', - 'normal-active': '#c0c1c2', - dark: '#b4b5b6', - 'dark-hover': '#909192', - 'dark-active': '#6c6c6d', + white: '#ffffff', + 'light-01': '#fdfdfd', + 'light-02': '#fafbfb', + 'normal-01': '#f0f1f3', + 'normal-02': '#d8d9db', + 'normal-03': '#c0c1c2', + 'dark-01': '#b4b5b6', + 'dark-02': '#909192', + 'dark-03': '#6c6c6d', darker: '#545455', + black: '#222222', }, green: { - light: '#e6f6f4', - 'light-hover': '#d9f2ef', - 'light-active': '#b0e4dd', - normal: '#00a991', - 'normal-hover': '#009883', - 'normal-active': '#008774', - dark: '#007f6d', - 'dark-hover': '#006557', - 'dark-active': '#004c41', + 'light-01': '#e6f6f4', + 'light-02': '#d9f2ef', + 'light-03': '#b0e4dd', + 'normal-01': '#00a991', + 'normal-02': '#009883', + 'normal-03': '#008774', + 'dark-01': '#007f6d', + 'dark-02': '#006557', + 'dark-03': '#004c41', darker: '#003b33', }, }, From 02157c77e8633a86b5bc9399585938c34ea40ed3 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:26:30 +0900 Subject: [PATCH 17/47] =?UTF-8?q?=E2=9C=A8[Feat]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20#34=20(#4?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Chore] zod, react-hook-form 설치 #34 * [Feat] label과 input으로 이루어진 입력 FormField 구현 #34 * [Feat] 제출 버튼 구현 #34 * [Feat] 로그인 입력 폼 UI 작업 완료 #34 * [Feat] 비밀번호 가리기 / 보기 기능 구현 #34 * [Test] LoginForm 테스트코드, storybook 작성 #35 * [Test] FormField 테스트코드, storybook 작성 #35 * [Test] SubmitButton 테스트코드, storybook 작성 #35 * [Rename] 각 부분 폴더 생성 #34 * [Feat] 이메일, 비밀번호 제출 기능 구현 #34 * [Test] 로그인 버튼 누르고 isSubmitting 상태인 경우 버튼 비활성화되는지 테스트코드 작성 #35 * [Test] 아이디, 비밀번호 제출되는지 테스트 코드 작성 #35 * [Feat] 이메일, 비밀번호 zod로 유효성 검사 구현 #34 * [Refactor] 비밀번호 가리기 hook으로 분리 #34 * ✨[Feat] 로그인 폼 유효성 검사 통과 시 버튼 활성화 및 색상 변경 #34 * ✨[Feat] 로그인 실패 시 에러 메시지 표시 기능 구현 #40 * ✨[Feat] 로그인 성공시 localstorage에 토큰 저장 #40 * ✨[Feat] 토큰 만료시간 지나면 토큰 삭제 기능 구현 #40 * ♻️[Refactor] 토큰 auth로만 관리 #40 * [Feat] 로그인 성공시 홈으로 리디렉션 #40 * ♻️[Refactor] 토큰 쿠키에 저장으로 변경 #34 * ♻️[Refactor] 에러메시지 상수화 #34 * ✨[Feat] 페이지별 권한에 따른 접근 처리 기능 구현 #43 (#44) * ✨[Feat] 페이지별 권한에 따른 접근 처리 기능 구현 #43 * 🐛[Fix] 빌드에러 useSearchParams Suspense 경계 추가 #43 * ♻️[Refactor] AUTH_REQUIRED_PATHS에서 교환하기 페이지 삭제 * ♻️[Refactor] placeholder 텍스트 상수로 분리 * ♻️[Refactor] onSubmit hook으로 분리 * 🐛[Fix] 충돌 해결 --- .eslintrc.json | 5 +- package-lock.json | 37 ++++++++++++ package.json | 3 + public/icons/index.ts | 2 + public/icons/visibility_off.tsx | 22 +++++++ public/icons/visibility_on.tsx | 22 +++++++ src/api/axios.ts | 0 src/app/login/page.tsx | 11 +++- src/components/header/HeaderBar.tsx | 13 ++++- src/features/auth/api/auth.ts | 23 ++++++++ src/features/auth/api/index.ts | 1 - .../form-field/FormField.stories.tsx | 28 +++++++++ .../components/form-field/FormField.test.tsx | 57 ++++++++++++++++++ .../auth/components/form-field/FormField.tsx | 58 +++++++++++++++++++ src/features/auth/components/index.ts | 1 - .../submit-button/SubmitButton.stories.tsx | 16 +++++ .../submit-button/SubmitButton.test.tsx | 25 ++++++++ .../components/submit-button/SubmitButton.tsx | 27 +++++++++ src/features/auth/constants/messages.ts | 10 ++++ src/features/auth/container/index.ts | 1 - .../login-form/LoginForm.stories.tsx | 12 ++++ .../container/login-form/LoginForm.test.tsx | 57 ++++++++++++++++++ .../auth/container/login-form/LoginForm.tsx | 57 ++++++++++++++++++ src/features/auth/hooks/index.ts | 1 - src/features/auth/hooks/useLoginSubmit.ts | 56 ++++++++++++++++++ .../auth/hooks/usePasswordVisibility.ts | 13 +++++ src/features/auth/types/index.ts | 1 - src/features/auth/types/loginFormSchema.ts | 13 +++++ src/features/auth/utils/cookies.ts | 16 +++++ src/lib/utils/apiClient.ts | 11 ++-- src/middleware.ts | 28 +++++++++ src/store/authStore.ts | 19 ++++++ src/store/index.ts | 1 - 33 files changed, 632 insertions(+), 15 deletions(-) create mode 100644 public/icons/visibility_off.tsx create mode 100644 public/icons/visibility_on.tsx create mode 100644 src/api/axios.ts create mode 100644 src/features/auth/api/auth.ts delete mode 100644 src/features/auth/api/index.ts create mode 100644 src/features/auth/components/form-field/FormField.stories.tsx create mode 100644 src/features/auth/components/form-field/FormField.test.tsx create mode 100644 src/features/auth/components/form-field/FormField.tsx delete mode 100644 src/features/auth/components/index.ts create mode 100644 src/features/auth/components/submit-button/SubmitButton.stories.tsx create mode 100644 src/features/auth/components/submit-button/SubmitButton.test.tsx create mode 100644 src/features/auth/components/submit-button/SubmitButton.tsx create mode 100644 src/features/auth/constants/messages.ts delete mode 100644 src/features/auth/container/index.ts create mode 100644 src/features/auth/container/login-form/LoginForm.stories.tsx create mode 100644 src/features/auth/container/login-form/LoginForm.test.tsx create mode 100644 src/features/auth/container/login-form/LoginForm.tsx delete mode 100644 src/features/auth/hooks/index.ts create mode 100644 src/features/auth/hooks/useLoginSubmit.ts create mode 100644 src/features/auth/hooks/usePasswordVisibility.ts delete mode 100644 src/features/auth/types/index.ts create mode 100644 src/features/auth/types/loginFormSchema.ts create mode 100644 src/features/auth/utils/cookies.ts create mode 100644 src/middleware.ts create mode 100644 src/store/authStore.ts delete mode 100644 src/store/index.ts diff --git a/.eslintrc.json b/.eslintrc.json index 786aab69..55e7e8e6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,9 +1,10 @@ { "extends": ["next/core-web-vitals", "prettier"], - "plugins": ["prettier", "react-hooks"], + "plugins": ["prettier", "react-hooks", "@typescript-eslint"], "rules": { "prettier/prettier": "error", - "no-unused-vars": "error" + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error" }, "settings": { "import/resolver": { diff --git a/package-lock.json b/package-lock.json index d9563012..41cdbc4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,16 @@ "version": "0.1.0", "dependencies": { "@headlessui/react": "^2.2.0", + "@hookform/resolvers": "^3.9.1", "@tanstack/react-query": "^5.61.3", "@tanstack/react-query-devtools": "^5.61.3", "axios": "^1.7.8", "next": "15.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.2", "tailwind-merge": "^2.5.5", + "zod": "^3.23.8", "zustand": "^5.0.1" }, "devDependencies": { @@ -2755,6 +2758,15 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -15880,6 +15892,22 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.53.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.2.tgz", + "integrity": "sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -19026,6 +19054,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zustand": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", diff --git a/package.json b/package.json index 9ff8b551..11e07d75 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@headlessui/react": "^2.2.0", + "@hookform/resolvers": "^3.9.1", "@tanstack/react-query": "^5.61.3", "@tanstack/react-query-devtools": "^5.61.3", "axios": "^1.7.8", @@ -34,6 +35,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "tailwind-merge": "^2.5.5", + "react-hook-form": "^7.53.2", + "zod": "^3.23.8", "zustand": "^5.0.1" }, "devDependencies": { diff --git a/public/icons/index.ts b/public/icons/index.ts index 5e20c43f..e10398dd 100644 --- a/public/icons/index.ts +++ b/public/icons/index.ts @@ -3,3 +3,5 @@ export { default as RatingIcon } from './RatingIcon'; export { default as HeartIcon } from './HeartIcon'; export { default as RightArrow } from './RightArrow'; export { default as WaveIcon } from './WaveIcon'; +export { default as VisibilityOn } from './visibility_on'; +export { default as VisibilityOff } from './visibility_off'; diff --git a/public/icons/visibility_off.tsx b/public/icons/visibility_off.tsx new file mode 100644 index 00000000..669725f1 --- /dev/null +++ b/public/icons/visibility_off.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from 'react'; +interface IcCheckProps extends SVGProps { + width?: number; + height?: number; +} +function VisibilityOff({ width = 24, height = 24 }: IcCheckProps) { + return ( + + + + ); +} +export default VisibilityOff; diff --git a/public/icons/visibility_on.tsx b/public/icons/visibility_on.tsx new file mode 100644 index 00000000..408068b1 --- /dev/null +++ b/public/icons/visibility_on.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from 'react'; +interface IcCheckProps extends SVGProps { + width?: number; + height?: number; +} +function VisibilityOn({ width = 24, height = 24 }: IcCheckProps) { + return ( + + + + ); +} +export default VisibilityOn; diff --git a/src/api/axios.ts b/src/api/axios.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 16696478..ea94ab60 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,4 +1,13 @@ +import { Suspense } from 'react'; +import LoginForm from '@/features/auth/container/login-form/LoginForm'; + function Login() { - return
Login
; + return ( +
+ Loading...
}> + + +
+ ); } export default Login; diff --git a/src/components/header/HeaderBar.tsx b/src/components/header/HeaderBar.tsx index 80f92f49..abbc84c5 100644 --- a/src/components/header/HeaderBar.tsx +++ b/src/components/header/HeaderBar.tsx @@ -1,9 +1,16 @@ 'use client'; -import React from 'react'; +import React, { useEffect } from 'react'; import NavButton from './NavButton'; +import { useAuthStore } from '@/store/authStore'; function HeaderBar() { + const { isLoggedIn, checkLoginStatus } = useAuthStore(); + + useEffect(() => { + checkLoginStatus(); + }, [checkLoginStatus]); + const navItems = [ { href: '/', label: '홈' }, { href: '/bookclub', label: '책 모임' }, @@ -23,7 +30,9 @@ function HeaderBar() {
- 로그인 + + {isLoggedIn ? '프로필' : '로그인'} +
diff --git a/src/features/auth/api/auth.ts b/src/features/auth/api/auth.ts new file mode 100644 index 00000000..15c7f0b9 --- /dev/null +++ b/src/features/auth/api/auth.ts @@ -0,0 +1,23 @@ +import apiClient from '@/lib/utils/apiClient'; +import { LoginFormData } from '../types/loginFormSchema'; +import { useAuthStore } from '@/store/authStore'; +import { setCookie } from '@/features/auth/utils/cookies'; + +export const login = async (data: LoginFormData) => { + try { + const response = await apiClient.post<{ token: string }>( + '/auths/signin', + data, + ); + const { token } = response.data; + + setCookie('auth_token', token); + const { setIsLoggedIn } = useAuthStore.getState(); + setIsLoggedIn(true); + + return response.data; + } catch (error) { + console.error('로그인 에러:', error); + throw error; + } +}; diff --git a/src/features/auth/api/index.ts b/src/features/auth/api/index.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/features/auth/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/features/auth/components/form-field/FormField.stories.tsx b/src/features/auth/components/form-field/FormField.stories.tsx new file mode 100644 index 00000000..ef71fe08 --- /dev/null +++ b/src/features/auth/components/form-field/FormField.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import FormField from './FormField'; + +const meta: Meta = { + title: 'Auth/FormField', + component: FormField, +}; + +export default meta; +type Story = StoryObj; + +export const EmailField: Story = { + args: { + label: '아이디', + type: 'text', + placeholder: '이메일을 입력해주세요', + id: 'email', + }, +}; + +export const PasswordField: Story = { + args: { + label: '비밀번호', + type: 'password', + placeholder: '비밀번호를 입력해주세요.', + id: 'password', + }, +}; diff --git a/src/features/auth/components/form-field/FormField.test.tsx b/src/features/auth/components/form-field/FormField.test.tsx new file mode 100644 index 00000000..f13d71d9 --- /dev/null +++ b/src/features/auth/components/form-field/FormField.test.tsx @@ -0,0 +1,57 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import FormField from './FormField'; + +describe('FormField', () => { + const defaultProps = { + label: '아이디', + type: 'text', + placeholder: '이메일을 입력해주세요', + id: 'email', + register: { + onChange: jest.fn(), + onBlur: jest.fn(), + ref: jest.fn(), + name: 'email', + }, + }; + + it('label과 input이 올바르게 렌더링되어야 한다', () => { + render(); + + expect(screen.getByLabelText('아이디')).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('이메일을 입력해주세요'), + ).toBeInTheDocument(); + }); + + describe('password type input', () => { + const passwordProps = { + ...defaultProps, + type: 'password', + label: '비밀번호', + placeholder: '비밀번호를 입력해주세요', + id: 'password', + register: { + onChange: jest.fn(), + onBlur: jest.fn(), + ref: jest.fn(), + name: 'password', + }, + }; + + it('토글 버튼 클릭 시 input type이 변경되어야 한다', async () => { + render(); + + const input = screen.getByPlaceholderText('비밀번호를 입력해주세요'); + const toggleButton = screen.getByRole('button'); + + await userEvent.click(toggleButton); + expect(input).toHaveAttribute('type', 'text'); + + await userEvent.click(toggleButton); + expect(input).toHaveAttribute('type', 'password'); + }); + }); +}); diff --git a/src/features/auth/components/form-field/FormField.tsx b/src/features/auth/components/form-field/FormField.tsx new file mode 100644 index 00000000..046792b5 --- /dev/null +++ b/src/features/auth/components/form-field/FormField.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { VisibilityOn, VisibilityOff } from '../../../../../public/icons'; +import { UseFormRegisterReturn } from 'react-hook-form'; +import { usePasswordVisibility } from '../../hooks/usePasswordVisibility'; + +interface FormFieldProps { + label: string; + type: string; + placeholder: string; + id: string; + register: UseFormRegisterReturn; + error?: string; +} + +function FormField({ + label, + type, + placeholder, + id, + register, + error, +}: FormFieldProps) { + const { showPassword, togglePassword, passwordType } = + usePasswordVisibility(); + + return ( +
+ +
+ + {type === 'password' && ( + + )} +
+ {error &&

{error}

} +
+ ); +} + +export default FormField; diff --git a/src/features/auth/components/index.ts b/src/features/auth/components/index.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/features/auth/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/features/auth/components/submit-button/SubmitButton.stories.tsx b/src/features/auth/components/submit-button/SubmitButton.stories.tsx new file mode 100644 index 00000000..8eb4bf84 --- /dev/null +++ b/src/features/auth/components/submit-button/SubmitButton.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import SubmitButton from './SubmitButton'; + +const meta: Meta = { + title: 'Auth/SubmitButton', + component: SubmitButton, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: '로그인', + }, +}; diff --git a/src/features/auth/components/submit-button/SubmitButton.test.tsx b/src/features/auth/components/submit-button/SubmitButton.test.tsx new file mode 100644 index 00000000..cd7af184 --- /dev/null +++ b/src/features/auth/components/submit-button/SubmitButton.test.tsx @@ -0,0 +1,25 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import SubmitButton from './SubmitButton'; + +describe('SubmitButton', () => { + it('버튼이 올바르게 렌더링되어야 한다', () => { + render(테스트 버튼); + + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('type', 'submit'); + }); + + it('children prop이 올바르게 표시되어야 한다', () => { + render(로그인); + + expect(screen.getByText('로그인')).toBeInTheDocument(); + }); + + it('isSubmitting prop이 true일 때 버튼이 비활성화되어야 한다', () => { + render(로그인); + + expect(screen.getByRole('button')).toBeDisabled(); + }); +}); diff --git a/src/features/auth/components/submit-button/SubmitButton.tsx b/src/features/auth/components/submit-button/SubmitButton.tsx new file mode 100644 index 00000000..aad48eab --- /dev/null +++ b/src/features/auth/components/submit-button/SubmitButton.tsx @@ -0,0 +1,27 @@ +interface SubmitButtonProps { + children: React.ReactNode; + isSubmitting?: boolean; + disabled?: boolean; +} + +function SubmitButton({ + children, + isSubmitting = false, + disabled = false, +}: SubmitButtonProps) { + return ( + + ); +} + +export default SubmitButton; diff --git a/src/features/auth/constants/messages.ts b/src/features/auth/constants/messages.ts new file mode 100644 index 00000000..c893786b --- /dev/null +++ b/src/features/auth/constants/messages.ts @@ -0,0 +1,10 @@ +export const AUTH_ERROR_MESSAGES = { + USER_NOT_FOUND: '존재하지 않는 아이디입니다.', + INVALID_CREDENTIALS: '비밀번호가 아이디와 일치하지 않습니다.', + SERVER_ERROR: '서버 오류가 발생했습니다', +} as const; + +export const LOGIN_FORM_PLACEHOLDERS = { + EMAIL: '이메일을 입력해주세요', + PASSWORD: '비밀번호를 입력해주세요.', +} as const; diff --git a/src/features/auth/container/index.ts b/src/features/auth/container/index.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/features/auth/container/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/features/auth/container/login-form/LoginForm.stories.tsx b/src/features/auth/container/login-form/LoginForm.stories.tsx new file mode 100644 index 00000000..137fd587 --- /dev/null +++ b/src/features/auth/container/login-form/LoginForm.stories.tsx @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import LoginForm from './LoginForm'; + +const meta: Meta = { + title: 'Auth/LoginForm', + component: LoginForm, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/features/auth/container/login-form/LoginForm.test.tsx b/src/features/auth/container/login-form/LoginForm.test.tsx new file mode 100644 index 00000000..2add16e1 --- /dev/null +++ b/src/features/auth/container/login-form/LoginForm.test.tsx @@ -0,0 +1,57 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import LoginForm from './LoginForm'; + +jest.mock('react-hook-form', () => ({ + useForm: () => ({ + register: () => ({}), + handleSubmit: (fn: any) => fn, + formState: { + isSubmitting: false, + errors: {}, + isValid: true, + }, + setError: jest.fn(), + reset: jest.fn(), + }), +})); + +// next/navigation mock +jest.mock('next/navigation', () => ({ + useRouter: () => ({ + replace: jest.fn(), + }), + useSearchParams: () => null, +})); + +describe('LoginForm', () => { + it('폼이 올바르게 렌더링되어야 한다', () => { + render(); + + expect(screen.getByRole('heading', { name: '로그인' })).toBeInTheDocument(); + expect(screen.getByLabelText('아이디')).toBeInTheDocument(); + expect(screen.getByLabelText('비밀번호')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '로그인' })).toBeInTheDocument(); + }); + + it('이메일과 비밀번호를 입력할 수 있어야 한다', async () => { + render(); + + const emailInput = screen.getByLabelText('아이디'); + const passwordInput = screen.getByLabelText('비밀번호'); + + await userEvent.type(emailInput, 'test@example.com'); + await userEvent.type(passwordInput, 'password123'); + + expect(emailInput).toHaveValue('test@example.com'); + expect(passwordInput).toHaveValue('password123'); + }); + + it('로그인 버튼이 제출 가능한 상태여야 한다', () => { + render(); + + const submitButton = screen.getByRole('button', { name: '로그인' }); + expect(submitButton).toBeEnabled(); + }); +}); diff --git a/src/features/auth/container/login-form/LoginForm.tsx b/src/features/auth/container/login-form/LoginForm.tsx new file mode 100644 index 00000000..51cf5e71 --- /dev/null +++ b/src/features/auth/container/login-form/LoginForm.tsx @@ -0,0 +1,57 @@ +'use client'; + +import React from 'react'; +import FormField from '../../components/form-field/FormField'; +import SubmitButton from '../../components/submit-button/SubmitButton'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { loginFormSchema, LoginFormData } from '../../types/loginFormSchema'; +import { useLoginSubmit } from '../../hooks/useLoginSubmit'; + +export default function LoginForm() { + const { + register, + handleSubmit, + setError, + reset, + formState: { isSubmitting, errors, isValid }, + } = useForm({ + resolver: zodResolver(loginFormSchema), + mode: 'onChange', + }); + + const onSubmit = useLoginSubmit(setError, reset); + + return ( +
+
+

로그인

+
+ + + + 로그인 + + +
+
+ ); +} diff --git a/src/features/auth/hooks/index.ts b/src/features/auth/hooks/index.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/features/auth/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/features/auth/hooks/useLoginSubmit.ts b/src/features/auth/hooks/useLoginSubmit.ts new file mode 100644 index 00000000..9b3eef84 --- /dev/null +++ b/src/features/auth/hooks/useLoginSubmit.ts @@ -0,0 +1,56 @@ +import { useRouter, useSearchParams } from 'next/navigation'; +import { UseFormReset, UseFormSetError } from 'react-hook-form'; +import { login } from '../api/auth'; +import { LoginFormData } from '../types/loginFormSchema'; +import { AUTH_ERROR_MESSAGES } from '../constants/messages'; + +type LoginErrorCode = 'USER_NOT_FOUND' | 'INVALID_CREDENTIALS' | 'SERVER_ERROR'; + +interface LoginError { + response: { + data: { + code: LoginErrorCode; + }; + }; +} + +export const useLoginSubmit = ( + setError: UseFormSetError, + reset: UseFormReset, +) => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleSubmit = async (data: LoginFormData) => { + try { + const response = await login(data); + console.log('로그인 성공:', response); + reset(); + + const returnUrl = searchParams.get('returnUrl') || '/'; + router.replace(returnUrl); + } catch (error) { + const { code } = (error as LoginError).response.data; + + switch (code) { + case 'USER_NOT_FOUND': + setError('email', { + type: 'manual', + message: AUTH_ERROR_MESSAGES.USER_NOT_FOUND, + }); + break; + case 'INVALID_CREDENTIALS': + setError('password', { + type: 'manual', + message: AUTH_ERROR_MESSAGES.INVALID_CREDENTIALS, + }); + break; + case 'SERVER_ERROR': + console.error(AUTH_ERROR_MESSAGES.SERVER_ERROR); + break; + } + } + }; + + return handleSubmit; +}; diff --git a/src/features/auth/hooks/usePasswordVisibility.ts b/src/features/auth/hooks/usePasswordVisibility.ts new file mode 100644 index 00000000..fdeac2c8 --- /dev/null +++ b/src/features/auth/hooks/usePasswordVisibility.ts @@ -0,0 +1,13 @@ +import { useState } from 'react'; + +export const usePasswordVisibility = () => { + const [showPassword, setShowPassword] = useState(false); + + const togglePassword = () => setShowPassword((prev) => !prev); + + return { + showPassword, + togglePassword, + passwordType: showPassword ? 'text' : 'password', + } as const; +}; diff --git a/src/features/auth/types/index.ts b/src/features/auth/types/index.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/features/auth/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/features/auth/types/loginFormSchema.ts b/src/features/auth/types/loginFormSchema.ts new file mode 100644 index 00000000..d604ec38 --- /dev/null +++ b/src/features/auth/types/loginFormSchema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const loginFormSchema = z.object({ + email: z + .string() + .min(1, '이메일을 입력해주세요.') + .email('올바른 이메일 형식이 아닙니다.'), + password: z + .string() + .min(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' }), +}); + +export type LoginFormData = z.infer; diff --git a/src/features/auth/utils/cookies.ts b/src/features/auth/utils/cookies.ts new file mode 100644 index 00000000..3b298d45 --- /dev/null +++ b/src/features/auth/utils/cookies.ts @@ -0,0 +1,16 @@ +export const setCookie = (name: string, value: string) => { + document.cookie = `${name}=${value};path=/`; +}; + +export const getCookie = (name: string): string | null => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + return parts.pop()?.split(';').shift() || null; + } + return null; +}; + +export const deleteCookie = (name: string) => { + document.cookie = `${name}=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;`; +}; diff --git a/src/lib/utils/apiClient.ts b/src/lib/utils/apiClient.ts index ecea7a72..2b9dedbc 100644 --- a/src/lib/utils/apiClient.ts +++ b/src/lib/utils/apiClient.ts @@ -1,7 +1,9 @@ import axios from 'axios'; +import { getCookie, deleteCookie } from '@/features/auth/utils/cookies'; +import { useAuthStore } from '@/store/authStore'; const apiClient = axios.create({ - baseURL: 'https://api.example.com/', + baseURL: process.env.NEXT_PUBLIC_API_URL, timeout: 5000, headers: { 'Content-Type': 'application/json', @@ -11,8 +13,7 @@ const apiClient = axios.create({ // Request Interceptor apiClient.interceptors.request.use( (config) => { - // 예: 인증 토큰 추가 - const token = localStorage.getItem('token'); + const token = getCookie('auth_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } @@ -27,8 +28,10 @@ apiClient.interceptors.request.use( apiClient.interceptors.response.use( (response) => response, (error) => { - // 예: 에러 메시지 처리 if (error.response?.status === 401) { + const { setIsLoggedIn } = useAuthStore.getState(); + setIsLoggedIn(false); + deleteCookie('auth_token'); alert('로그인이 필요합니다.'); } return Promise.reject(error); diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 00000000..1b764c86 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,28 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +const AUTH_REQUIRED_PATHS = ['/wish', '/profile']; + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + const token = request.cookies.get('auth_token'); + + if (AUTH_REQUIRED_PATHS.includes(pathname)) { + if (!token) { + const loginUrl = new URL('/login', request.url); + + loginUrl.searchParams.set('returnUrl', pathname); + return NextResponse.redirect(loginUrl); + } + } + + if (pathname === '/login' && token) { + return NextResponse.redirect(new URL('/', request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ['/wish', '/profile', '/login'], +}; diff --git a/src/store/authStore.ts b/src/store/authStore.ts new file mode 100644 index 00000000..2bc4149d --- /dev/null +++ b/src/store/authStore.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; +import { getCookie } from '@/features/auth/utils/cookies'; + +interface AuthState { + isLoggedIn: boolean; + setIsLoggedIn: (value: boolean) => void; + checkLoginStatus: () => void; +} + +export const useAuthStore = create((set) => ({ + isLoggedIn: false, + + setIsLoggedIn: (value: boolean) => set({ isLoggedIn: value }), + + checkLoginStatus: () => { + const token = getCookie('auth_token'); + set({ isLoggedIn: !!token }); + }, +})); diff --git a/src/store/index.ts b/src/store/index.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/store/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; From a07eb03845d6aa90a94550b37e6d4d8a3c88f3f2 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:03:08 +0900 Subject: [PATCH 18/47] =?UTF-8?q?=F0=9F=90=9B[Fix]=20LoginForm=20storybook?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../login-form/LoginForm.stories.tsx | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/features/auth/container/login-form/LoginForm.stories.tsx b/src/features/auth/container/login-form/LoginForm.stories.tsx index 137fd587..07c16169 100644 --- a/src/features/auth/container/login-form/LoginForm.stories.tsx +++ b/src/features/auth/container/login-form/LoginForm.stories.tsx @@ -1,10 +1,37 @@ import type { Meta, StoryObj } from '@storybook/react'; import LoginForm from './LoginForm'; -const meta: Meta = { +const mockUseRouter = () => ({ + useRouter: () => ({ + push: () => {}, + replace: () => {}, + refresh: () => {}, + }), +}); + +const meta = { title: 'Auth/LoginForm', component: LoginForm, -}; + parameters: { + nextjs: { + appDirectory: true, + navigation: { + pathname: '/login', + query: {}, + }, + }, + mockData: { + router: mockUseRouter(), + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; export default meta; type Story = StoryObj; From 979e9bb1f3bd2b7a560098e354e3cd27bae62103 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:08:52 +0900 Subject: [PATCH 19/47] =?UTF-8?q?=F0=9F=92=84[Design]=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=83=89=EC=83=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 💄[Design] 헤더 바 색상 green-normal로 변경 #9 * 💄[Design] 홈 버튼 bookco로 변경 #9 * ♻️[Refactor] NAV_ITEMS 상수로 분리 #9 * 💄[Design] 네비게이션 버튼 클릭시 색상 변경 #9 * ✅[Test] NAV_ITEMS 상수 분리로 인한 테스트 코드 수정 #9 --- src/components/header/HeaderBar.test.tsx | 70 +++++++++++------------- src/components/header/HeaderBar.tsx | 32 ++++------- src/components/header/NavButton.tsx | 9 +-- src/constants/index.ts | 1 + src/constants/navigation.ts | 8 +++ 5 files changed, 57 insertions(+), 63 deletions(-) create mode 100644 src/constants/navigation.ts diff --git a/src/components/header/HeaderBar.test.tsx b/src/components/header/HeaderBar.test.tsx index 3d4a7519..6d64b23b 100644 --- a/src/components/header/HeaderBar.test.tsx +++ b/src/components/header/HeaderBar.test.tsx @@ -1,14 +1,11 @@ import { render, screen } from '@testing-library/react'; import HeaderBar from './HeaderBar'; import '@testing-library/jest-dom'; +import { NAV_ITEMS } from '@/constants/navigation'; -const navigationLinks = [ - { name: '홈' }, - { name: '책 모임' }, - { name: '책 교환' }, - { name: '찜 목록' }, - { name: '로그인' }, -]; +jest.mock('next/navigation', () => ({ + usePathname: jest.fn(() => '/exchange'), +})); describe('HeaderBar 컴포넌트 테스트', () => { beforeEach(() => { @@ -16,45 +13,40 @@ describe('HeaderBar 컴포넌트 테스트', () => { }); describe('네비게이션 링크', () => { - it('모든 네비게이션 링크가 올바르게 렌더링되어야 한다', () => { - navigationLinks.forEach((link) => { - const linkElement = screen.getByRole('link', { name: link.name }); + it('NAV_ITEMS의 모든 링크가 올바르게 렌더링되어야 한다', () => { + NAV_ITEMS.forEach((item) => { + const linkElement = screen.getByRole('link', { name: item.label }); expect(linkElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute('href', item.href); }); }); - it('홈 링크에 올바른 스타일이 적용되어야 한다', () => { - const homeLink = screen.getByRole('link', { name: '홈' }); - expect(homeLink).toHaveClass('hover:scale-105 md:text-base'); + it('로그인 링크가 올바르게 렌더링되어야 한다', () => { + const loginLink = screen.getByRole('link', { name: '로그인' }); + expect(loginLink).toBeInTheDocument(); + expect(loginLink).toHaveAttribute('href', '/login'); }); - }); -}); - -jest.mock('next/navigation', () => ({ - usePathname: () => '/', -})); - -describe('HeaderBar 컴포넌트 테스트', () => { - beforeEach(() => { - render(); - }); - it('책 모임 링크가 올바른 href를 가져야 한다', () => { - const bookclubLink = screen.getByRole('link', { name: '책 모임' }); - expect(bookclubLink).toHaveAttribute('href', '/bookclub'); - }); -}); - -jest.mock('next/navigation', () => ({ - useRouter: jest.fn(), - usePathname: jest.fn(), -})); + it('bookco 링크는 항상 활성화되어야 한다', () => { + const bookcoItem = NAV_ITEMS.find((item) => item.id === 'bookco'); + const bookcoLink = screen.getByRole('link', { name: bookcoItem!.label }); + expect(bookcoLink).toHaveClass('font-bold'); + }); -describe('HeaderBar 네비게이션 테스트', () => { - it('각 네비게이션 버튼 클릭시 올바른 href 속성을 가져야 한다', () => { - render(); + it('현재 경로와 일치하는 링크는 활성화되어야 한다', () => { + const exchangeItem = NAV_ITEMS.find((item) => item.id === 'exchange'); + const exchangeLink = screen.getByRole('link', { + name: exchangeItem!.label, + }); + expect(exchangeLink).toHaveClass('font-bold'); + }); - const homeLink = screen.getByRole('link', { name: '홈' }); - expect(homeLink).toHaveAttribute('href', '/'); + it('현재 경로와 일치하지 않는 링크는 비활성화되어야 한다', () => { + const bookclubItem = NAV_ITEMS.find((item) => item.id === 'bookclub'); + const bookclubLink = screen.getByRole('link', { + name: bookclubItem!.label, + }); + expect(bookclubLink).toHaveClass('text-green-light-01'); + }); }); }); diff --git a/src/components/header/HeaderBar.tsx b/src/components/header/HeaderBar.tsx index abbc84c5..1e1b2f6d 100644 --- a/src/components/header/HeaderBar.tsx +++ b/src/components/header/HeaderBar.tsx @@ -1,38 +1,30 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import NavButton from './NavButton'; -import { useAuthStore } from '@/store/authStore'; +import { NAV_ITEMS } from '@/constants/navigation'; +import { usePathname } from 'next/navigation'; function HeaderBar() { - const { isLoggedIn, checkLoginStatus } = useAuthStore(); - - useEffect(() => { - checkLoginStatus(); - }, [checkLoginStatus]); - - const navItems = [ - { href: '/', label: '홈' }, - { href: '/bookclub', label: '책 모임' }, - { href: '/exchange', label: '책 교환' }, - { href: '/wish', label: '찜 목록' }, - ]; + const pathname = usePathname(); return ( - From 8b5d6f01bf08a032860a680826c7ad777c9103f0 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:34:46 +0900 Subject: [PATCH 28/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20#124=20(#127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️[Refactor] 로그인 버튼 공용 컴포넌트 버튼으로 변경 #124 * 🔥[Remove] 기존 로그인 버튼 삭제 #124 --- .../submit-button/SubmitButton.stories.tsx | 16 ----------- .../submit-button/SubmitButton.test.tsx | 25 ----------------- .../components/submit-button/SubmitButton.tsx | 27 ------------------- .../auth/container/login-form/LoginForm.tsx | 13 ++++++--- 4 files changed, 9 insertions(+), 72 deletions(-) delete mode 100644 src/features/auth/components/submit-button/SubmitButton.stories.tsx delete mode 100644 src/features/auth/components/submit-button/SubmitButton.test.tsx delete mode 100644 src/features/auth/components/submit-button/SubmitButton.tsx diff --git a/src/features/auth/components/submit-button/SubmitButton.stories.tsx b/src/features/auth/components/submit-button/SubmitButton.stories.tsx deleted file mode 100644 index 8eb4bf84..00000000 --- a/src/features/auth/components/submit-button/SubmitButton.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import SubmitButton from './SubmitButton'; - -const meta: Meta = { - title: 'Auth/SubmitButton', - component: SubmitButton, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - children: '로그인', - }, -}; diff --git a/src/features/auth/components/submit-button/SubmitButton.test.tsx b/src/features/auth/components/submit-button/SubmitButton.test.tsx deleted file mode 100644 index cd7af184..00000000 --- a/src/features/auth/components/submit-button/SubmitButton.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import SubmitButton from './SubmitButton'; - -describe('SubmitButton', () => { - it('버튼이 올바르게 렌더링되어야 한다', () => { - render(테스트 버튼); - - const button = screen.getByRole('button'); - expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('type', 'submit'); - }); - - it('children prop이 올바르게 표시되어야 한다', () => { - render(로그인); - - expect(screen.getByText('로그인')).toBeInTheDocument(); - }); - - it('isSubmitting prop이 true일 때 버튼이 비활성화되어야 한다', () => { - render(로그인); - - expect(screen.getByRole('button')).toBeDisabled(); - }); -}); diff --git a/src/features/auth/components/submit-button/SubmitButton.tsx b/src/features/auth/components/submit-button/SubmitButton.tsx deleted file mode 100644 index aad48eab..00000000 --- a/src/features/auth/components/submit-button/SubmitButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -interface SubmitButtonProps { - children: React.ReactNode; - isSubmitting?: boolean; - disabled?: boolean; -} - -function SubmitButton({ - children, - isSubmitting = false, - disabled = false, -}: SubmitButtonProps) { - return ( - - ); -} - -export default SubmitButton; diff --git a/src/features/auth/container/login-form/LoginForm.tsx b/src/features/auth/container/login-form/LoginForm.tsx index 51cf5e71..941030d9 100644 --- a/src/features/auth/container/login-form/LoginForm.tsx +++ b/src/features/auth/container/login-form/LoginForm.tsx @@ -2,7 +2,7 @@ import React from 'react'; import FormField from '../../components/form-field/FormField'; -import SubmitButton from '../../components/submit-button/SubmitButton'; +import Button from '@/components/button/Button'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { loginFormSchema, LoginFormData } from '../../types/loginFormSchema'; @@ -47,9 +47,14 @@ export default function LoginForm() { register={register('password')} error={errors.password?.message} /> - - 로그인 - + + ); +} + +export default SortingButton; From 3e50d19478ec0f08835f48fb6359e9d1dad78129 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:17:17 +0900 Subject: [PATCH 30/47] =?UTF-8?q?=E2=9C=A8[Feat]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#128=20(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨[Feat] 로그아웃 기능 구현 #128 * ♻️[Refactor] 로그아웃 시 리다이렉션 router.push에서 router.replace로 수정 #128 * ♻️[Refactor] 라우팅 변경 #128 * ♻️[Refactor] 스토리북 수정 #128 --- src/components/header/HeaderBar.stories.tsx | 32 +++++++++++++++++++++ src/components/header/HeaderBar.test.tsx | 9 +++++- src/components/header/HeaderBar.tsx | 18 +++++++++--- src/features/auth/api/auth.ts | 15 +++++++++- src/features/auth/hooks/useLoginSubmit.ts | 2 +- 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/components/header/HeaderBar.stories.tsx b/src/components/header/HeaderBar.stories.tsx index 4afdb28a..a47b22fe 100644 --- a/src/components/header/HeaderBar.stories.tsx +++ b/src/components/header/HeaderBar.stories.tsx @@ -1,12 +1,44 @@ import type { Meta, StoryObj } from '@storybook/react'; import HeaderBar from './HeaderBar'; +const mockAuthStore = { + isLoggedIn: true, + user: { + image: 'https://via.placeholder.com/32', + }, +}; + +declare global { + var usePathname: () => string; + var useRouter: () => Record; + var useAuthStore: () => typeof mockAuthStore; +} + const meta: Meta = { title: 'Components/HeaderBar', component: HeaderBar, parameters: { layout: 'fullscreen', + nextjs: { + appDirectory: true, + }, }, + decorators: [ + (Story) => { + global.useRouter = () => ({ + push: () => {}, + replace: () => {}, + prefetch: () => Promise.resolve(), + back: () => {}, + pathname: '/', + route: '/', + }); + global.usePathname = () => '/'; + global.useAuthStore = () => mockAuthStore; + + return ; + }, + ], }; export default meta; diff --git a/src/components/header/HeaderBar.test.tsx b/src/components/header/HeaderBar.test.tsx index 3f61b554..3a0ea83e 100644 --- a/src/components/header/HeaderBar.test.tsx +++ b/src/components/header/HeaderBar.test.tsx @@ -6,10 +6,16 @@ import { useAuthStore } from '@/store/authStore'; jest.mock('next/navigation', () => ({ usePathname: jest.fn(() => '/exchange'), + useRouter: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + back: jest.fn(), + })), })); describe('HeaderBar 컴포넌트 테스트', () => { beforeEach(() => { + useAuthStore.setState({ isLoggedIn: false, user: null }); render(); }); @@ -42,7 +48,7 @@ describe('HeaderBar 컴포넌트 테스트', () => { expect(exchangeLink).toHaveClass('font-bold'); }); - it('현재 경로와 일치하지 않는 링크는 비활성화되어야 한다', () => { + it('현재 경로와 일치하는 않는 링크는 비활성화되어야 한다', () => { const bookclubItem = NAV_ITEMS.find((item) => item.id === 'bookclub'); const bookclubLink = screen.getByRole('link', { name: bookclubItem!.label, @@ -54,6 +60,7 @@ describe('HeaderBar 컴포넌트 테스트', () => { describe('로그인 상태에 따른 버튼 렌더링', () => { beforeEach(() => { + jest.clearAllMocks(); useAuthStore.setState({ isLoggedIn: false, user: null }); }); diff --git a/src/components/header/HeaderBar.tsx b/src/components/header/HeaderBar.tsx index a74ff437..b6084a94 100644 --- a/src/components/header/HeaderBar.tsx +++ b/src/components/header/HeaderBar.tsx @@ -3,17 +3,27 @@ import React from 'react'; import NavButton from './NavButton'; import { NAV_ITEMS } from '@/constants/navigation'; -import { usePathname } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { useAuthStore } from '@/store/authStore'; import DropDown from '../drop-down/DropDown'; +import { logout } from '@/features/auth/api/auth'; function HeaderBar() { const pathname = usePathname(); const { isLoggedIn, user } = useAuthStore(); + const router = useRouter(); - const handleDropDownChange = (value: string | undefined) => { - // 드롭다운 메뉴 선택 처리 - console.log(value); + const handleDropDownChange = async (value: string | undefined) => { + if (value === 'LOGOUT') { + try { + await logout(); + router.replace('/exchange'); + } catch (error) { + console.error('로그아웃 실패:', error); + } + } else if (value === 'MY_PAGE') { + router.push('/profile'); + } }; return ( diff --git a/src/features/auth/api/auth.ts b/src/features/auth/api/auth.ts index 9af23def..a947bcb6 100644 --- a/src/features/auth/api/auth.ts +++ b/src/features/auth/api/auth.ts @@ -1,7 +1,7 @@ import apiClient from '@/lib/utils/apiClient'; import { LoginFormData } from '../types/loginFormSchema'; import { useAuthStore } from '@/store/authStore'; -import { setCookie } from '@/features/auth/utils/cookies'; +import { setCookie, deleteCookie } from '@/features/auth/utils/cookies'; import { User } from '../types/user'; export const login = async (data: LoginFormData) => { @@ -36,3 +36,16 @@ export const getUserInfo = async () => { throw error; } }; + +export const logout = async () => { + try { + await apiClient.post('/auths/signout'); + const { setIsLoggedIn, setUser } = useAuthStore.getState(); + setIsLoggedIn(false); + setUser(null); + deleteCookie('auth_token'); + } catch (error) { + console.error('로그아웃 에러:', error); + throw error; + } +}; diff --git a/src/features/auth/hooks/useLoginSubmit.ts b/src/features/auth/hooks/useLoginSubmit.ts index 9b3eef84..0e00c942 100644 --- a/src/features/auth/hooks/useLoginSubmit.ts +++ b/src/features/auth/hooks/useLoginSubmit.ts @@ -27,7 +27,7 @@ export const useLoginSubmit = ( console.log('로그인 성공:', response); reset(); - const returnUrl = searchParams.get('returnUrl') || '/'; + const returnUrl = searchParams.get('returnUrl') || '/exchange'; router.replace(returnUrl); } catch (error) { const { code } = (error as LoginError).response.data; From 7c38437aa7c03df0cb178ebb0a98650237837fb7 Mon Sep 17 00:00:00 2001 From: sun <104830526+sunnwave@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:52:04 +0900 Subject: [PATCH 31/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20feature=20?= =?UTF-8?q?common=20popup=20#10=20(#133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 💄[Design] PopUp 컴포넌트 레이아웃 구현 #10 * ✨[Feat] PopUp의 Button 컴포넌트 구현, 스토리북 코드 작성 #31 * 💄[Design] 팝업창 css 구현 완료 ✅[Test] 팝업창 스토리북 테스트 코드 구현 완료 #10 * ✅[Test] 팝업창 스토리북 테스트코드 타입별 코드 추가 #26 * ♻️[Refactor] Button 컴포넌트 이용하여 PopUp 컴포넌트 리팩토링 #10 * 🔥[Remove] PopUpButton 컴포넌트 파일 삭제 #10 * ✅[Test] PopUp 컴포넌트 jest 테스트코드 수정 #10 --- package-lock.json | 813 +++++++++++++++++------- public/icons/IcClose.tsx | 34 + public/icons/index.ts | 1 + src/components/pop-up/PopUp.stories.tsx | 58 ++ src/components/pop-up/PopUp.test.tsx | 82 +++ src/components/pop-up/PopUp.tsx | 75 +++ 6 files changed, 837 insertions(+), 226 deletions(-) create mode 100644 public/icons/IcClose.tsx create mode 100644 src/components/pop-up/PopUp.stories.tsx create mode 100644 src/components/pop-up/PopUp.test.tsx create mode 100644 src/components/pop-up/PopUp.tsx diff --git a/package-lock.json b/package-lock.json index 41cdbc4a..91a06d4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -102,7 +101,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -117,7 +115,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -127,7 +124,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -158,7 +154,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -171,7 +166,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -181,7 +175,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.26.2", @@ -198,7 +191,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.25.9" @@ -211,7 +203,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -225,7 +216,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.25.9", @@ -242,7 +232,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -252,7 +241,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -262,7 +250,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -284,7 +271,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -294,7 +280,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -312,7 +297,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -322,7 +306,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -339,7 +322,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -353,7 +335,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -367,7 +348,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -385,7 +365,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.25.9" @@ -398,7 +377,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -408,7 +386,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -426,7 +403,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", @@ -444,7 +420,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -458,7 +433,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -472,7 +446,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -482,7 +455,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -492,7 +464,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -502,7 +473,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -517,7 +487,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -531,7 +500,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.26.0" @@ -547,7 +515,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -564,7 +531,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -580,7 +546,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -596,7 +561,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -614,7 +578,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -631,7 +594,6 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -712,7 +674,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -728,7 +689,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -770,7 +730,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -896,7 +855,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -912,7 +870,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -929,7 +886,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -945,7 +901,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -963,7 +918,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -981,7 +935,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -997,7 +950,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1013,7 +965,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", @@ -1030,7 +981,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", @@ -1047,7 +997,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1068,7 +1017,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" @@ -1078,7 +1026,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1095,7 +1042,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1111,7 +1057,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -1128,7 +1073,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1144,7 +1088,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -1161,7 +1104,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1177,7 +1119,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", @@ -1194,7 +1135,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1210,7 +1150,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1227,7 +1166,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -1245,7 +1183,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1261,7 +1198,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1277,7 +1213,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1293,7 +1228,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1309,7 +1243,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -1326,7 +1259,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -1344,7 +1276,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -1363,7 +1294,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -1380,7 +1310,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -1397,7 +1326,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1413,7 +1341,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1429,7 +1356,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1445,7 +1371,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -1463,7 +1388,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1480,7 +1404,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1496,7 +1419,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1513,7 +1435,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1529,7 +1450,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", @@ -1546,7 +1466,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1564,7 +1483,21 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", - "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.9.tgz", + "integrity": "sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1580,7 +1513,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz", "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1596,7 +1528,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1616,7 +1547,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz", "integrity": "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-transform-react-jsx": "^7.25.9" @@ -1632,7 +1562,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz", "integrity": "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1649,7 +1578,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1666,7 +1594,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -1683,7 +1610,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1730,7 +1656,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1746,7 +1671,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1763,7 +1687,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1779,7 +1702,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1795,7 +1717,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1811,7 +1732,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1831,7 +1751,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1847,7 +1766,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -1864,7 +1782,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -1881,7 +1798,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -1898,7 +1814,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.0", @@ -1982,7 +1897,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1992,7 +1906,6 @@ "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -2007,7 +1920,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.9.tgz", "integrity": "sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2028,7 +1940,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2048,7 +1959,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2061,7 +1971,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.25.9", @@ -2076,7 +1985,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.25.9", @@ -2095,7 +2003,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" @@ -2105,7 +2012,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -3712,7 +3618,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -3727,7 +3632,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" @@ -3737,7 +3641,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" @@ -3758,14 +3661,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", @@ -4978,84 +4879,417 @@ "storybook": "^8.4.5" } }, - "node_modules/@storybook/test": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.4.5.tgz", - "integrity": "sha512-mHsRc6m60nfcEBsjvUkKz+Jnz0or4WH5jmJ1VL2pGKO4VzESCPqAwDnwDqP2YyeSQ0b/MAKUT5kdoLE2RE2eVw==", - "dev": true, + "node_modules/@storybook/test": { + "version": "8.4.5", + "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.4.5.tgz", + "integrity": "sha512-mHsRc6m60nfcEBsjvUkKz+Jnz0or4WH5jmJ1VL2pGKO4VzESCPqAwDnwDqP2YyeSQ0b/MAKUT5kdoLE2RE2eVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf": "^0.1.11", + "@storybook/global": "^5.0.0", + "@storybook/instrumenter": "8.4.5", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.5.0", + "@testing-library/user-event": "14.5.2", + "@vitest/expect": "2.0.5", + "@vitest/spy": "2.0.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.5" + } + }, + "node_modules/@storybook/test/node_modules/@testing-library/jest-dom": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@storybook/test/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/test/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/theming": { + "version": "8.4.5", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.5.tgz", + "integrity": "sha512-45e/jeG4iuqdZcHg3PbB6dwXQTwlnnEB7r/QcVExyC7ibrkTnjUfvxzyUw4mmU3CXETFGD5EcUobFkgK+/aPxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", "license": "MIT", "dependencies": { - "@storybook/csf": "^0.1.11", - "@storybook/global": "^5.0.0", - "@storybook/instrumenter": "8.4.5", - "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", - "@testing-library/user-event": "14.5.2", - "@vitest/expect": "2.0.5", - "@vitest/spy": "2.0.5" + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "type": "github", + "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { - "storybook": "^8.4.5" + "@svgr/core": "*" } }, - "node_modules/@storybook/test/node_modules/@testing-library/jest-dom": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", - "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", - "dev": true, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.21", - "redent": "^3.0.0" + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" }, "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" } }, - "node_modules/@storybook/test/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, + "node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@storybook/test/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@storybook/theming": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.5.tgz", - "integrity": "sha512-45e/jeG4iuqdZcHg3PbB6dwXQTwlnnEB7r/QcVExyC7ibrkTnjUfvxzyUw4mmU3CXETFGD5EcUobFkgK+/aPxQ==", - "dev": true, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" }, - "peerDependencies": { - "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@swc/counter": { @@ -5277,6 +5511,15 @@ "node": ">= 10" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -6925,7 +7168,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -7482,7 +7724,6 @@ "version": "0.4.12", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", @@ -7497,7 +7738,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7507,7 +7747,6 @@ "version": "0.10.6", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.2", @@ -7521,7 +7760,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" @@ -7649,7 +7887,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { @@ -7827,7 +8064,6 @@ "version": "4.24.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7960,7 +8196,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" @@ -8500,14 +8735,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/core-js-compat": { "version": "3.39.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.24.2" @@ -8727,11 +8960,23 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -8760,6 +9005,39 @@ "node": ">=4" } }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -8874,7 +9152,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8923,7 +9200,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9130,7 +9406,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -9188,7 +9463,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -9222,7 +9496,6 @@ "version": "1.5.64", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz", "integrity": "sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==", - "dev": true, "license": "ISC" }, "node_modules/elliptic": { @@ -9341,7 +9614,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -9351,7 +9623,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/error-stack-parser": { @@ -9595,7 +9866,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10249,7 +10519,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -10749,7 +11018,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10788,7 +11056,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -11094,7 +11361,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -11386,7 +11652,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -11600,7 +11865,6 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -13202,7 +13466,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -13271,7 +13534,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -13291,7 +13553,6 @@ "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==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -13428,7 +13689,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/lint-staged": { @@ -13789,7 +14049,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, "license": "MIT" }, "node_modules/lodash.memoize": { @@ -13983,7 +14242,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.3" @@ -14078,6 +14336,12 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, "node_modules/memfs": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", @@ -14258,7 +14522,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/mz": { @@ -14391,7 +14654,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, "license": "MIT", "dependencies": { "lower-case": "^2.0.2", @@ -14469,7 +14731,6 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true, "license": "MIT" }, "node_modules/normalize-path": { @@ -14509,7 +14770,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -14818,7 +15078,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" @@ -14849,7 +15108,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -14942,7 +15200,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -14966,7 +15223,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16045,14 +16301,12 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -16065,14 +16319,12 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" @@ -16108,7 +16360,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -16126,14 +16377,12 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, "license": "MIT" }, "node_modules/regjsparser": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" @@ -16197,7 +16446,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -16238,7 +16486,6 @@ "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" @@ -16854,6 +17101,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -17432,7 +17689,6 @@ "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==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17441,6 +17697,117 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/svgo/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -18084,7 +18451,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -18239,7 +18606,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -18249,7 +18615,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -18263,7 +18628,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -18273,7 +18637,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -18307,7 +18670,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -18974,7 +19336,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { diff --git a/public/icons/IcClose.tsx b/public/icons/IcClose.tsx new file mode 100644 index 00000000..7ae8dd0f --- /dev/null +++ b/public/icons/IcClose.tsx @@ -0,0 +1,34 @@ +import { SVGProps } from 'react'; + +interface IcCloseProps extends SVGProps { + width?: number; + height?: number; + isActive?: boolean; + color?: string; +} + +function IcClose({ + width = 24, + height = 24, + isActive = false, + color, +}: IcCloseProps) { + return ( + + + + ); +} + +export default IcClose; diff --git a/public/icons/index.ts b/public/icons/index.ts index 96849f91..b2a09236 100644 --- a/public/icons/index.ts +++ b/public/icons/index.ts @@ -10,3 +10,4 @@ export { default as VisibilityOn } from './visibility_on'; export { default as VisibilityOff } from './visibility_off'; export { default as LocationIcon } from './LocationIcon'; export { default as HostIcon } from './HostIcon'; +export { default as IcClose } from './IcClose'; diff --git a/src/components/pop-up/PopUp.stories.tsx b/src/components/pop-up/PopUp.stories.tsx new file mode 100644 index 00000000..3873bc60 --- /dev/null +++ b/src/components/pop-up/PopUp.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import PopUp from './PopUp'; + +const POPUP_LABEL = { + large: `정말 나가시겠어요?\n작성된 내용이 모두 삭제됩니다.`, + small: `가입이 완료되었습니다!`, +}; + +const meta: Meta = { + title: 'components/PopUp', + component: PopUp, + parameters: { + layout: 'centerd', + }, +}; + +type Story = StoryObj; +export const LargeOneButton: Story = { + args: { + isOpen: true, + isLarge: true, + label: POPUP_LABEL.large, + handlePopUpClose: (e) => alert(e), + handlePopUpConfirm: (e) => alert(e), + }, +}; + +export const LargeTwoButton: Story = { + args: { + isOpen: true, + isLarge: true, + isTwoButton: true, + label: POPUP_LABEL.large, + handlePopUpClose: (e) => alert(e), + handlePopUpConfirm: (e) => alert(e), + }, +}; + +export const SmallOneButton: Story = { + args: { + isOpen: true, + label: POPUP_LABEL.small, + handlePopUpClose: (e) => alert(e), + handlePopUpConfirm: (e) => alert(e), + }, +}; + +export const SmallTwoButton: Story = { + args: { + isOpen: true, + isTwoButton: true, + label: POPUP_LABEL.small, + handlePopUpClose: (e) => alert(e), + handlePopUpConfirm: (e) => alert(e), + }, +}; + +export default meta; diff --git a/src/components/pop-up/PopUp.test.tsx b/src/components/pop-up/PopUp.test.tsx new file mode 100644 index 00000000..1bf65050 --- /dev/null +++ b/src/components/pop-up/PopUp.test.tsx @@ -0,0 +1,82 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import PopUp from './PopUp'; + +const POPUP_LABEL = { + large: `정말 나가시겠어요?\n작성된 내용이 모두 삭제됩니다.`, + small: `가입이 완료되었습니다!`, +}; + +const mockHandlePopUpClose = jest.fn(); +const mockHandlePopUpConfirm = jest.fn(); + +describe('PopUp 렌더링 테스트', () => { + it('small, single button 렌더링 테스트', () => { + render( + , + ); + const popUp = screen.getByRole('pop-up'); + const confirm = screen.getByText('확인'); + const cancel = screen.queryByText('취소'); + expect(popUp).toBeInTheDocument(); + expect(confirm).toBeInTheDocument(); + expect(cancel).not.toBeInTheDocument(); + }); + it('small, two button 렌더링 테스트', () => { + render( + , + ); + const popUp = screen.getByRole('pop-up'); + const confirm = screen.getByText('확인'); + const cancel = screen.getByText('취소'); + expect(popUp).toBeInTheDocument(); + expect(confirm).toBeInTheDocument(); + expect(cancel).toBeInTheDocument(); + }); + it('large, single button 렌더링 테스트', () => { + render( + , + ); + const popUp = screen.getByRole('pop-up'); + const confirm = screen.getByText('확인'); + const cancel = screen.queryByText('취소'); + expect(popUp).toBeInTheDocument(); + expect(confirm).toBeInTheDocument(); + expect(cancel).not.toBeInTheDocument(); + }); + it('large, two button 렌더링 테스트', () => { + render( + , + ); + const popUp = screen.getByRole('pop-up'); + const confirm = screen.getByText('확인'); + const cancel = screen.getByText('취소'); + expect(popUp).toBeInTheDocument(); + expect(confirm).toBeInTheDocument(); + expect(cancel).toBeInTheDocument(); + }); +}); diff --git a/src/components/pop-up/PopUp.tsx b/src/components/pop-up/PopUp.tsx new file mode 100644 index 00000000..eeb59180 --- /dev/null +++ b/src/components/pop-up/PopUp.tsx @@ -0,0 +1,75 @@ +import { IcClose } from '../../../public/icons'; +import Button from '../button/Button'; + +interface PopUpProps { + isOpen: boolean; + label: string; + handlePopUpClose: (result: boolean) => void; + handlePopUpConfirm: (result: boolean) => void; + isLarge?: boolean; + isTwoButton?: boolean; +} + +function PopUp({ + isOpen, + isLarge = false, + isTwoButton = false, + label, + handlePopUpClose, + handlePopUpConfirm, +}: PopUpProps) { + const onClickClose = () => { + if (handlePopUpClose) { + handlePopUpClose(false); + } + }; + + const onClickConfirm = () => { + if (handlePopUpConfirm) { + handlePopUpConfirm(true); + } + }; + return ( + <> +
+
+
+ +
+ {label} +
+
+ {isTwoButton ? ( +
+
+
+
+ + ); +} + +export default PopUp; From 4b42c07c9c5db9fc21037db23877e7e3bc519d35 Mon Sep 17 00:00:00 2001 From: Minkyung Kim <97824352+wynter24@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:14:15 +0900 Subject: [PATCH 32/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20[Refactor]=20Compoun?= =?UTF-8?q?d=20Pattern=20=EC=A0=81=EC=9A=A9=ED=95=98=EC=97=AC=20WrittenRev?= =?UTF-8?q?iew=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0=20#100=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 💄[Design] WrittenReview, RatingIcon 변경된 디자인으로 수정 #100 * ♻️[Refactor] 이미지 에러 처리 로직 간소화 및 상위 컴포넌트로 책임 분리 #100 * 💄♻️[Design, Refactor] UI 디자인 변경 및 컴포넌트 컴파운드 패턴 적용 #100 * ✅[Test] 스토리북 수정 #100 * 🔥[Remove] WrittenReview 테스트 파일 삭제 #100 --- public/icons/RatingIcon.tsx | 2 +- .../written-review/WrittenReview.stories.tsx | 78 ++++++++++- .../written-review/WrittenReview.test.tsx | 55 -------- .../written-review/WrittenReview.tsx | 132 ++++++++++++------ .../written-review/types/writtenReview.ts | 24 ++++ 5 files changed, 189 insertions(+), 102 deletions(-) delete mode 100644 src/components/written-review/WrittenReview.test.tsx create mode 100644 src/components/written-review/types/writtenReview.ts diff --git a/public/icons/RatingIcon.tsx b/public/icons/RatingIcon.tsx index 4582487b..14ae97b0 100644 --- a/public/icons/RatingIcon.tsx +++ b/public/icons/RatingIcon.tsx @@ -12,7 +12,7 @@ function RatingIcon({ checked = false, ...props }: RatingIconProps) { - const heartColor = checked ? '#EA580C' : '#D1D5DB'; + const heartColor = checked ? '#00a991' : '#d8d9db'; return ( ; export default meta; -type Story = StoryObj; +type Story = StoryObj; -export const CreatedReview: Story = { +export const FindClubReview: Story = { + render: (args) => ( + +
+ + + +
+ + ), args: { ratingCount: 4, comment: - '따듯하게 느껴지는 공간이에요 :) 평소에 달램 이용해보고 싶었는데 이렇게 같이 달램 생기니까 너무 좋아요! 프로그램이 더 많이 늘어났으면 좋겠어요.', + '아침부터 자기발전을 위한 시간을 가져서 좋았어요. 각자의 길 위에서 달려가는 생생한 순간을 공유해주셔서 감사합니다!', + profileImage: + 'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg', + userName: '다람쥐', + createdAt: '2024.01.25', + }, +}; + +export const MypageReview: Story = { + render: (args) => ( +
+ +
+ +
+ + + + +
+
+
+
+ ), + args: { profileImage: 'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg', - userName: '럽윈즈올', + userName: '다람쥐', + ratingCount: 4, + comment: + '아침부터 자기발전을 위한 시간을 가져서 좋았어요. 각자의 길 위에서 달려가는 생생한 순간을 공유해주셔서 감사합니다!', createdAt: '2024.01.25', + clubImage: + 'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg', + clubName: '달램핏 오피스 스트레칭', + clubImageAlt: '달램핏 이미지', + location: '을지로 3가', }, }; diff --git a/src/components/written-review/WrittenReview.test.tsx b/src/components/written-review/WrittenReview.test.tsx deleted file mode 100644 index 35a40d3e..00000000 --- a/src/components/written-review/WrittenReview.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import WrittenReview from './WrittenReview'; -import { act } from 'react'; - -describe('WrittenReview Component', () => { - const defaultProfileImage = - 'https://cdn.pixabay.com/photo/2018/02/12/10/45/heart-3147976_1280.jpg'; - const validProfileImage = - 'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg'; - const brokenProfileImage = 'https://broken-url.com/image.jpg'; - - it('유효한 이미지 경우 정상적으로 렌더링', () => { - render( - , - ); - const imgElement = screen.getByAltText("Test User's profile picture"); - - expect(imgElement.getAttribute('src')).toContain( - encodeURIComponent(validProfileImage), - ); - }); - - it('유효하지 않은 이미지 경우 기본 이미지로 대체', async () => { - render( - , - ); - - const imgElement = screen.getByAltText("Test User's profile picture"); - - expect(imgElement.getAttribute('src')).toContain( - encodeURIComponent(brokenProfileImage), - ); - - await act(async () => { - imgElement.dispatchEvent(new Event('error')); - }); - - expect(imgElement.getAttribute('src')).toContain( - encodeURIComponent(defaultProfileImage), - ); - }); -}); diff --git a/src/components/written-review/WrittenReview.tsx b/src/components/written-review/WrittenReview.tsx index df4d53ec..c77a5231 100644 --- a/src/components/written-review/WrittenReview.tsx +++ b/src/components/written-review/WrittenReview.tsx @@ -1,59 +1,107 @@ -'use client'; - import Image from 'next/image'; import RatingDisplay from '../rating-display/RatingDisplay'; -import { useState, useEffect } from 'react'; - -// 디자인 확정시, 기본 이미지 변경 -const defaultProfileImage = - 'https://cdn.pixabay.com/photo/2018/02/12/10/45/heart-3147976_1280.jpg'; - -interface WrittenReviewProps { - ratingCount: number; - comment: string; - profileImage?: string; - userName: string; - createdAt: string; +import { LocationIcon } from '../../../public/icons'; +import { + ClubImageProps, + ClubInfoProps, + CommentProps, + RatingProps, + UserProfileProps, +} from './types/writtenReview'; + +// 기본 이미지 (변경될 수 있음) +const defaultProfileImage = '/images/profile.png'; +const defaultClubImage = '/images/profile.png'; + +function handleImageError( + event: React.SyntheticEvent, + defaultSrc: string, +) { + event.currentTarget.src = defaultSrc; } export default function WrittenReview({ - ratingCount, - comment, - profileImage, - userName, - createdAt, -}: WrittenReviewProps) { - const [imgSrc, setImgSrc] = useState(profileImage || defaultProfileImage); + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {children} +
+
+ ); +} - useEffect(() => { - setImgSrc(profileImage || defaultProfileImage); - }, [profileImage]); +function Rating({ ratingCount }: RatingProps) { + return ; +} - const handleImageError = () => { - setImgSrc(defaultProfileImage); - }; +function Comment({ text }: CommentProps) { + return ( +

+ {text} +

+ ); +} +function UserProfile({ + profileImage, + userName, + createdAt, + className, +}: UserProfileProps) { return ( -
- -

- {comment} -

-
+
+ {profileImage && ( {`${userName}'s handleImageError(e, defaultProfileImage)} /> -

- {userName} -

-

{createdAt}

+ )} +

+ {userName} +

+

{createdAt}

+
+ ); +} + +function ClubImage({ src, alt }: ClubImageProps) { + return ( + {alt handleImageError(e, defaultClubImage)} + /> + ); +} + +function ClubInfo({ clubName, location }: ClubInfoProps) { + return ( +
+

{clubName}

+
+ + + {location} +
-
-
+ ); } + +// WrittenReview의 자식 컴포넌트 연결 +WrittenReview.Rating = Rating; +WrittenReview.Comment = Comment; +WrittenReview.UserProfile = UserProfile; +WrittenReview.ClubInfo = ClubInfo; +WrittenReview.ClubImage = ClubImage; diff --git a/src/components/written-review/types/writtenReview.ts b/src/components/written-review/types/writtenReview.ts new file mode 100644 index 00000000..0f22dacc --- /dev/null +++ b/src/components/written-review/types/writtenReview.ts @@ -0,0 +1,24 @@ +export interface UserProfileProps { + profileImage?: string; + userName?: string; + createdAt: string; + className?: string; +} + +export interface ClubImageProps { + src: string; + alt?: string; +} + +export interface ClubInfoProps { + clubName: string; + location: string; +} + +export interface CommentProps { + text: string; +} + +export interface RatingProps { + ratingCount: number; +} From 0076ac29e128b32b4a7a23c40416d9dcce3c43bc Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:16:11 +0900 Subject: [PATCH 33/47] =?UTF-8?q?=F0=9F=92=84[Design]=20Sub=5FTab=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 💄[Design] 버튼 색상 추가 #134 * ♻️[Refactor] SubTab 디자인 변경 #134 --- src/components/button/Button.tsx | 4 ++- src/components/tab/Tab.test.tsx | 38 -------------------- src/components/tab/Tab.tsx | 60 ++++++++++++++++++++------------ src/constants/button.ts | 5 +++ 4 files changed, 46 insertions(+), 61 deletions(-) diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index 03cd6523..e2b2e042 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -10,6 +10,7 @@ interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { | 'green-normal-01' | 'green-light-03' | 'gray-normal-03' + | 'gray-normal-02' | 'gray-darker'; isSubmitting?: boolean; } @@ -40,7 +41,8 @@ export default function Button({ type TextClassType = | 'text-green-normal-01' | 'text-gray-darker' - | 'text-white'; + | 'text-white' + | 'text-gray-dark-02'; let textClass: TextClassType = color.text; diff --git a/src/components/tab/Tab.test.tsx b/src/components/tab/Tab.test.tsx index 448b641f..41566077 100644 --- a/src/components/tab/Tab.test.tsx +++ b/src/components/tab/Tab.test.tsx @@ -25,20 +25,6 @@ describe('Tab 컴포넌트', () => { }); }); - it('활성화된 탭은 올바른 스타일이 적용되어야 한다', () => { - render( - , - ); - - const activeTab = screen.getByText('탭1'); - expect(activeTab).toHaveClass('border-green-dark-01', 'text-green-dark-01'); - }); - it('탭 클릭시 onTabChange가 호출되어야 한다', () => { render( { expect(mockOnTabChange).toHaveBeenCalledWith('탭2'); }); - - it('tabType에 따라 올바른 텍스트 크기가 적용되어야 한다', () => { - const { rerender } = render( - , - ); - - expect(screen.getByText('탭1')).toHaveClass('text-xl'); - - rerender( - , - ); - - expect(screen.getByText('탭1')).toHaveClass('text-lg'); - }); }); diff --git a/src/components/tab/Tab.tsx b/src/components/tab/Tab.tsx index 642adc38..83e1c3b6 100644 --- a/src/components/tab/Tab.tsx +++ b/src/components/tab/Tab.tsx @@ -1,4 +1,5 @@ import { TabType } from '@/constants/tabs'; +import Button from '@/components/button/Button'; interface TabProps { items: readonly T[]; @@ -7,36 +8,51 @@ interface TabProps { tabType: TabType; } +const getTabStyles = { + main: { + base: 'text-xl border-b-2 rounded-none px-4 py-2 transition-all', + active: 'border-green-dark-01 text-green-dark-01', + inactive: 'border-transparent text-gray-dark-02 hover:text-green-dark-01', + }, + container: 'flex gap-2', +}; + function Tab({ items, activeTab, onTabChange, tabType, }: TabProps) { - const getTabStyle = () => { - switch (tabType) { - case 'MAIN_TAB': - return 'text-xl'; - case 'SUB_TAB': - return 'text-lg'; - } - }; + const renderMainTab = (item: T) => ( + + ); + + const renderSubTab = (item: T) => ( + - ))} +
+ {items.map((item) => + tabType === 'SUB_TAB' ? renderSubTab(item) : renderMainTab(item), + )}
); } diff --git a/src/constants/button.ts b/src/constants/button.ts index 6e6f05ed..6c652bdb 100644 --- a/src/constants/button.ts +++ b/src/constants/button.ts @@ -28,6 +28,11 @@ export const COLOR_GROUPS = { text: 'text-gray-darker', border: 'border-gray-darker', }, + 'gray-normal-02': { + bg: 'bg-gray-normal-02', + text: 'text-gray-dark-02', + border: 'border-gray-normal-02', + }, 'gray-darker': { bg: 'bg-gray-darker', text: 'text-gray-darker', From 6a68ac3fc7b0fbfca664359841b0cecee75d599c Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:19:16 +0900 Subject: [PATCH 34/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=A7=8C=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=ED=97=A4=EB=8D=94=20=EB=B0=94=20=EC=83=88?= =?UTF-8?q?=EB=A1=9C=EA=B3=A0=EC=B9=A8=20=EC=8B=9C=20isLoggedIn=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=99=95=EC=9D=B8=20#137=20(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️[Refactor] 토큰 만료기한 추가 #137 * ♻️[Refactor] 새로고침 시 토큰 유무 확인 #137 * ✅[Test] 테스트 코드 수정 #137 --- src/components/header/HeaderBar.test.tsx | 9 +++++++-- src/components/header/HeaderBar.tsx | 8 ++++++-- src/features/auth/utils/cookies.ts | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/header/HeaderBar.test.tsx b/src/components/header/HeaderBar.test.tsx index 3a0ea83e..71aafdfa 100644 --- a/src/components/header/HeaderBar.test.tsx +++ b/src/components/header/HeaderBar.test.tsx @@ -61,14 +61,18 @@ describe('HeaderBar 컴포넌트 테스트', () => { describe('로그인 상태에 따른 버튼 렌더링', () => { beforeEach(() => { jest.clearAllMocks(); - useAuthStore.setState({ isLoggedIn: false, user: null }); + useAuthStore.setState({ + isLoggedIn: false, + user: null, + checkLoginStatus: jest.fn(), + }); }); it('로그인 상태일 때 드롭다운 버튼이 렌더링되어야 한다', () => { useAuthStore.setState({ isLoggedIn: true, user: { - image: '/images/default-profile.png', + image: '/images/profile.png', teamId: 'team-id', id: 1, email: 'user@example.com', @@ -77,6 +81,7 @@ describe('로그인 상태에 따른 버튼 렌더링', () => { createdAt: new Date(), updatedAt: new Date(), }, + checkLoginStatus: jest.fn(), }); render(); diff --git a/src/components/header/HeaderBar.tsx b/src/components/header/HeaderBar.tsx index b6084a94..65e303a3 100644 --- a/src/components/header/HeaderBar.tsx +++ b/src/components/header/HeaderBar.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useEffect } from 'react'; import NavButton from './NavButton'; import { NAV_ITEMS } from '@/constants/navigation'; import { usePathname, useRouter } from 'next/navigation'; @@ -10,9 +10,13 @@ import { logout } from '@/features/auth/api/auth'; function HeaderBar() { const pathname = usePathname(); - const { isLoggedIn, user } = useAuthStore(); + const { isLoggedIn, user, checkLoginStatus } = useAuthStore(); const router = useRouter(); + useEffect(() => { + checkLoginStatus(); + }, [checkLoginStatus]); + const handleDropDownChange = async (value: string | undefined) => { if (value === 'LOGOUT') { try { diff --git a/src/features/auth/utils/cookies.ts b/src/features/auth/utils/cookies.ts index 3b298d45..ca37c320 100644 --- a/src/features/auth/utils/cookies.ts +++ b/src/features/auth/utils/cookies.ts @@ -1,5 +1,7 @@ export const setCookie = (name: string, value: string) => { - document.cookie = `${name}=${value};path=/`; + const expirationDate = new Date(); + expirationDate.setTime(expirationDate.getTime() + 60 * 60 * 1000); + document.cookie = `${name}=${value};path=/;expires=${expirationDate.toUTCString()}`; }; export const getCookie = (name: string): string | null => { From e2b7db4d804e04f2aa17ae369d494abfb72918ea Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:42:58 +0900 Subject: [PATCH 35/47] =?UTF-8?q?=E2=9C=A8=20[Feature]=20=EB=82=98?= =?UTF-8?q?=EC=9D=98=20=EB=AA=A8=EC=9E=84=20=EC=B9=B4=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️[Refactor] chip 형태 리팩토링 * ✨[Feat] 모임 칩 구현 * ✅[Test] 테스트 코드 작성 * ✅[Test] 스토리북 경로 조정 * ♻️[Refactor] text 자동지정되도록 수정 * ✨[Feat] 기본 레이아웃 구현 --- .../card/my-club-card/MyClubCard.stories.tsx | 79 +++++++++++++++++++ .../card/my-club-card/MyClubCard.tsx | 68 ++++++++++++++++ src/components/card/types/models.ts | 12 +++ src/components/chip/Chip.stories.tsx | 8 +- src/components/chip/Chip.test.tsx | 2 +- src/components/chip/Chip.tsx | 14 ++-- .../chip/club-chip/ClubChip.stories.tsx | 48 +++++++++++ .../chip/club-chip/ClubChip.test.tsx | 29 +++++++ src/components/chip/club-chip/ClubChip.tsx | 54 +++++++++++++ tailwind.config.ts | 3 + 10 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 src/components/card/my-club-card/MyClubCard.stories.tsx create mode 100644 src/components/card/my-club-card/MyClubCard.tsx create mode 100644 src/components/chip/club-chip/ClubChip.stories.tsx create mode 100644 src/components/chip/club-chip/ClubChip.test.tsx create mode 100644 src/components/chip/club-chip/ClubChip.tsx diff --git a/src/components/card/my-club-card/MyClubCard.stories.tsx b/src/components/card/my-club-card/MyClubCard.stories.tsx new file mode 100644 index 00000000..47ee9add --- /dev/null +++ b/src/components/card/my-club-card/MyClubCard.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import MyClubCard from './MyClubCard'; + +const meta = { + title: 'Components/Card/MyClubCard', + component: MyClubCard, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mockClubMeeting = { + meetingInfo: { + title: '을지로에서 만나는 독서 모임', + location: '을지로 3가', + datetime: '12/14(토) 오전 10:00', + category: '자유책', + }, + imageInfo: { + url: 'https://picsum.photos/seed/bookclub/800/450', + isLiked: true, + onLikeClick: () => alert('좋아요를 눌렀습니다!'), + }, + clubStatus: { + isCompleted: false, + isConfirmed: true, + }, + actions: { + onClick: () => alert('카드를 클릭했습니다!'), + onDelete: () => alert('모임을 삭제했습니다!'), + }, +}; + +export const Default: Story = { + args: { + meeting: mockClubMeeting, + }, + render: (args) => ( +
+ +
+ ), +}; + +export const Completed: Story = { + args: { + meeting: { + ...mockClubMeeting, + clubStatus: { + ...mockClubMeeting.clubStatus, + isCompleted: true, + }, + }, + }, +}; + +export const Pending: Story = { + args: { + meeting: { + ...mockClubMeeting, + clubStatus: { + ...mockClubMeeting.clubStatus, + isConfirmed: false, + }, + }, + }, +}; + +export const Canceled: Story = { + args: { + meeting: { + ...mockClubMeeting, + isCanceled: true, + }, + }, +}; diff --git a/src/components/card/my-club-card/MyClubCard.tsx b/src/components/card/my-club-card/MyClubCard.tsx new file mode 100644 index 00000000..ffc5c651 --- /dev/null +++ b/src/components/card/my-club-card/MyClubCard.tsx @@ -0,0 +1,68 @@ +import { ComponentPropsWithoutRef } from 'react'; +import Card from '../Card'; +import ClubChip from '@/components/chip/club-chip/ClubChip'; +import Button from '@/components/button/Button'; +import { ClubMeeting } from '@/components/card/types'; +import Chip from '@/components/chip/Chip'; + +interface MyClubCardProps extends ComponentPropsWithoutRef<'article'> { + meeting: ClubMeeting; +} + +function MyClubCard({ meeting, className, ...props }: MyClubCardProps) { + const { + meetingInfo, + imageInfo, + clubStatus, + isCanceled = false, + actions, + } = meeting; + + return ( + +
+ + + {/* 첫 번째 줄: ClubChip + Chip */} +
+
+ + +
+ +
+ + {/* 두 번째 줄: 모임 정보 */} +
+

+ {meetingInfo.title} +

+
+ {meetingInfo.location} + {meetingInfo.datetime} +
+
+ + {/* 세 번째 줄: 버튼 */} +
+
+ + +
+
+
+ ); +} + +export default MyClubCard; diff --git a/src/components/card/types/models.ts b/src/components/card/types/models.ts index 040849bf..f44ca4d6 100644 --- a/src/components/card/types/models.ts +++ b/src/components/card/types/models.ts @@ -18,3 +18,15 @@ export interface FullMeeting extends Omit { hostInfo: HostInfo; actions: FullActions; } + +export interface ClubStatus { + isCompleted: boolean; + isConfirmed: boolean; +} + +export interface ClubMeeting extends BaseProps { + meetingInfo: MeetingInfo; + imageInfo: ImageInfo; + clubStatus: ClubStatus; + actions: SimpleActions; +} diff --git a/src/components/chip/Chip.stories.tsx b/src/components/chip/Chip.stories.tsx index ee2229b9..65e6d013 100644 --- a/src/components/chip/Chip.stories.tsx +++ b/src/components/chip/Chip.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import Chip from './Chip'; const meta = { - title: 'Components/Chip', + title: 'Components/Chip/Base', component: Chip, parameters: { layout: 'centered', @@ -29,7 +29,7 @@ export const RoundedLight: Story = { export const SquareOutlined: Story = { args: { text: '오프라인', - variant: 'square-light', + variant: 'square-outlined', }, }; @@ -54,13 +54,13 @@ export const AllStates: Story = {
- +
- +
diff --git a/src/components/chip/Chip.test.tsx b/src/components/chip/Chip.test.tsx index cb924c0a..0611475d 100644 --- a/src/components/chip/Chip.test.tsx +++ b/src/components/chip/Chip.test.tsx @@ -2,7 +2,7 @@ import Chip from '@/components/chip/Chip'; import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; -describe('TextChip', () => { +describe('Chip', () => { it('텍스트가 올바르게 렌더링되어야 한다', () => { render(); const chip = screen.getByText('테스트'); diff --git a/src/components/chip/Chip.tsx b/src/components/chip/Chip.tsx index 9e3e3a03..af5810ef 100644 --- a/src/components/chip/Chip.tsx +++ b/src/components/chip/Chip.tsx @@ -1,11 +1,13 @@ import { twMerge } from 'tailwind-merge'; const CHIP_VARIANTS = { - 'rounded-filled': 'rounded-full bg-green-light-02 text-green-dark-01', - 'rounded-light': 'rounded-full bg-green-normal-01 text-gray-white', - 'square-light': - 'rounded border border-gray-normal-02 bg-gray-light-01 text-gray-dark-01', - 'square-filled': 'rounded bg-green-dark-01 text-gray-dark-01', + 'rounded-filled': + 'rounded-full bg-green-light-02 text-green-dark-01 font-semibold', + 'rounded-light': + 'rounded-full bg-green-normal-01 text-gray-white font-semibold', + 'square-outlined': + 'rounded-md border border-green-normal-01 bg-gray-white text-green-normal-01', + 'square-filled': 'rounded-md bg-green-normal-01 text-gray-white px-2', } as const; type ChipVariant = keyof typeof CHIP_VARIANTS; @@ -24,7 +26,7 @@ function Chip({ className, }: ChipProps) { const baseStyles = - 'inline-flex items-center justify-center px-2.5 py-1 text-sm font-semibold'; + 'inline-flex items-center justify-center px-2.5 py-1 text-sm font-medium'; const combinedClassName = twMerge( baseStyles, diff --git a/src/components/chip/club-chip/ClubChip.stories.tsx b/src/components/chip/club-chip/ClubChip.stories.tsx new file mode 100644 index 00000000..74f41fcc --- /dev/null +++ b/src/components/chip/club-chip/ClubChip.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ClubChip from './ClubChip'; + +const meta = { + title: 'Components/Chip/ClubChip', + component: ClubChip, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Completed: Story = { + args: { + variant: 'completed', + }, +}; + +export const Scheduled: Story = { + args: { + variant: 'scheduled', + }, +}; + +export const Pending: Story = { + args: { + variant: 'pending', + }, +}; + +export const Confirmed: Story = { + args: { + variant: 'confirmed', + }, +}; + +export const AllStates: Story = { + render: () => ( +
+ + + + +
+ ), +}; diff --git a/src/components/chip/club-chip/ClubChip.test.tsx b/src/components/chip/club-chip/ClubChip.test.tsx new file mode 100644 index 00000000..f511a9a2 --- /dev/null +++ b/src/components/chip/club-chip/ClubChip.test.tsx @@ -0,0 +1,29 @@ +import ClubChip from './ClubChip'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; + +describe('ClubChip', () => { + it('variant에 따라 올바른 텍스트가 렌더링되어야 한다', () => { + render(); + expect(screen.getByText('참여완료')).toBeInTheDocument(); + }); + + it('모든 variant가 올바르게 렌더링되어야 한다', () => { + const { rerender } = render(); + expect(screen.getByText('참여완료')).toBeInTheDocument(); + + rerender(); + expect(screen.getByText('참여예정')).toBeInTheDocument(); + + rerender(); + expect(screen.getByText('개설대기')).toBeInTheDocument(); + + rerender(); + expect(screen.getByText('개설확정')).toBeInTheDocument(); + }); + + it('className prop으로 스타일을 오버라이드할 수 있어야 한다', () => { + render(); + expect(screen.getByText('참여완료')).toHaveClass('custom-class'); + }); +}); diff --git a/src/components/chip/club-chip/ClubChip.tsx b/src/components/chip/club-chip/ClubChip.tsx new file mode 100644 index 00000000..500baa9e --- /dev/null +++ b/src/components/chip/club-chip/ClubChip.tsx @@ -0,0 +1,54 @@ +import Chip from '../Chip'; +import { twMerge } from 'tailwind-merge'; + +type ClubChipVariant = 'completed' | 'scheduled' | 'pending' | 'confirmed'; + +const CLUB_CHIP_TEXT = { + completed: '참여완료', + scheduled: '참여예정', + pending: '개설대기', + confirmed: '개설확정', +} as const; + +interface ClubChipProps { + variant: ClubChipVariant; + className?: string; +} + +function ClubChip({ variant, className }: ClubChipProps) { + const getChipVariant = () => { + switch (variant) { + case 'completed': + return 'square-filled'; + case 'scheduled': + return 'square-filled'; + case 'pending': + return 'square-outlined'; + case 'confirmed': + return 'square-filled'; + } + }; + + const getCustomClassName = () => { + switch (variant) { + case 'completed': + return 'bg-gray-normal-01 text-gray-dark-02'; + case 'scheduled': + return 'bg-green-normal-01 text-gray-white'; + case 'pending': + return 'border-blue-normal-01 text-blue-normal-01'; + case 'confirmed': + return 'bg-blue-normal-01 text-gray-white'; + } + }; + + return ( + + ); +} + +export default ClubChip; diff --git a/tailwind.config.ts b/tailwind.config.ts index ec01778d..c017cc5e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -37,6 +37,9 @@ const config: Config = { 'dark-03': '#004c41', darker: '#003b33', }, + blue: { + 'normal-01': '#007AFF', + }, }, }, }, From e04aad8dcbea5351222a60188aed4168de55e7da Mon Sep 17 00:00:00 2001 From: sun <104830526+sunnwave@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:56:25 +0900 Subject: [PATCH 36/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20#143=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️[Refactor] 버튼 컴포넌트 컬러클래스 코드 리팩토링, COLOR_SCHEMES 수정, 스토리북 수정 #143 * 🎨[Style]layout 배경색상 변경, 패딩값 변경 --- src/app/layout.tsx | 4 +- src/components/button/Button.stories.tsx | 27 +++-- src/components/button/Button.tsx | 45 +++------ src/constants/button.ts | 123 ++++++++++++++++++----- 4 files changed, 132 insertions(+), 67 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b6668862..f8719a36 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,10 +16,10 @@ export default function RootLayout({ }>) { return ( - + -
+
{children}
diff --git a/src/components/button/Button.stories.tsx b/src/components/button/Button.stories.tsx index 4396e2de..3a62c91b 100644 --- a/src/components/button/Button.stories.tsx +++ b/src/components/button/Button.stories.tsx @@ -12,7 +12,7 @@ const meta = { export default meta; type Story = StoryObj; -export const Default: Story = { +export const Solid: Story = { args: { text: '확인', size: 'medium', @@ -20,13 +20,12 @@ export const Default: Story = { themeColor: 'green-normal-01', }, }; - -export const Small: Story = { +export const Outline: Story = { args: { text: '확인', - size: 'small', + size: 'medium', fillType: 'outline', - themeColor: 'green-light-03', + themeColor: 'green-normal-01', }, }; @@ -35,7 +34,16 @@ export const LightSolid: Story = { text: '확인', size: 'modal', fillType: 'lightSolid', - themeColor: 'gray-normal-03', + themeColor: 'green-normal-01', + lightColor: 'green-light-03', + }, +}; +export const Small: Story = { + args: { + text: '확인', + size: 'small', + fillType: 'outline', + themeColor: 'green-light-03', }, }; @@ -58,10 +66,11 @@ export const Submitting: Story = { export const Disabled: Story = { args: { - text: '확인', + text: '생성하기', size: 'large', - fillType: 'solid', - themeColor: 'gray-darker', + fillType: 'lightSolid', + themeColor: 'gray-dark-01', + lightColor: 'gray-normal-01', disabled: true, }, parameters: { diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index e2b2e042..9f5e5478 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -1,17 +1,13 @@ import React from 'react'; import { twMerge } from 'tailwind-merge'; -import { BASE_CLASSES, COLOR_GROUPS, SIZE } from '@/constants'; +import { COLOR_SCHEMES, SIZE } from '@/constants'; interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { text: string; size: 'large' | 'medium' | 'small' | 'modal'; - fillType: 'solid' | 'outline' | 'lightSolid' | 'lightOutline'; - themeColor: - | 'green-normal-01' - | 'green-light-03' - | 'gray-normal-03' - | 'gray-normal-02' - | 'gray-darker'; + fillType: 'solid' | 'outline' | 'lightSolid'; + themeColor: keyof typeof COLOR_SCHEMES; + lightColor?: keyof typeof COLOR_SCHEMES; isSubmitting?: boolean; } @@ -20,6 +16,7 @@ export default function Button({ size, fillType = 'solid', themeColor = 'green-normal-01', + lightColor, isSubmitting, ...buttonProps }: ButtonProps) { @@ -36,35 +33,17 @@ export default function Button({ : themeColor; const variantClasses = (() => { - const color = COLOR_GROUPS[resolvedColor]; - - type TextClassType = - | 'text-green-normal-01' - | 'text-gray-darker' - | 'text-white' - | 'text-gray-dark-02'; - - let textClass: TextClassType = color.text; - - // 배경색과 글자색이 동일한 경우 textClass를 흰색으로 덮어쓰기 - if ( - (fillType === 'lightSolid' || fillType === 'lightOutline') && - color.bg.includes(color.text.replace('text-', 'bg-')) - ) { - textClass = 'text-white'; - } - switch (fillType) { case 'solid': - return `${color.bg} ${BASE_CLASSES.solid}`; + return `text-gray-white ${COLOR_SCHEMES[resolvedColor]['bg']}`; case 'outline': - return `${BASE_CLASSES.outline} ${color.text} ${color.border}`; + return `bg-gray-white border ${COLOR_SCHEMES[resolvedColor]['text']} ${COLOR_SCHEMES[resolvedColor]['border']}`; case 'lightSolid': - return `${color.bg} ${textClass}`; - case 'lightOutline': - return `${BASE_CLASSES.lightOutline} ${color.bg} ${textClass} ${color.border}`; - default: - throw new Error(`잘못된 fillType 값입니다: ${fillType}`); + if (lightColor) { + return `${COLOR_SCHEMES[resolvedColor]['text']} ${COLOR_SCHEMES[lightColor]['bg']}`; + } else { + return `${COLOR_SCHEMES[resolvedColor]['text']} bg-gray-white`; + } } })(); diff --git a/src/constants/button.ts b/src/constants/button.ts index 6c652bdb..26326def 100644 --- a/src/constants/button.ts +++ b/src/constants/button.ts @@ -5,41 +5,118 @@ export const SIZE = { small: 'min-w-[120px] h-[40px] px-3 text-sm', } as const; -export const BASE_CLASSES = { - solid: 'text-white', - outline: 'bg-white border', - lightSolid: '', - lightOutline: 'border', -} as const; - -export const COLOR_GROUPS = { - 'green-normal-01': { - bg: 'bg-green-normal-01', - text: 'text-green-normal-01', - border: 'border-green-normal-01', +export const COLOR_SCHEMES = { + 'gray-white': { + bg: 'bg-gray-white', + text: 'text-gray-white', + border: 'border-gray-white', }, - 'green-light-03': { - bg: 'bg-green-light-03', - text: 'text-green-normal-01', - border: 'border-green-normal-01', + 'gray-light-01': { + bg: 'bg-gray-light-01', + text: 'text-gray-light-01', + border: 'border-gray-light-01', }, - 'gray-normal-03': { - bg: 'bg-gray-normal-03', - text: 'text-gray-darker', - border: 'border-gray-darker', + 'gray-light-02': { + bg: 'bg-gray-light-02', + text: 'text-gray-light-02', + border: 'border-gray-light-02', + }, + 'gray-normal-01': { + bg: 'bg-gray-normal-01', + text: 'text-gray-normal-01', + border: 'border-gray-normal-01', }, 'gray-normal-02': { bg: 'bg-gray-normal-02', - text: 'text-gray-dark-02', + text: 'text-gray-normal-02', border: 'border-gray-normal-02', }, + 'gray-normal-03': { + bg: 'bg-gray-normal-03', + text: 'text-gray-normal-03', + border: 'border-gray-normal-03', + }, + 'gray-dark-01': { + bg: 'bg-gray-dark-01', + text: 'text-gray-dark-01', + border: 'border-gray-dark-01', + }, + 'gray-dark-02': { + bg: 'bg-gray-dark-02', + text: 'text-gray-dark-02', + border: 'border-gray-dark-02', + }, + 'gray-dark-03': { + bg: 'bg-gray-dark-03', + text: 'text-gray-dark-03', + border: 'border-gray-dark-03', + }, 'gray-darker': { bg: 'bg-gray-darker', text: 'text-gray-darker', border: 'border-gray-darker', }, + 'gray-black': { + bg: 'bg-gray-black', + text: 'text-gray-black', + border: 'border-gray-black', + }, + 'green-light-01': { + bg: 'bg-green-light-01', + text: 'text-green-light-01', + border: 'border-green-light-01', + }, + 'green-light-02': { + bg: 'bg-green-light-02', + text: 'text-green-light-02', + border: 'border-green-light-02', + }, + 'green-light-03': { + bg: 'bg-green-light-03', + text: 'text-green-light-03', + border: 'border-green-light-03', + }, + 'green-normal-01': { + bg: 'bg-green-normal-01', + text: 'text-green-normal-01', + border: 'border-green-normal-01', + }, + 'green-normal-02': { + bg: 'bg-green-normal-02', + text: 'text-green-normal-02', + border: 'border-green-normal-02', + }, + 'green-normal-03': { + bg: 'bg-green-normal-03', + text: 'text-green-normal-03', + border: 'border-green-normal-03', + }, + 'green-dark-01': { + bg: 'bg-green-dark-01', + text: 'text-green-dark-01', + border: 'border-green-dark-01', + }, + 'green-dark-02': { + bg: 'bg-green-dark-02', + text: 'text-green-dark-02', + border: 'border-green-dark-02', + }, + 'green-dark-03': { + bg: 'bg-green-dark-03', + text: 'text-green-dark-03', + border: 'border-green-dark-03', + }, + 'green-darker': { + bg: 'bg-green-darker', + text: 'text-green-darker', + border: 'border-green-darker', + }, + 'blue-normal-01': { + bg: 'bg-blue-normal-01', + text: 'text-blue-normal-01', + border: 'border-blue-normal-01', + }, } as const; export type ButtonSize = keyof typeof SIZE; -export type ButtonFillType = keyof typeof BASE_CLASSES; -export type ButtonColorGroup = keyof typeof COLOR_GROUPS; +export type ButtonColorGroup = keyof typeof COLOR_SCHEMES; From 2b22db772244ad5314eb070169ad5485e8a3cd04 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:02:21 +0900 Subject: [PATCH 37/47] =?UTF-8?q?=E2=9C=A8[Feat]=20Modal=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8,=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20/=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=AA=A8=EB=8B=AC=20=EA=B0=9C=EB=B0=9C=20#141=20(#?= =?UTF-8?q?145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️[Refactor] avatar xl 사이즈 추가 #141 * ✨[Feat] 수정하기 아이콘 추가 #141 * ♻️[Refactor] className을 prop으로 받아 커스텀 디자인 할 수 있게 리팩토링 #141 * ✨[Feat] Modal 컴포넌트 개발 #141 * ✨[Feat] 프로필 수정 모달 개발 #141 * ♻️[Refactor] 오타 수정 #141 * 🚚[Rename] 프로필 수정 모달 위치 변경 #141 * ✅[Test] 테스트코드, storybook 작성 #141 * 💄[Design] 추가된 색상으로 변경 #141 * 💄[Design] margin 추가 #141 * 💄[Design] 제목 텍스트 크기 변경 #141 * ♻️[Refactor] 코드 위치 가독성 좋게 수정 #141 * ✨[Feat] 리뷰 평점 하트 개발 #141 * ✨[Feat] 리뷰 작성 모달 개발 #141 * ✅[Test] 스토리북 수정 #141 * ♻️[Refactor] 닫기 버튼 icClose로 변경 #141 * ♻️[Refactor] 하트 아이콘 ratingIcon으로 변경 #141 * 🔥[Remove] 리뷰하트아이콘 삭제 #141 * ✅[Test] 테스트 코드 수정 #141 --- public/icons/EditIcon.tsx | 26 ++++ src/components/button/Button.tsx | 5 +- src/components/modal/Modal.stories.tsx | 48 +++++++ src/components/modal/Modal.test.tsx | 56 +++++++++ src/components/modal/Modal.tsx | 73 +++++++++++ src/constants/avatar.ts | 1 + .../profile/components/ProfileEditModal.tsx | 118 ++++++++++++++++++ .../profile/components/WriteReviewModal.tsx | 101 +++++++++++++++ src/features/profile/components/index.ts | 1 - 9 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 public/icons/EditIcon.tsx create mode 100644 src/components/modal/Modal.stories.tsx create mode 100644 src/components/modal/Modal.test.tsx create mode 100644 src/components/modal/Modal.tsx create mode 100644 src/features/profile/components/ProfileEditModal.tsx create mode 100644 src/features/profile/components/WriteReviewModal.tsx delete mode 100644 src/features/profile/components/index.ts diff --git a/public/icons/EditIcon.tsx b/public/icons/EditIcon.tsx new file mode 100644 index 00000000..da762f81 --- /dev/null +++ b/public/icons/EditIcon.tsx @@ -0,0 +1,26 @@ +import { SVGProps } from 'react'; + +interface EditIconProps extends SVGProps { + width?: number; + height?: number; +} + +function EditIcon({ width = 7, height = 9, ...props }: EditIconProps) { + return ( + + + + ); +} + +export default EditIcon; diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index 9f5e5478..bf36618c 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -2,13 +2,14 @@ import React from 'react'; import { twMerge } from 'tailwind-merge'; import { COLOR_SCHEMES, SIZE } from '@/constants'; -interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { +export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { text: string; size: 'large' | 'medium' | 'small' | 'modal'; fillType: 'solid' | 'outline' | 'lightSolid'; themeColor: keyof typeof COLOR_SCHEMES; lightColor?: keyof typeof COLOR_SCHEMES; isSubmitting?: boolean; + className?: string; } export default function Button({ @@ -18,6 +19,7 @@ export default function Button({ themeColor = 'green-normal-01', lightColor, isSubmitting, + className, ...buttonProps }: ButtonProps) { const { disabled } = buttonProps; @@ -54,6 +56,7 @@ export default function Button({ baseClasses, variantClasses, isButtonDisabled && 'cursor-not-allowed', + className, ); return ( diff --git a/src/components/modal/Modal.stories.tsx b/src/components/modal/Modal.stories.tsx new file mode 100644 index 00000000..7193e05e --- /dev/null +++ b/src/components/modal/Modal.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Modal from './Modal'; + +const meta: Meta = { + title: 'Components/Modal', + component: Modal, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isOpen: { + control: 'boolean', + description: '모달의 표시 여부를 제어합니다', + }, + title: { + control: 'text', + description: '모달의 제목', + }, + onClose: { action: '닫기 클릭' }, + onConfirm: { action: '확인 클릭' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isOpen: true, + title: '모달 제목', + cancelText: '취소', + confirmText: '확인', + children: '모달에 넣을 children', + onClose: () => console.log('닫기 클릭'), + onConfirm: () => console.log('확인 클릭'), + cancelButtonProps: { + themeColor: 'gray-dark-01', + lightColor: 'gray-normal-01', + fillType: 'lightSolid', + }, + confirmButtonProps: { + themeColor: 'green-normal-01', + lightColor: 'green-light-03', + fillType: 'lightSolid', + }, + }, +}; diff --git a/src/components/modal/Modal.test.tsx b/src/components/modal/Modal.test.tsx new file mode 100644 index 00000000..defd2bf0 --- /dev/null +++ b/src/components/modal/Modal.test.tsx @@ -0,0 +1,56 @@ +import '@testing-library/jest-dom'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Modal from './Modal'; + +describe('Modal 컴포넌트', () => { + const defaultProps = { + isOpen: true, + onClose: jest.fn(), + title: '테스트 모달', + onConfirm: jest.fn(), + cancelText: '취소', + confirmText: '확인', + children:
모달 내용
, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('isOpen이 true일 때 모달이 렌더링되어야 함', () => { + render(); + + expect(screen.getByText('테스트 모달')).toBeInTheDocument(); + expect(screen.getByText('모달 내용')).toBeInTheDocument(); + expect(screen.getByText('취소')).toBeInTheDocument(); + expect(screen.getByText('확인')).toBeInTheDocument(); + }); + + it('isOpen이 false일 때 모달이 렌더링되지 않아야 함', () => { + render(); + + expect(screen.queryByText('테스트 모달')).not.toBeInTheDocument(); + }); + + it('닫기 버튼 클릭 시 onClose가 호출되어야 함', () => { + render(); + + const closeButton = screen.getByLabelText('닫기'); + fireEvent.click(closeButton); + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + it('취소 버튼 클릭 시 onClose가 호출되어야 함', () => { + render(); + + fireEvent.click(screen.getByText('취소')); + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + it('확인 버튼 클릭 시 onConfirm이 호출되어야 함', () => { + render(); + + fireEvent.click(screen.getByText('확인')); + expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx new file mode 100644 index 00000000..8896b800 --- /dev/null +++ b/src/components/modal/Modal.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import Button, { ButtonProps } from '../button/Button'; +import { IcClose } from '../../../public/icons'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; + title: string; + onConfirm: () => void; + cancelText: string; + confirmText: string; + cancelButtonProps?: Partial; + confirmButtonProps?: Partial; +} + +function Modal({ + isOpen, + onClose, + children, + title, + onConfirm, + cancelText, + confirmText, + cancelButtonProps, + confirmButtonProps, +}: ModalProps) { + if (!isOpen) return null; + + return ( +
+
+ +
+
+

{title}

+ +
+ +
{children}
+ +
+
+
+
+ ); +} + +export default Modal; diff --git a/src/constants/avatar.ts b/src/constants/avatar.ts index 991c7b9c..4e6f7028 100644 --- a/src/constants/avatar.ts +++ b/src/constants/avatar.ts @@ -2,6 +2,7 @@ export const AVATAR_SIZE = { sm: 'h-[29px] w-[29px]', md: 'h-[38px] w-[38px]', lg: 'h-[56px] w-[56px]', + xl: 'h-[74px] w-[71px]', } as const; export type AvatarSize = keyof typeof AVATAR_SIZE; diff --git a/src/features/profile/components/ProfileEditModal.tsx b/src/features/profile/components/ProfileEditModal.tsx new file mode 100644 index 00000000..361dfd57 --- /dev/null +++ b/src/features/profile/components/ProfileEditModal.tsx @@ -0,0 +1,118 @@ +import { useState } from 'react'; +import { useAuthStore } from '@/store/authStore'; +import Avatar from '@/components/avatar/Avatar'; +import EditIcon from '../../../../public/icons/EditIcon'; +import Modal from '@/components/modal/Modal'; + +interface ProfileData { + name: string; + companyName: string; + image?: string | null; +} + +interface ProfileEditModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: (updatedData: ProfileData) => void; + profileData: ProfileData; +} + +function ProfileEditContent({ + formData, + handleChange, +}: { + formData: ProfileData; + handleChange: (e: React.ChangeEvent) => void; +}) { + return ( +
+
+
+
+ +
+ +
+ +
+
+ 닉네임 + +
+
+ 한 줄 소개 + +
+
+
+
+ ); +} + +function ProfileEditModal({ + isOpen, + onClose, + onConfirm, + profileData, +}: ProfileEditModalProps) { + const { user } = useAuthStore(); + const [formData, setFormData] = useState({ + name: profileData.name || user?.name || '', + companyName: profileData.companyName || user?.companyName || '', + image: profileData.image || user?.image || '/images/profile.png', + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleConfirm = () => { + onConfirm(formData); + }; + + return ( + + + + ); +} + +export default ProfileEditModal; diff --git a/src/features/profile/components/WriteReviewModal.tsx b/src/features/profile/components/WriteReviewModal.tsx new file mode 100644 index 00000000..0f2ba14d --- /dev/null +++ b/src/features/profile/components/WriteReviewModal.tsx @@ -0,0 +1,101 @@ +'use client'; + +import React, { useState } from 'react'; +import Modal from '@/components/modal/Modal'; +import RatingIcon from '../../../../public/icons/RatingIcon'; + +const INITIAL_RATING = 5; +const RATING_RANGE = [1, 2, 3, 4, 5] as const; + +interface WriteReviewContentProps { + rating: number; + setRating: (rating: number) => void; + review: string; + setReview: (review: string) => void; +} + +interface WriteReviewModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: (rating: number, review: string) => void; +} +function WriteReviewContent({ + rating, + setRating, + review, + setReview, +}: WriteReviewContentProps) { + return ( +
+
+

+ 모임은 어떠셨나요? +

+
+ {RATING_RANGE.map((heart) => ( + + ))} +
+
+
+

+ 모임에 참여한 경험을 공유해주세요. +

+