From f884343433a202061fcad5f53703e635191a0b56 Mon Sep 17 00:00:00 2001 From: jjunier Date: Sat, 9 May 2026 14:34:16 +0900 Subject: [PATCH 1/7] feat(frontend): set up frontend foundation and CI pipeline [D2C-31] - scaffold React, Vite, and TypeScript frontend app under apps/web - add React Router based page routing and shared MainLayout - add fetch-based API client and localStorage utilities for user/cart state - add placeholder pages for home, products, auth, cart, checkout, orders, and reviews - apply initial E-Commerce header policy and global styles - add GitHub Actions CI workflow for backend and frontend validation - add PR template and Python lint configurations for ruff and pylint --- .env.example | 2 - .github/pull_request_template.md | 51 + .github/workflows/ci.yml | 124 ++ .gitignore | 8 + .pylintrc | 25 + apps/web/.env.example | 1 + apps/web/index.html | 12 + apps/web/next.config.ts | 0 apps/web/package-lock.json | 1766 +++++++++++++++++ apps/web/package.json | 24 + apps/web/src/app/App.tsx | 6 + apps/web/src/app/router.tsx | 56 + apps/web/src/components/layout/MainLayout.tsx | 47 + apps/web/src/features/auth/LoginPage.tsx | 8 + apps/web/src/features/auth/SignupPage.tsx | 8 + apps/web/src/features/cart/CartPage.tsx | 8 + .../src/features/checkout/CheckoutPage.tsx | 8 + apps/web/src/features/home/HomePage.tsx | 19 + .../src/features/orders/OrderHistoryPage.tsx | 8 + .../features/products/ProductDetailPage.tsx | 13 + .../src/features/products/ProductListPage.tsx | 8 + .../src/features/reviews/ReviewCreatePage.tsx | 8 + apps/web/src/main.tsx | 10 + apps/web/src/services/apiClient.ts | 46 + apps/web/src/stores/userStore.ts | 45 + apps/web/src/styles/global.css | 156 ++ apps/web/tsconfig.json | 22 + apps/web/vite.config.ts | 10 + pyproject.toml | 17 + 29 files changed, 2514 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .pylintrc create mode 100644 apps/web/.env.example create mode 100644 apps/web/index.html delete mode 100644 apps/web/next.config.ts create mode 100644 apps/web/package-lock.json create mode 100644 apps/web/src/app/App.tsx create mode 100644 apps/web/src/app/router.tsx create mode 100644 apps/web/src/components/layout/MainLayout.tsx create mode 100644 apps/web/src/features/auth/LoginPage.tsx create mode 100644 apps/web/src/features/auth/SignupPage.tsx create mode 100644 apps/web/src/features/cart/CartPage.tsx create mode 100644 apps/web/src/features/checkout/CheckoutPage.tsx create mode 100644 apps/web/src/features/home/HomePage.tsx create mode 100644 apps/web/src/features/orders/OrderHistoryPage.tsx create mode 100644 apps/web/src/features/products/ProductDetailPage.tsx create mode 100644 apps/web/src/features/products/ProductListPage.tsx create mode 100644 apps/web/src/features/reviews/ReviewCreatePage.tsx create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/services/apiClient.ts create mode 100644 apps/web/src/stores/userStore.ts create mode 100644 apps/web/src/styles/global.css create mode 100644 apps/web/vite.config.ts create mode 100644 pyproject.toml diff --git a/.env.example b/.env.example index bdb3307..faa59b7 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,3 @@ -NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 - APP_ENV=local APP_PORT=8000 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e69de29..57231b6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -0,0 +1,51 @@ +## 개요 + + +--- + +## 포함 범위 + + +--- + +## 검증 결과 + + +--- + +## 브랜치 통합 방식 + + +--- + +## 향후 브랜치 운영 원칙 + + +--- + +## 체크리스트 + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..41d5f35 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,124 @@ +name: CI + +on: + pull_request: + branches: + - develop + - main + push: + branches: + - develop + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend: + name: Backend CI + runs-on: ubuntu-latest + + defaults: + run: + working-directory: apps/api + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: d2c_commerce + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d d2c_commerce" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + APP_ENV: test + APP_PORT: 8000 + POSTGRES_DB: d2c_commerce + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/d2c_commerce + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install PostgreSQL client + run: | + sudo apt-get update + sudo apt-get install -y postgresql-client + working-directory: . + + - name: Install backend dependencies + run: | + python -m pip install --upgrade pip + pip install -r ../../requirements.txt + pip install ruff pylint pytest + + - name: Run Ruff + run: | + ruff check backend tests + + - name: Run Pylint + run: | + pylint backend tests + + - name: Apply database schema + run: | + psql "$DATABASE_URL" -f ../../db/ddl/schema.sql + + - name: Load seed data + run: | + psql "$DATABASE_URL" -f ../../db/seeds/seed_categories.sql + psql "$DATABASE_URL" -f ../../db/seeds/seed_campaigns.sql + psql "$DATABASE_URL" -f ../../db/seeds/seed_products.sql + psql "$DATABASE_URL" -f ../../db/seeds/seed_coupons.sql + + - name: Run backend tests + run: | + python -m pytest ./tests -v + + frontend: + name: Frontend CI + runs-on: ubuntu-latest + + defaults: + run: + working-directory: apps/web + + env: + VITE_API_BASE_URL: http://localhost:8000 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: apps/web/package-lock.json + + - name: Install frontend dependencies + run: npm ci + + - name: Run frontend typecheck + run: npm run typecheck + + - name: Build frontend + run: npm run build \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2674ebb..6fa9fed 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .env.local .env.* !.env.example +!**/.env.example # Virtual environments .venv/ @@ -41,10 +42,17 @@ htmlcov/ # Node / Frontend # ========================= node_modules/ +**/node_modules/ .next/ dist/ +**/dist/ out/ coverage/ +**/coverage/ + +# Vite cache +.vite/ +**/.vite/ # ========================= # Logs diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..6a07424 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,25 @@ +[MASTER] +ignore=.venv,venv,node_modules,dist,build +ignore-patterns=node_modules,dist,build + +[MESSAGES CONTROL] +disable= + C0114, + C0115, + C0116, + R0903, + R0913, + R0914, + R0915, + W0718, + W1203 + +[FORMAT] +max-line-length=100 + +[DESIGN] +max-args=8 +max-locals=20 +max-returns=8 +max-branches=15 +max-statements=60 \ No newline at end of file diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 0000000..b53dc6b --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8000 \ No newline at end of file diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..97ce28c --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + D2C Commerce Prototype + + +
+ + + \ No newline at end of file diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json new file mode 100644 index 0000000..f8d2c82 --- /dev/null +++ b/apps/web/package-lock.json @@ -0,0 +1,1766 @@ +{ + "name": "d2c-commerce-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "d2c-commerce-web", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^5.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0", + "typescript": "^5.8.0", + "vite": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz", + "integrity": "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "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==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.352", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.352.tgz", + "integrity": "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.0.tgz", + "integrity": "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.0.tgz", + "integrity": "sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.15.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/apps/web/package.json b/apps/web/package.json index e69de29..f564b31 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -0,0 +1,24 @@ +{ + "name": "d2c-commerce-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@vitejs/plugin-react": "^5.0.0", + "vite": "^7.0.0", + "typescript": "^5.8.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0" + } +} \ No newline at end of file diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx new file mode 100644 index 0000000..c598ae6 --- /dev/null +++ b/apps/web/src/app/App.tsx @@ -0,0 +1,6 @@ +import { RouterProvider } from "react-router-dom"; +import { router } from "./router"; + +export function App() { + return ; +} \ No newline at end of file diff --git a/apps/web/src/app/router.tsx b/apps/web/src/app/router.tsx new file mode 100644 index 0000000..b2ebd05 --- /dev/null +++ b/apps/web/src/app/router.tsx @@ -0,0 +1,56 @@ +import { createBrowserRouter } from "react-router-dom"; +import { MainLayout } from "../components/layout/MainLayout"; +import { HomePage } from "../features/home/HomePage"; +import { ProductListPage } from "../features/products/ProductListPage"; +import { ProductDetailPage } from "../features/products/ProductDetailPage"; +import { SignupPage } from "../features/auth/SignupPage"; +import { LoginPage } from "../features/auth/LoginPage"; +import { CartPage } from "../features/cart/CartPage"; +import { CheckoutPage } from "../features/checkout/CheckoutPage"; +import { OrderHistoryPage } from "../features/orders/OrderHistoryPage"; +import { ReviewCreatePage } from "../features/reviews/ReviewCreatePage"; + +export const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + index: true, + element: + }, + { + path: "products", + element: + }, + { + path: "products/:productId", + element: + }, + { + path: "signup", + element: + }, + { + path: "login", + element: + }, + { + path: "cart", + element: + }, + { + path: "checkout", + element: + }, + { + path: "orders", + element: + }, + { + path: "reviews/new", + element: + } + ] + } +]); \ No newline at end of file diff --git a/apps/web/src/components/layout/MainLayout.tsx b/apps/web/src/components/layout/MainLayout.tsx new file mode 100644 index 0000000..e1a8c1e --- /dev/null +++ b/apps/web/src/components/layout/MainLayout.tsx @@ -0,0 +1,47 @@ +import { Link, Outlet } from "react-router-dom"; +import { clearStoredUser, getStoredUser } from "../../stores/userStore"; + +export function MainLayout() { + const user = getStoredUser(); + + const handleLogout = () => { + clearStoredUser(); + window.location.href = "/"; + }; + + return ( +
+
+ + D2C Commerce + + + +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/auth/LoginPage.tsx b/apps/web/src/features/auth/LoginPage.tsx new file mode 100644 index 0000000..8d3a8f7 --- /dev/null +++ b/apps/web/src/features/auth/LoginPage.tsx @@ -0,0 +1,8 @@ +export function LoginPage() { + return ( +
+

로그인

+

D2C-36에서 로그인 폼과 사용자 상태 저장 흐름을 구현합니다.

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/auth/SignupPage.tsx b/apps/web/src/features/auth/SignupPage.tsx new file mode 100644 index 0000000..d111981 --- /dev/null +++ b/apps/web/src/features/auth/SignupPage.tsx @@ -0,0 +1,8 @@ +export function SignupPage() { + return ( +
+

회원가입

+

D2C-35에서 회원가입 폼과 사용자 생성 API를 연동합니다.

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/cart/CartPage.tsx b/apps/web/src/features/cart/CartPage.tsx new file mode 100644 index 0000000..3ade2f3 --- /dev/null +++ b/apps/web/src/features/cart/CartPage.tsx @@ -0,0 +1,8 @@ +export function CartPage() { + return ( +
+

장바구니

+

D2C-37에서 장바구니 조회, 상품 제거, 체크아웃 진입 흐름을 구현합니다.

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/checkout/CheckoutPage.tsx b/apps/web/src/features/checkout/CheckoutPage.tsx new file mode 100644 index 0000000..e4dd675 --- /dev/null +++ b/apps/web/src/features/checkout/CheckoutPage.tsx @@ -0,0 +1,8 @@ +export function CheckoutPage() { + return ( +
+

체크아웃

+

D2C-38에서 쿠폰 적용, 주문 생성, 결제 시뮬레이션 흐름을 구현합니다.

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/home/HomePage.tsx b/apps/web/src/features/home/HomePage.tsx new file mode 100644 index 0000000..7320e61 --- /dev/null +++ b/apps/web/src/features/home/HomePage.tsx @@ -0,0 +1,19 @@ +import { Link } from "react-router-dom"; + +export function HomePage() { + return ( +
+

D2C Commerce Prototype

+

상품 탐색부터 주문, 결제, 리뷰 작성까지 이어지는 D2C 커머스 프로토타입입니다.

+ +
+ + 상품 보러가기 + + + 주문 내역 확인 + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/orders/OrderHistoryPage.tsx b/apps/web/src/features/orders/OrderHistoryPage.tsx new file mode 100644 index 0000000..9f0ad83 --- /dev/null +++ b/apps/web/src/features/orders/OrderHistoryPage.tsx @@ -0,0 +1,8 @@ +export function OrderHistoryPage() { + return ( +
+

주문 내역

+

D2C-39에서 주문 내역 조회와 결제 상태 확인 흐름을 구현합니다.

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/products/ProductDetailPage.tsx b/apps/web/src/features/products/ProductDetailPage.tsx new file mode 100644 index 0000000..da52b45 --- /dev/null +++ b/apps/web/src/features/products/ProductDetailPage.tsx @@ -0,0 +1,13 @@ +import { useParams } from "react-router-dom"; + +export function ProductDetailPage() { + const { productId } = useParams(); + + return ( +
+

상품 상세

+

D2C-34에서 상품 상세 조회와 장바구니 담기 흐름을 구현합니다.

+

productId: {productId}

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/products/ProductListPage.tsx b/apps/web/src/features/products/ProductListPage.tsx new file mode 100644 index 0000000..11aacdb --- /dev/null +++ b/apps/web/src/features/products/ProductListPage.tsx @@ -0,0 +1,8 @@ +export function ProductListPage() { + return ( +
+

카테고리/상품 목록

+

D2C-33에서 카테고리 및 상품 목록 API를 연동합니다.

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/reviews/ReviewCreatePage.tsx b/apps/web/src/features/reviews/ReviewCreatePage.tsx new file mode 100644 index 0000000..6deb8c1 --- /dev/null +++ b/apps/web/src/features/reviews/ReviewCreatePage.tsx @@ -0,0 +1,8 @@ +export function ReviewCreatePage() { + return ( +
+

리뷰 작성

+

D2C-40에서 주문 상품 기반 리뷰 작성 흐름을 구현합니다.

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..3f1a3e6 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./app/App"; +import "./styles/global.css"; + +createRoot(document.getElementById("root")!).render( + + + +); \ No newline at end of file diff --git a/apps/web/src/services/apiClient.ts b/apps/web/src/services/apiClient.ts new file mode 100644 index 0000000..3e392b1 --- /dev/null +++ b/apps/web/src/services/apiClient.ts @@ -0,0 +1,46 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; + +type RequestOptions = { + method?: "GET" | "POST" | "PATCH" | "DELETE"; + body?: unknown; + headers?: Record; +}; + +export class ApiError extends Error { + status: number; + detail: unknown; + + constructor(status: number, detail: unknown) { + super(typeof detail === "string" ? detail : "API request failed"); + this.name = "ApiError"; + this.status = status; + this.detail = detail; + } +} + +export async function apiClient( + path: string, + options: RequestOptions = {} +): Promise { + const { method = "GET", body, headers = {} } = options; + + const response = await fetch(`${API_BASE_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + ...headers + }, + body: body ? JSON.stringify(body) : undefined + }); + + const contentType = response.headers.get("content-type"); + const isJson = contentType?.includes("application/json"); + + const data = isJson ? await response.json() : null; + + if (!response.ok) { + throw new ApiError(response.status, data?.detail ?? data); + } + + return data as T; +} \ No newline at end of file diff --git a/apps/web/src/stores/userStore.ts b/apps/web/src/stores/userStore.ts new file mode 100644 index 0000000..09d3434 --- /dev/null +++ b/apps/web/src/stores/userStore.ts @@ -0,0 +1,45 @@ +const USER_STORAGE_KEY = "d2c_user"; +const CART_STORAGE_KEY = "d2c_cart_id"; + +export type StoredUser = { + user_id: string; + email: string; + user_name: string; + user_status: string; +}; + +export function getStoredUser(): StoredUser | null { + const raw = localStorage.getItem(USER_STORAGE_KEY); + + if (!raw) { + return null; + } + + try { + return JSON.parse(raw) as StoredUser; + } catch { + localStorage.removeItem(USER_STORAGE_KEY); + return null; + } +} + +export function setStoredUser(user: StoredUser) { + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user)); +} + +export function clearStoredUser() { + localStorage.removeItem(USER_STORAGE_KEY); + localStorage.removeItem(CART_STORAGE_KEY); +} + +export function getStoredCartId(): string | null { + return localStorage.getItem(CART_STORAGE_KEY); +} + +export function setStoredCartId(cartId: string) { + localStorage.setItem(CART_STORAGE_KEY, cartId); +} + +export function clearStoredCartId() { + localStorage.removeItem(CART_STORAGE_KEY); +} \ No newline at end of file diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css new file mode 100644 index 0000000..dc3ffdb --- /dev/null +++ b/apps/web/src/styles/global.css @@ -0,0 +1,156 @@ +:root { + font-family: + Inter, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + color: #1f2937; + background-color: #f8fafc; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +select, +textarea { + font: inherit; +} + +.app-shell { + min-height: 100vh; + background-color: #f8fafc; +} + +.app-header { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 16px 32px; + border-bottom: 1px solid #e5e7eb; + background-color: #ffffff; +} + +.app-logo { + font-size: 20px; + font-weight: 700; +} + +.app-nav { + display: flex; + align-items: center; + gap: 16px; + font-size: 14px; +} + +.app-nav a { + color: #374151; +} + +.app-nav a:hover { + color: #111827; +} + +.app-user { + color: #6b7280; + font-weight: 500; +} + +.link-button { + border: 0; + padding: 0; + color: #374151; + background: none; + cursor: pointer; +} + +.link-button:hover { + color: #111827; +} + +.app-main { + width: min(1120px, calc(100% - 32px)); + margin: 0 auto; + padding: 48px 0; +} + +.page-section { + padding: 32px; + border: 1px solid #e5e7eb; + border-radius: 16px; + background-color: #ffffff; +} + +.page-section h1 { + margin: 0 0 12px; + font-size: 28px; +} + +.page-section p { + margin: 0 0 16px; + color: #4b5563; + line-height: 1.6; +} + +.action-row { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 24px; +} + +.primary-link, +.secondary-link { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 40px; + padding: 0 16px; + border-radius: 10px; + font-weight: 600; +} + +.primary-link { + color: #ffffff; + background-color: #111827; +} + +.secondary-link { + color: #111827; + border: 1px solid #d1d5db; + background-color: #ffffff; +} + +.signup-link { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 34px; + padding: 0 12px; + border-radius: 999px; + color: #ffffff !important; + background-color: #111827; +} + +.signup-link:hover { + background-color: #374151; +} \ No newline at end of file diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index e69de29..e76ea3a 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client"] + }, + "include": ["src"], + "references": [] +} \ No newline at end of file diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..52b58b8 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + host: "127.0.0.1", + port: 5173 + } +}); \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..01ac6be --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.ruff] +line-length = 100 +target-version = "py312" +exclude = [ + ".git", + ".venv", + "venv", + "node_modules", + "dist", + "build", + "apps/web/node_modules", + "apps/web/dist", +] + +[tool.ruff.lint] +select = ["E", "F", "I", "B"] +ignore = [] \ No newline at end of file From 3df57aa1150fce6ed199e92d1fa3a7b809dbd1b8 Mon Sep 17 00:00:00 2001 From: jjunier Date: Sat, 9 May 2026 17:12:25 +0900 Subject: [PATCH 2/7] fix(backend): remove unused payment approval variable [D2C-31] - remove unused approved_amount assignment in payment simulation service - ensure backend ruff check passes in CI --- apps/api/backend/services/payment_service.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/api/backend/services/payment_service.py b/apps/api/backend/services/payment_service.py index 7fdc1d3..d32905a 100644 --- a/apps/api/backend/services/payment_service.py +++ b/apps/api/backend/services/payment_service.py @@ -105,7 +105,6 @@ def simulate_payment(payload: PaymentSimulationRequest) -> dict[str, Any]: if payload.simulate_result == "success": payment_status = "paid" paid_amount = requested_amount - approved_amount = requested_amount failure_code = None paid_at = datetime.now() pg_provider = "mock_pg" @@ -117,7 +116,6 @@ def simulate_payment(payload: PaymentSimulationRequest) -> dict[str, Any]: else: payment_status = "failed" paid_amount = Decimal("0") - approved_amount = Decimal("0") failure_code = "SIMULATED_FAILURE" paid_at = None pg_provider = "mock_pg" From 4b9a49dfc69520690745f8947b05279ad9ec1e7c Mon Sep 17 00:00:00 2001 From: jjunier Date: Sat, 9 May 2026 17:34:52 +0900 Subject: [PATCH 3/7] ci: relax initial backend pylint rules for CI baseline [D2C-31] - ensure GitHub Actions uses the root .pylintrc file - relax pylint convention and refactor rules for existing backend code - keep backend CI focused on ruff, pylint baseline, and pytest validation --- .github/workflows/ci.yml | 2 +- .pylintrc | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41d5f35..e697779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: - name: Run Pylint run: | - pylint backend tests + pylint --rcfile=../../.pylintrc backend tests - name: Apply database schema run: | diff --git a/.pylintrc b/.pylintrc index 6a07424..6235707 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,4 +1,4 @@ -[MASTER] +[MAIN] ignore=.venv,venv,node_modules,dist,build ignore-patterns=node_modules,dist,build @@ -7,10 +7,17 @@ disable= C0114, C0115, C0116, + C0301, + C0303, + C0304, + C0411, + R0801, R0903, + R0912, R0913, R0914, R0915, + R1730, W0718, W1203 @@ -19,7 +26,7 @@ max-line-length=100 [DESIGN] max-args=8 -max-locals=20 +max-locals=30 max-returns=8 -max-branches=15 -max-statements=60 \ No newline at end of file +max-branches=20 +max-statements=80 \ No newline at end of file From 1e2d091ef70f5c6792fbc70e503d5a4cbcb81da9 Mon Sep 17 00:00:00 2001 From: jjunier Date: Sat, 9 May 2026 17:45:20 +0900 Subject: [PATCH 4/7] ci: add psycopg2 driver for backend test environment [D2C-31] - add psycopg2-binary to support SQLAlchemy postgresql:// database URLs - fix backend pytest collection failure in GitHub Actions CI --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 8d4f368..e76a1b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ packaging==26.1 pluggy==1.6.0 psycopg==3.3.3 psycopg-binary==3.3.3 +psycopg2-binary==2.9.10 pydantic==2.13.2 pydantic-settings==2.13.1 pydantic_core==2.46.2 From 098fec732765b7a824f41e28c55dff5dc6f4f37c Mon Sep 17 00:00:00 2001 From: jjunier Date: Sat, 9 May 2026 20:51:04 +0900 Subject: [PATCH 5/7] feat(frontend): implement home entry navigation [D2C-32] - build home page hero section for D2C Commerce prototype - add entry cards for product browsing, cart, checkout, and order history flows - add login and signup CTA section for unauthenticated users - improve home page spacing, typography, and responsive layout --- apps/web/src/features/home/HomePage.tsx | 90 +++++++++++-- apps/web/src/styles/global.css | 171 +++++++++++++++++++++++- 2 files changed, 252 insertions(+), 9 deletions(-) diff --git a/apps/web/src/features/home/HomePage.tsx b/apps/web/src/features/home/HomePage.tsx index 7320e61..8940087 100644 --- a/apps/web/src/features/home/HomePage.tsx +++ b/apps/web/src/features/home/HomePage.tsx @@ -1,19 +1,93 @@ import { Link } from "react-router-dom"; +import { getStoredUser } from "../../stores/userStore"; export function HomePage() { + const user = getStoredUser(); + return ( -
-

D2C Commerce Prototype

-

상품 탐색부터 주문, 결제, 리뷰 작성까지 이어지는 D2C 커머스 프로토타입입니다.

+
+
+
+

D2C Commerce Prototype

+

상품 탐색부터 주문, 결제, 리뷰까지 이어지는 커머스 흐름을 검증합니다.

+

+ D2C Commerce Prototype은 사용자가 상품을 탐색하고, 장바구니에 담고, + 체크아웃과 결제를 거쳐 리뷰를 작성하는 전체 사용자 흐름을 검증하기 위한 + 웹 프로토타입입니다. +

+ +
+ + 상품 둘러보기 + + + 장바구니 확인 + +
+
+
+ +
+ + 01 +

상품 탐색

+

카테고리와 상품 목록을 확인하고, 관심 상품의 상세 정보를 조회합니다.

+ -
- - 상품 보러가기 + + 02 +

장바구니

+

상품을 장바구니에 담고, 주문 전 상품 구성과 금액을 확인합니다.

- - 주문 내역 확인 + + + 03 +

체크아웃

+

쿠폰 적용, 주문 생성, 결제 시뮬레이션으로 이어지는 구매 흐름을 진행합니다.

+ + + + 04 +

주문 내역

+

주문 상태와 결제 상태를 확인하고, 구매 상품 기반 리뷰 작성으로 이동합니다.

+ +
+ {user ? ( + <> +
+

{user.user_name}님, 계속해서 구매 흐름을 확인해보세요.

+

주문 내역과 장바구니에서 이전에 진행하던 흐름을 이어갈 수 있습니다.

+
+
+ + 주문 내역 보기 + + + 장바구니 보기 + +
+ + ) : ( + <> +
+

회원가입 또는 로그인 후 전체 구매 흐름을 검증할 수 있습니다.

+

+ 로그인 이후 사용자 식별자를 기반으로 장바구니, 주문, 리뷰 흐름이 연결됩니다. +

+
+
+ + 로그인 + + + 회원가입 + +
+ + )} +
); } \ No newline at end of file diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index dc3ffdb..1499d19 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -1,6 +1,10 @@ :root { font-family: Inter, + "Pretendard", + "Noto Sans KR", + "Apple SD Gothic Neo", + "Malgun Gothic", system-ui, -apple-system, BlinkMacSystemFont, @@ -88,7 +92,7 @@ textarea { } .app-main { - width: min(1120px, calc(100% - 32px)); + width: min(1180px, calc(100% - 48px)); margin: 0 auto; padding: 48px 0; } @@ -153,4 +157,169 @@ textarea { .signup-link:hover { background-color: #374151; +} + +.home-page { + display: flex; + flex-direction: column; + gap: 28px; +} + +.home-hero { + overflow: hidden; + border: 1px solid #e5e7eb; + border-radius: 24px; + background: + radial-gradient(circle at top right, rgba(17, 24, 39, 0.1), transparent 34%), + #ffffff; +} + +.home-hero-content { + max-width: 720px; + padding: 48px 40px; +} + +.home-eyebrow { + margin: 0 0 12px; + color: #6b7280; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.home-hero h1 { + max-width: 680px; + margin: 0 0 18px; + color: #111827; + font-size: clamp(32px, 4vw, 48px); + font-weight: 800; + line-height: 1.18; + letter-spacing: -0.04em; + word-break: keep-all; +} + +.home-hero-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 30px; +} + +.home-panel-actions { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + justify-content: flex-end; + gap: 10px; +} + +.home-description { + max-width: 620px; + margin: 0; + color: #4b5563; + font-size: 16px; + line-height: 1.75; + word-break: keep-all; +} + +.home-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; +} + +.home-card { + display: flex; + min-height: 180px; + flex-direction: column; + gap: 14px; + padding: 26px 24px; + border: 1px solid #e5e7eb; + border-radius: 18px; + background-color: #ffffff; + transition: + transform 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +.home-card:hover { + transform: translateY(-2px); + border-color: #d1d5db; + box-shadow: 0 14px 28px rgba(15, 23, 42, 0.07); +} + +.home-card-label { + color: #9ca3af; + font-size: 12px; + font-weight: 700; +} + +.home-card h2 { + margin: 0; + color: #111827; + font-size: 20px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.home-card p { + margin: 0; + color: #4b5563; + font-size: 15px; + line-height: 1.7; + word-break: keep-all; +} + +.home-account-panel { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 28px 30px; + border: 1px solid #e5e7eb; + border-radius: 20px; + background-color: #ffffff; +} + +.home-account-panel h2 { + margin: 0 0 8px; + color: #111827; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.03em; + word-break: keep-all; +} + +.home-account-panel p { + margin: 0; + color: #4b5563; + line-height: 1.7; + word-break: keep-all; +} + +@media (max-width: 960px) { + .home-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .home-account-panel { + align-items: flex-start; + flex-direction: column; + } + + .home-panel-actions { + justify-content: flex-start; + } +} + +@media (max-width: 640px) { + .home-hero-content { + padding: 40px 24px; + } + + .home-grid { + grid-template-columns: 1fr; + } } \ No newline at end of file From 0e6b131c6e5cf1a103435120a8762f8adcd76b1b Mon Sep 17 00:00:00 2001 From: jjunier Date: Sun, 10 May 2026 19:28:27 +0900 Subject: [PATCH 6/7] feat(frontend): implement product catalog navigation [D2C-33] - connect category and product list APIs to the product catalog page - add category-based product filtering and product detail navigation - implement product card grid with loading, empty, and error states - add client-side pagination with 12, 24, and 48 item display options - scroll to catalog top when changing page or display size - enable CORS for local frontend API requests --- apps/api/backend/main.py | 12 + .../src/features/products/ProductListPage.tsx | 248 ++++++++++- apps/web/src/services/apiClient.ts | 15 +- apps/web/src/services/catalogApi.ts | 11 + apps/web/src/styles/global.css | 395 +++++++++++++++--- apps/web/src/types/catalog.ts | 19 + 6 files changed, 644 insertions(+), 56 deletions(-) create mode 100644 apps/web/src/services/catalogApi.ts create mode 100644 apps/web/src/types/catalog.ts diff --git a/apps/api/backend/main.py b/apps/api/backend/main.py index 6b345df..00e6cf5 100644 --- a/apps/api/backend/main.py +++ b/apps/api/backend/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from backend.api.routes.auth import router as auth_router from backend.api.routes.cart_items import router as cart_items_router @@ -17,6 +18,17 @@ app = FastAPI(title="D2C Commerce Prototype API") +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://localhost:5173", + "http://127.0.0.1:5173", + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + app.include_router(health_router) app.include_router(auth_router) app.include_router(categories_router) diff --git a/apps/web/src/features/products/ProductListPage.tsx b/apps/web/src/features/products/ProductListPage.tsx index 11aacdb..e922dac 100644 --- a/apps/web/src/features/products/ProductListPage.tsx +++ b/apps/web/src/features/products/ProductListPage.tsx @@ -1,8 +1,248 @@ +import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Link } from "react-router-dom"; +import { getCategories, getProducts } from "../../services/catalogApi"; +import type { Category, Product } from "../../types/catalog"; + +const PAGE_SIZE_OPTIONS = [12, 24, 48] as const; +const DEFAULT_PAGE_SIZE = 24; + +function formatPrice(value: string | number, currency: string) { + const numericValue = Number(value); + + if (Number.isNaN(numericValue)) { + return `${value} ${currency}`; + } + + return new Intl.NumberFormat("ko-KR", { + style: "currency", + currency, + maximumFractionDigits: 0, + }).format(numericValue); +} + +function getVisibleProducts(products: Product[], currentPage: number, pageSize: number) { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + + return products.slice(startIndex, endIndex); +} + export function ProductListPage() { + const [categories, setCategories] = useState([]); + const [products, setProducts] = useState([]); + const [selectedCategoryId, setSelectedCategoryId] = useState(""); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [currentPage, setCurrentPage] = useState(1); + const [isLoading, setIsLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(null); + + const productListTopRef = useRef(null); + + const scrollToProductListTop = useCallback(() => { + requestAnimationFrame(() => { + productListTopRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }); + }, []); + + const selectedCategoryName = useMemo(() => { + if (!selectedCategoryId) { + return "전체 상품"; + } + + return ( + categories.find((category) => category.category_id === selectedCategoryId) + ?.category_name ?? "선택한 카테고리" + ); + }, [categories, selectedCategoryId]); + + const totalPages = Math.max(1, Math.ceil(products.length / pageSize)); + + const paginatedProducts = useMemo( + () => getVisibleProducts(products, currentPage, pageSize), + [products, currentPage, pageSize], + ); + + const pageStartItem = products.length === 0 ? 0 : (currentPage - 1) * pageSize + 1; + const pageEndItem = Math.min(currentPage * pageSize, products.length); + + useEffect(() => { + async function loadCategories() { + try { + const categoryData = await getCategories(); + setCategories(categoryData); + } catch { + setErrorMessage("카테고리 목록을 불러오지 못했습니다. 잠시 후 다시 시도해주세요."); + } + } + + loadCategories(); + }, []); + + useEffect(() => { + async function loadProducts() { + try { + setIsLoading(true); + setErrorMessage(null); + + const productData = await getProducts(selectedCategoryId || undefined); + + setProducts(productData); + setCurrentPage(1); + } catch { + setErrorMessage("상품 목록을 불러오지 못했습니다. 잠시 후 다시 시도해주세요."); + } finally { + setIsLoading(false); + } + } + + loadProducts(); + }, [selectedCategoryId]); + + const handleCategorySelect = (categoryId: string) => { + setSelectedCategoryId(categoryId); + }; + + const handlePageSizeChange = (event: ChangeEvent) => { + setPageSize(Number(event.target.value)); + setCurrentPage(1); + scrollToProductListTop(); + }; + + const handlePreviousPage = () => { + setCurrentPage((page) => Math.max(1, page - 1)); + scrollToProductListTop(); + }; + + const handleNextPage = () => { + setCurrentPage((page) => Math.min(totalPages, page + 1)); + scrollToProductListTop(); + }; + return ( -
-

카테고리/상품 목록

-

D2C-33에서 카테고리 및 상품 목록 API를 연동합니다.

+
+
+
+

Product Catalog

+

카테고리별 상품을 탐색해보세요.

+

+ 상품 목록에서 관심 상품을 선택하면 상세 화면으로 이동하여 장바구니 담기 + 흐름을 이어갈 수 있습니다. +

+
+
+ +
+ + + {categories.map((category) => ( + + ))} +
+ +
+
+
+

{selectedCategoryName}

+ + {products.length}개 상품 + {products.length > 0 && ` · ${pageStartItem}-${pageEndItem}개 표시 중`} + +
+
+ + +
+ + {isLoading ? ( +
상품 목록을 불러오는 중입니다.
+ ) : errorMessage ? ( +
{errorMessage}
+ ) : products.length === 0 ? ( +
표시할 상품이 없습니다.
+ ) : ( + <> +
+ {paginatedProducts.map((product) => ( + +
+ {product.brand_name ?? "D2C"} +
+ +
+
+ {product.brand_name ?? "브랜드 미지정"} + {product.product_status} +
+ +

{product.product_name}

+ +
+ {formatPrice(product.sale_price, product.currency)} + {Number(product.list_price) !== Number(product.sale_price) && ( + {formatPrice(product.list_price, product.currency)} + )} +
+
+ + ))} +
+ +
+ + + + {currentPage} / {totalPages} + + + +
+ + )}
); -} \ No newline at end of file +} diff --git a/apps/web/src/services/apiClient.ts b/apps/web/src/services/apiClient.ts index 3e392b1..933e5d3 100644 --- a/apps/web/src/services/apiClient.ts +++ b/apps/web/src/services/apiClient.ts @@ -24,13 +24,18 @@ export async function apiClient( ): Promise { const { method = "GET", body, headers = {} } = options; + const requestHeaders: Record = { + ...headers, + }; + + if (body !== undefined) { + requestHeaders["Content-Type"] = "application/json"; + } + const response = await fetch(`${API_BASE_URL}${path}`, { method, - headers: { - "Content-Type": "application/json", - ...headers - }, - body: body ? JSON.stringify(body) : undefined + headers: requestHeaders, + body: body !== undefined ? JSON.stringify(body) : undefined, }); const contentType = response.headers.get("content-type"); diff --git a/apps/web/src/services/catalogApi.ts b/apps/web/src/services/catalogApi.ts new file mode 100644 index 0000000..2b54d0f --- /dev/null +++ b/apps/web/src/services/catalogApi.ts @@ -0,0 +1,11 @@ +import { apiClient } from "./apiClient"; +import type { Category, Product } from "../types/catalog"; + +export function getCategories() { + return apiClient("/categories"); +} + +export function getProducts(categoryId?: string) { + const query = categoryId ? `?category_id=${categoryId}` : ""; + return apiClient(`/products${query}`); +} \ No newline at end of file diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index 1499d19..39db273 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -36,6 +36,7 @@ textarea { font: inherit; } +/* App layout */ .app-shell { min-height: 100vh; background-color: #f8fafc; @@ -79,6 +80,13 @@ textarea { font-weight: 500; } +.app-main { + width: min(1180px, calc(100% - 48px)); + margin: 0 auto; + padding: 48px 0; +} + +/* Shared actions */ .link-button { border: 0; padding: 0; @@ -91,37 +99,6 @@ textarea { color: #111827; } -.app-main { - width: min(1180px, calc(100% - 48px)); - margin: 0 auto; - padding: 48px 0; -} - -.page-section { - padding: 32px; - border: 1px solid #e5e7eb; - border-radius: 16px; - background-color: #ffffff; -} - -.page-section h1 { - margin: 0 0 12px; - font-size: 28px; -} - -.page-section p { - margin: 0 0 16px; - color: #4b5563; - line-height: 1.6; -} - -.action-row { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 24px; -} - .primary-link, .secondary-link { display: inline-flex; @@ -159,6 +136,42 @@ textarea { background-color: #374151; } +/* Generic placeholder page */ +.page-section { + padding: 32px; + border: 1px solid #e5e7eb; + border-radius: 16px; + background-color: #ffffff; +} + +.page-section h1 { + margin: 0 0 12px; + font-size: 28px; +} + +.page-section p { + margin: 0 0 16px; + color: #4b5563; + line-height: 1.6; +} + +.action-row { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 24px; +} + +.section-eyebrow { + margin: 0 0 10px; + color: #6b7280; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +/* Home page */ .home-page { display: flex; flex-direction: column; @@ -199,21 +212,6 @@ textarea { word-break: keep-all; } -.home-hero-actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 30px; -} - -.home-panel-actions { - display: flex; - flex-shrink: 0; - flex-wrap: wrap; - justify-content: flex-end; - gap: 10px; -} - .home-description { max-width: 620px; margin: 0; @@ -223,6 +221,13 @@ textarea { word-break: keep-all; } +.home-hero-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 30px; +} + .home-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -299,6 +304,267 @@ textarea { word-break: keep-all; } +.home-panel-actions { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + justify-content: flex-end; + gap: 10px; +} + +/* Product list page */ +.product-list-page { + display: flex; + flex-direction: column; + gap: 24px; +} + +.product-list-header { + display: flex; + justify-content: space-between; + gap: 24px; + padding: 32px; + border: 1px solid #e5e7eb; + border-radius: 24px; + background-color: #ffffff; +} + +.product-list-header h1 { + margin: 0 0 12px; + color: #111827; + font-size: clamp(28px, 4vw, 42px); + line-height: 1.18; + letter-spacing: -0.04em; + word-break: keep-all; +} + +.product-list-header p { + max-width: 680px; + margin: 0; + color: #4b5563; + line-height: 1.7; + word-break: keep-all; +} + +.category-filter { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.category-chip { + min-height: 38px; + padding: 0 14px; + border: 1px solid #d1d5db; + border-radius: 999px; + color: #374151; + background-color: #ffffff; + cursor: pointer; +} + +.category-chip:hover { + border-color: #9ca3af; + color: #111827; +} + +.category-chip.active { + border-color: #111827; + color: #ffffff; + background-color: #111827; +} + +.product-list-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + scroll-margin-top: 150px; +} + +.product-list-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.product-list-summary h2 { + margin: 0 0 6px; + color: #111827; + font-size: 22px; + letter-spacing: -0.03em; +} + +.product-list-summary span { + color: #6b7280; + font-size: 14px; +} + +.page-size-control { + display: inline-flex; + align-items: center; + gap: 10px; + color: #4b5563; + font-size: 14px; + white-space: nowrap; +} + +.page-size-control select { + min-height: 38px; + padding: 0 34px 0 12px; + border: 1px solid #d1d5db; + border-radius: 10px; + color: #111827; + background-color: #ffffff; + cursor: pointer; +} + +.page-size-control select:focus { + outline: 2px solid #111827; + outline-offset: 2px; +} + +.product-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; +} + +.product-card { + overflow: hidden; + border: 1px solid #e5e7eb; + border-radius: 18px; + background-color: #ffffff; + transition: + transform 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +.product-card:hover { + transform: translateY(-2px); + border-color: #d1d5db; + box-shadow: 0 14px 28px rgba(15, 23, 42, 0.07); +} + +.product-image-placeholder { + display: flex; + min-height: 160px; + align-items: center; + justify-content: center; + background: + radial-gradient(circle at top right, rgba(17, 24, 39, 0.1), transparent 32%), + #f3f4f6; +} + +.product-image-placeholder span { + color: #6b7280; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.product-card-body { + display: flex; + min-height: 156px; + flex-direction: column; + gap: 12px; + padding: 18px; +} + +.product-meta { + display: flex; + justify-content: space-between; + gap: 10px; + color: #6b7280; + font-size: 12px; +} + +.product-card h3 { + margin: 0; + color: #111827; + font-size: 17px; + line-height: 1.45; + letter-spacing: -0.02em; + word-break: keep-all; +} + +.product-price-row { + display: flex; + align-items: baseline; + gap: 8px; + margin-top: auto; +} + +.product-price-row strong { + color: #111827; + font-size: 18px; +} + +.product-price-row span { + color: #9ca3af; + font-size: 13px; + text-decoration: line-through; +} + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 14px; + margin-top: 8px; +} + +.pagination button { + min-height: 38px; + padding: 0 14px; + border: 1px solid #d1d5db; + border-radius: 10px; + color: #111827; + background-color: #ffffff; + cursor: pointer; +} + +.pagination button:hover:not(:disabled) { + border-color: #111827; +} + +.pagination button:disabled { + color: #9ca3af; + background-color: #f3f4f6; + cursor: not-allowed; +} + +.pagination span { + min-width: 72px; + color: #4b5563; + font-size: 14px; + text-align: center; +} + +.state-box { + padding: 32px; + border: 1px solid #e5e7eb; + border-radius: 18px; + color: #4b5563; + background-color: #ffffff; + text-align: center; +} + +.state-box.error { + color: #991b1b; + border-color: #fecaca; + background-color: #fef2f2; +} + +/* Responsive layout */ +@media (max-width: 1080px) { + .product-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + @media (max-width: 960px) { .home-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -314,6 +580,16 @@ textarea { } } +@media (max-width: 820px) { + .product-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .product-list-header { + padding: 28px 24px; + } +} + @media (max-width: 640px) { .home-hero-content { padding: 40px 24px; @@ -322,4 +598,29 @@ textarea { .home-grid { grid-template-columns: 1fr; } -} \ No newline at end of file + + .product-list-toolbar { + align-items: flex-start; + flex-direction: column; + } + + .page-size-control { + width: 100%; + justify-content: space-between; + } + + .page-size-control select { + flex: 1; + } +} + +@media (max-width: 560px) { + .product-grid { + grid-template-columns: 1fr; + } + + .product-list-summary { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/apps/web/src/types/catalog.ts b/apps/web/src/types/catalog.ts new file mode 100644 index 0000000..dbb408a --- /dev/null +++ b/apps/web/src/types/catalog.ts @@ -0,0 +1,19 @@ +export type Category = { + category_id: string; + parent_category_id: string | null; + category_name: string; + category_depth: number; + category_status: string; +}; + +export type Product = { + product_id: string; + category_id: string; + product_name: string; + product_status: string; + list_price: string | number; + sale_price: string | number; + currency: string; + brand_name: string | null; + is_active: boolean; +}; \ No newline at end of file From 366c21996e48cfdac2fccb2993bd510008beb4c8 Mon Sep 17 00:00:00 2001 From: jjunier Date: Mon, 11 May 2026 17:05:06 +0900 Subject: [PATCH 7/7] feat(frontend): implement product detail and cart add flow [D2C-34] - connect product detail API and render product detail page - add quantity selector and cart add action from product detail - create cart when stored cart_id is missing and persist cart_id in localStorage - show login guidance, success, and error feedback for cart add flow - add cart API client and cart-related frontend types - add favicon asset and register it in the Vite index page --- apps/web/index.html | 1 + apps/web/public/favicon.svg | 6 + .../features/products/ProductDetailPage.tsx | 224 ++++++++++++++++- apps/web/src/services/cartApi.ts | 25 ++ apps/web/src/services/catalogApi.ts | 6 +- apps/web/src/styles/global.css | 226 ++++++++++++++++++ apps/web/src/types/cart.ts | 18 ++ apps/web/src/types/catalog.ts | 4 +- 8 files changed, 503 insertions(+), 7 deletions(-) create mode 100644 apps/web/public/favicon.svg create mode 100644 apps/web/src/services/cartApi.ts create mode 100644 apps/web/src/types/cart.ts diff --git a/apps/web/index.html b/apps/web/index.html index 97ce28c..4d8d36e 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -3,6 +3,7 @@ + D2C Commerce Prototype diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg new file mode 100644 index 0000000..514bb52 --- /dev/null +++ b/apps/web/public/favicon.svg @@ -0,0 +1,6 @@ + + + + D2C + + \ No newline at end of file diff --git a/apps/web/src/features/products/ProductDetailPage.tsx b/apps/web/src/features/products/ProductDetailPage.tsx index da52b45..e78cc6d 100644 --- a/apps/web/src/features/products/ProductDetailPage.tsx +++ b/apps/web/src/features/products/ProductDetailPage.tsx @@ -1,13 +1,227 @@ -import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { addCartItem, createCart } from "../../services/cartApi"; +import { getProductDetail } from "../../services/catalogApi"; +import { + getStoredCartId, + getStoredUser, + setStoredCartId, +} from "../../stores/userStore"; +import type { ProductDetail } from "../../types/catalog"; + +function formatPrice(value: string | number, currency: string) { + const numericValue = Number(value); + + if (Number.isNaN(numericValue)) { + return `${value} ${currency}`; + } + + return new Intl.NumberFormat("ko-KR", { + style: "currency", + currency, + maximumFractionDigits: 0, + }).format(numericValue); +} export function ProductDetailPage() { const { productId } = useParams(); + const [product, setProduct] = useState(null); + const [quantity, setQuantity] = useState(1); + const [isLoading, setIsLoading] = useState(true); + const [isAddingToCart, setIsAddingToCart] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [cartMessage, setCartMessage] = useState(null); + const [cartErrorMessage, setCartErrorMessage] = useState(null); + + const user = getStoredUser(); + + useEffect(() => { + async function loadProductDetail() { + if (!productId) { + setErrorMessage("상품 식별자를 확인할 수 없습니다."); + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + setErrorMessage(null); + + const productData = await getProductDetail(productId); + setProduct(productData); + } catch { + setErrorMessage("상품 상세 정보를 불러오지 못했습니다."); + } finally { + setIsLoading(false); + } + } + + loadProductDetail(); + }, [productId]); + + const handleDecreaseQuantity = () => { + setQuantity((currentQuantity) => Math.max(1, currentQuantity - 1)); + }; + + const handleIncreaseQuantity = () => { + setQuantity((currentQuantity) => Math.min(99, currentQuantity + 1)); + }; + + const handleQuantityInputChange = (event: React.ChangeEvent) => { + const nextQuantity = Number(event.target.value); + + if (Number.isNaN(nextQuantity)) { + return; + } + + setQuantity(Math.min(99, Math.max(1, nextQuantity))); + }; + + const handleAddToCart = async () => { + if (!productId || !product) { + return; + } + + if (!user) { + setCartMessage(null); + setCartErrorMessage("로그인 후 장바구니에 상품을 담을 수 있습니다."); + return; + } + + try { + setIsAddingToCart(true); + setCartMessage(null); + setCartErrorMessage(null); + + let cartId = getStoredCartId(); + + if (!cartId) { + const createdCart = await createCart({ + user_id: user.user_id, + }); + + cartId = createdCart.cart_id; + setStoredCartId(cartId); + } + + await addCartItem(cartId, { + product_id: product.product_id, + quantity, + }); + + setCartMessage("상품을 장바구니에 담았습니다."); + } catch { + setCartErrorMessage("장바구니 담기에 실패했습니다. 잠시 후 다시 시도해주세요."); + } finally { + setIsAddingToCart(false); + } + }; + + if (isLoading) { + return
상품 상세 정보를 불러오는 중입니다.
; + } + + if (errorMessage || !product) { + return ( +
+
+ {errorMessage ?? "상품 정보를 확인할 수 없습니다."} +
+ + 상품 목록으로 돌아가기 + +
+ ); + } + + const hasDiscount = Number(product.list_price) !== Number(product.sale_price); + return ( -
-

상품 상세

-

D2C-34에서 상품 상세 조회와 장바구니 담기 흐름을 구현합니다.

-

productId: {productId}

+
+ + ← 상품 목록으로 돌아가기 + + +
+
+ {product.brand_name ?? "D2C"} +
+ +
+
+ {product.brand_name ?? "브랜드 미지정"} + {product.product_status} +
+ +

{product.product_name}

+ +
+ {formatPrice(product.sale_price, product.currency)} + {hasDiscount && ( + {formatPrice(product.list_price, product.currency)} + )} +
+ +
+

+ 이 상품은 D2C Commerce Prototype의 상품 탐색 및 장바구니 흐름 검증을 + 위한 샘플 상품입니다. +

+
+ +
+ 수량 +
+ + + +
+
+ + {!user && ( +
+ 로그인 후 장바구니에 상품을 담을 수 있습니다. +
+ + 로그인 + + + 회원가입 + +
+
+ )} + + {cartMessage &&
{cartMessage}
} + {cartErrorMessage &&
{cartErrorMessage}
} + +
+ + + + 장바구니 보기 + +
+
+
); } \ No newline at end of file diff --git a/apps/web/src/services/cartApi.ts b/apps/web/src/services/cartApi.ts new file mode 100644 index 0000000..3896370 --- /dev/null +++ b/apps/web/src/services/cartApi.ts @@ -0,0 +1,25 @@ +import { apiClient } from "./apiClient"; +import type { Cart, CartItem } from "../types/cart"; + +type CreateCartRequest = { + user_id: string; +}; + +type AddCartItemRequest = { + product_id: string; + quantity: number; +}; + +export function createCart(payload: CreateCartRequest) { + return apiClient("/carts", { + method: "POST", + body: payload, + }); +} + +export function addCartItem(cartId: string, payload: AddCartItemRequest) { + return apiClient(`/carts/${cartId}/items`, { + method: "POST", + body: payload, + }); +} \ No newline at end of file diff --git a/apps/web/src/services/catalogApi.ts b/apps/web/src/services/catalogApi.ts index 2b54d0f..290dd4e 100644 --- a/apps/web/src/services/catalogApi.ts +++ b/apps/web/src/services/catalogApi.ts @@ -1,5 +1,5 @@ import { apiClient } from "./apiClient"; -import type { Category, Product } from "../types/catalog"; +import type { Category, Product, ProductDetail } from "../types/catalog"; export function getCategories() { return apiClient("/categories"); @@ -8,4 +8,8 @@ export function getCategories() { export function getProducts(categoryId?: string) { const query = categoryId ? `?category_id=${categoryId}` : ""; return apiClient(`/products${query}`); +} + +export function getProductDetail(productId: string) { + return apiClient(`/products/${productId}`); } \ No newline at end of file diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index 39db273..01cf004 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -558,6 +558,232 @@ textarea { background-color: #fef2f2; } +/* Product Detail Page */ +.product-detail-page { + display: flex; + flex-direction: column; + gap: 20px; +} + +.product-detail-back-link { + width: fit-content; + color: #4b5563; + font-size: 14px; +} + +.product-detail-back-link:hover { + color: #111827; +} + +.product-detail-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(360px, 0.85fr); + gap: 28px; + align-items: stretch; +} + +.product-detail-image { + display: flex; + min-height: 520px; + align-items: center; + justify-content: center; + border: 1px solid #e5e7eb; + border-radius: 24px; + background: + radial-gradient(circle at top right, rgba(17, 24, 39, 0.1), transparent 32%), + #f3f4f6; +} + +.product-detail-image span { + color: #6b7280; + font-size: 16px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.product-detail-info { + display: flex; + flex-direction: column; + gap: 22px; + padding: 32px; + border: 1px solid #e5e7eb; + border-radius: 24px; + background-color: #ffffff; +} + +.product-detail-meta { + display: flex; + justify-content: space-between; + gap: 12px; + color: #6b7280; + font-size: 13px; +} + +.product-detail-info h1 { + margin: 0; + color: #111827; + font-size: clamp(30px, 4vw, 44px); + line-height: 1.18; + letter-spacing: -0.04em; + word-break: keep-all; +} + +.product-detail-price { + display: flex; + align-items: baseline; + gap: 10px; +} + +.product-detail-price strong { + color: #111827; + font-size: 28px; +} + +.product-detail-price span { + color: #9ca3af; + font-size: 15px; + text-decoration: line-through; +} + +.product-detail-description { + padding: 18px; + border-radius: 16px; + background-color: #f9fafb; +} + +.product-detail-description p { + margin: 0; + color: #4b5563; + line-height: 1.7; + word-break: keep-all; +} + +.quantity-control { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.quantity-control > span { + color: #111827; + font-weight: 700; +} + +.quantity-stepper { + display: inline-flex; + overflow: hidden; + border: 1px solid #d1d5db; + border-radius: 12px; + background-color: #ffffff; +} + +.quantity-stepper button { + width: 40px; + border: 0; + color: #111827; + background-color: #ffffff; + cursor: pointer; +} + +.quantity-stepper button:hover { + background-color: #f3f4f6; +} + +.quantity-stepper input { + width: 64px; + border: 0; + border-right: 1px solid #e5e7eb; + border-left: 1px solid #e5e7eb; + text-align: center; +} + +.primary-button { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + padding: 0 18px; + border: 0; + border-radius: 10px; + color: #ffffff; + background-color: #111827; + font-weight: 700; + cursor: pointer; +} + +.primary-button:hover:not(:disabled) { + background-color: #374151; +} + +.primary-button:disabled { + background-color: #9ca3af; + cursor: not-allowed; +} + +.product-detail-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: auto; +} + +.product-detail-notice { + text-align: left; +} + +.product-detail-notice-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 16px; +} + +.state-box.success { + color: #166534; + border-color: #bbf7d0; + background-color: #f0fdf4; +} + +@media (max-width: 900px) { + .product-detail-layout { + grid-template-columns: 1fr; + } + + .product-detail-image { + min-height: 360px; + } +} + +@media (max-width: 560px) { + .product-detail-info { + padding: 24px; + } + + .quantity-control { + align-items: flex-start; + flex-direction: column; + } + + .quantity-stepper { + width: 100%; + } + + .quantity-stepper input { + flex: 1; + } + + .product-detail-actions { + flex-direction: column; + } + + .product-detail-actions .primary-button, + .product-detail-actions .secondary-link { + width: 100%; + } +} + /* Responsive layout */ @media (max-width: 1080px) { .product-grid { diff --git a/apps/web/src/types/cart.ts b/apps/web/src/types/cart.ts new file mode 100644 index 0000000..cd98410 --- /dev/null +++ b/apps/web/src/types/cart.ts @@ -0,0 +1,18 @@ +export type Cart = { + cart_id: string; + user_id: string; + cart_status: string; + total_items?: number; + total_quantity?: number; + total_amount?: string | number; + currency?: string; +}; + +export type CartItem = { + cart_item_id: string; + cart_id: string; + product_id: string; + quantity: number; + unit_price: string | number; + currency: string; +}; \ No newline at end of file diff --git a/apps/web/src/types/catalog.ts b/apps/web/src/types/catalog.ts index dbb408a..0273083 100644 --- a/apps/web/src/types/catalog.ts +++ b/apps/web/src/types/catalog.ts @@ -16,4 +16,6 @@ export type Product = { currency: string; brand_name: string | null; is_active: boolean; -}; \ No newline at end of file +}; + +export type ProductDetail = Product; \ No newline at end of file