diff --git a/README.md b/README.md index 1b311c9..d6630d6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ ## Multi-Step-Form 기능 요구사항 +#### [온보딩 Swagger API](https://onboarding-server-idpj.onrender.com/docs) + +### 시연영상 + +

+ example-form +

+ ### 1. 온보딩 등록 폼 공통 요구사항 - [ ] 4단계로 구성된 프로필 등록 폼 구현 (닉네임 → 성별 → 선호 장르 → 인생 영화 혹은 드라마) @@ -27,12 +35,16 @@ - [ ] 최소 1개 이상 선택해야 다음 단계로 이동 가능 - [ ] 선택된 장르는 시각적으로 구분되어야 함 -#### 4단계: 인생 영화 입력 +#### 4단계: 좋아하는 작품 입력 - [ ] 자유 텍스트 입력 필드 구현 - [ ] 입력값이 있어야 완료 버튼 활성화 - [ ] 완료 시 전체 입력 데이터를 확인할 수 있어야 함 +#### 5단계: 온보딩 결과 페이지 + +- [ ] 1 ~ 4 단계에서 사용자가 입력한 정보 데이터를 최종적으로 규합하여 확인할 수 있어야 함 + ### 3.기술적 요구사항 포인트 - [ ] 단계별 컴포넌트 분리 및 재사용 가능한 구조 diff --git a/eslint.config.js b/eslint.config.js index 092408a..c8d0fb4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,8 +1,8 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; export default tseslint.config( { ignores: ['dist'] }, @@ -19,10 +19,7 @@ export default tseslint.config( }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + 'react-refresh/only-export-components': 'off', }, - }, -) + } +); diff --git a/example-form.gif b/example-form.gif new file mode 100644 index 0000000..ffe22a5 Binary files /dev/null and b/example-form.gif differ diff --git a/package.json b/package.json index 8368353..c99ef6a 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,25 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^5.0.1", + "@tailwindcss/vite": "^4.1.8", + "@tanstack/react-query": "^5.80.2", + "axios": "^1.9.0", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-hook-form": "^7.57.0", + "react-router-dom": "^7.6.2", + "tailwindcss": "^4.1.8", + "zod": "^3.25.50" }, "devDependencies": { "@eslint/js": "^9.25.0", + "@hookform/devtools": "^4.4.0", + "@tanstack/react-query-devtools": "^5.80.2", + "@types/node": "^22.15.29", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", + "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^4.4.1", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f55bd8..97655e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,34 +8,70 @@ importers: .: dependencies: + '@hookform/resolvers': + specifier: ^5.0.1 + version: 5.0.1(react-hook-form@7.57.0(react@19.1.0)) + '@tailwindcss/vite': + specifier: ^4.1.8 + version: 4.1.8(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)) + '@tanstack/react-query': + specifier: ^5.80.2 + version: 5.80.2(react@19.1.0) + axios: + specifier: ^1.9.0 + version: 1.9.0 react: specifier: ^19.1.0 version: 19.1.0 react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.57.0 + version: 7.57.0(react@19.1.0) + react-router-dom: + specifier: ^7.6.2 + version: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + tailwindcss: + specifier: ^4.1.8 + version: 4.1.8 + zod: + specifier: ^3.25.50 + version: 3.25.50 devDependencies: '@eslint/js': specifier: ^9.25.0 version: 9.26.0 + '@hookform/devtools': + specifier: ^4.4.0 + version: 4.4.0(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-query-devtools': + specifier: ^5.80.2 + version: 5.80.2(@tanstack/react-query@5.80.2(react@19.1.0))(react@19.1.0) + '@types/node': + specifier: ^22.15.29 + version: 22.15.29 '@types/react': specifier: ^19.1.2 version: 19.1.4 '@types/react-dom': specifier: ^19.1.2 version: 19.1.4(@types/react@19.1.4) + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 '@vitejs/plugin-react': specifier: ^4.4.1 - version: 4.4.1(vite@6.3.5) + version: 4.4.1(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)) eslint: specifier: ^9.25.0 - version: 9.26.0 + version: 9.26.0(jiti@2.4.2) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.26.0) + version: 5.2.0(eslint@9.26.0(jiti@2.4.2)) eslint-plugin-react-refresh: specifier: ^0.4.19 - version: 0.4.20(eslint@9.26.0) + version: 0.4.20(eslint@9.26.0(jiti@2.4.2)) globals: specifier: ^16.0.0 version: 16.1.0 @@ -44,10 +80,10 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.30.1 - version: 8.32.1(eslint@9.26.0)(typescript@5.8.3) + version: 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5 + version: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1) packages: @@ -122,6 +158,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.27.4': + resolution: {integrity: sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -134,6 +174,60 @@ packages: resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} engines: {node: '>=6.9.0'} + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.3.1': + resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.0': + resolution: {integrity: sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@esbuild/aix-ppc64@0.25.4': resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} engines: {node: '>=18'} @@ -322,6 +416,17 @@ packages: resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hookform/devtools@4.4.0': + resolution: {integrity: sha512-Mtlic+uigoYBPXlfvPBfiYYUZuyMrD3pTjDpVIhL6eCZTvQkHsKBSKeZCvXWUZr8fqrkzDg27N+ZuazLKq6Vmg==} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-dom: ^16.8.0 || ^17 || ^18 || ^19 + + '@hookform/resolvers@5.0.1': + resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -342,6 +447,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -476,6 +585,116 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@tailwindcss/node@4.1.8': + resolution: {integrity: sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==} + + '@tailwindcss/oxide-android-arm64@4.1.8': + resolution: {integrity: sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.8': + resolution: {integrity: sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.8': + resolution: {integrity: sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.8': + resolution: {integrity: sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': + resolution: {integrity: sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': + resolution: {integrity: sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.8': + resolution: {integrity: sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.8': + resolution: {integrity: sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.8': + resolution: {integrity: sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.8': + resolution: {integrity: sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': + resolution: {integrity: sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.8': + resolution: {integrity: sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.8': + resolution: {integrity: sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.8': + resolution: {integrity: sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A==} + peerDependencies: + vite: ^5.2.0 || ^6 + + '@tanstack/query-core@5.80.2': + resolution: {integrity: sha512-g2Es97uwFk7omkWiH9JmtLWSA8lTUFVseIyzqbjqJEEx7qN+Hg6jbBdDvelqtakamppaJtGORQ64hEJ5S6ojSg==} + + '@tanstack/query-devtools@5.80.0': + resolution: {integrity: sha512-D6gH4asyjaoXrCOt5vG5Og/YSj0D/TxwNQgtLJIgWbhbWCC/emu2E92EFoVHh4ppVWg1qT2gKHvKyQBEFZhCuA==} + + '@tanstack/react-query-devtools@5.80.2': + resolution: {integrity: sha512-nGeCXBkSoYHKkQQO6pBr+DE8ML/EGd79MvFCkBPRcAA8u65OMNOJPXB0sg2AGgXX2+enTEQLHWZwjzukNB9gIA==} + peerDependencies: + '@tanstack/react-query': ^5.80.2 + react: ^18 || ^19 + + '@tanstack/react-query@5.80.2': + resolution: {integrity: sha512-LfA0SVheJBOqC8RfJw/JbOW3yh2zuONQeWU5Prjm7yjUGUONeOedky1Bj39Cfj8MRdXrZV+DxNT7/DN/M907lQ==} + peerDependencies: + react: ^18 || ^19 + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -491,14 +710,32 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash@4.17.17': + resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==} + + '@types/node@22.15.29': + resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/react-dom@19.1.4': resolution: {integrity: sha512-WxYAszDYgsMV31OVyoG4jbAgJI1Gw0Xq9V19zwhy6+hUUJlJIdZ3r/cbdmTqFv++SktQkZ/X+46yGFxp5XJBEg==} peerDependencies: '@types/react': ^19.0.0 + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + '@types/react@19.1.4': resolution: {integrity: sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==} @@ -579,6 +816,16 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -624,6 +871,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -631,6 +882,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -642,6 +897,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -653,10 +911,18 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -676,10 +942,22 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -694,6 +972,13 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -706,6 +991,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.25.4: resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} engines: {node: '>=18'} @@ -833,6 +1122,9 @@ packages: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -844,6 +1136,19 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -896,6 +1201,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -907,10 +1215,17 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -942,6 +1257,13 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -960,6 +1282,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -975,6 +1301,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -993,6 +1322,78 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + little-state-machine@4.8.1: + resolution: {integrity: sha512-liPHqaWMQ7rzZryQUDnbZ1Gclnnai3dIyaJ0nAgwZRXMzqbYrydrlCI0NDojRUbE5VYh5vu6hygEUZiH77nQkQ==} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1000,9 +1401,15 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1023,10 +1430,18 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.1: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} @@ -1038,6 +1453,19 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1087,6 +1515,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -1099,10 +1531,17 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1130,6 +1569,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1154,10 +1596,41 @@ packages: peerDependencies: react: ^19.1.0 + react-hook-form@7.57.0: + resolution: {integrity: sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router-dom@7.6.2: + resolution: {integrity: sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.6.2: + resolution: {integrity: sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-simple-animate@3.5.3: + resolution: {integrity: sha512-Ob+SmB5J1tXDEZyOe2Hf950K4M8VaWBBmQ3cS2BUnTORqHjhK0iKG8fB+bo47ZL15t8d3g/Y0roiqH05UBjG7A==} + peerDependencies: + react-dom: ^16.8.0 || ^17 || ^18 || ^19 + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -1166,6 +1639,11 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1208,6 +1686,9 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -1239,6 +1720,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -1247,10 +1732,28 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@4.1.8: + resolution: {integrity: sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + tinyglobby@0.2.13: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} @@ -1289,6 +1792,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -1302,6 +1808,16 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-deep-compare-effect@1.8.1: + resolution: {integrity: sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13' + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -1361,6 +1877,14 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1370,8 +1894,8 @@ packages: peerDependencies: zod: ^3.24.1 - zod@3.24.4: - resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} + zod@3.25.50: + resolution: {integrity: sha512-VstOnRxf4tlSq0raIwbn0n+LA34SxVoZ8r3pkwSUM0jqNiA/HCMQEVjTuS5FZmHsge+9MDGTiAuHyml5T0um6A==} snapshots: @@ -1467,6 +1991,8 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/runtime@7.27.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -1490,6 +2016,89 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.27.4 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.3.1': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.4 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.3.1 + '@emotion/react': 11.14.0(@types/react@19.1.4)(react@19.1.0) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@emotion/utils': 1.4.2 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.4 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + '@esbuild/aix-ppc64@0.25.4': optional: true @@ -1565,9 +2174,9 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.26.0)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.26.0(jiti@2.4.2))': dependencies: - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -1609,6 +2218,27 @@ snapshots: '@eslint/core': 0.13.0 levn: 0.4.1 + '@hookform/devtools@4.4.0(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@emotion/react': 11.14.0(@types/react@19.1.4)(react@19.1.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0) + '@types/lodash': 4.17.17 + little-state-machine: 4.8.1(react@19.1.0) + lodash: 4.17.21 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-simple-animate: 3.5.3(react-dom@19.1.0(react@19.1.0)) + use-deep-compare-effect: 1.8.1(react@19.1.0) + uuid: 8.3.2 + transitivePeerDependencies: + - '@types/react' + - supports-color + + '@hookform/resolvers@5.0.1(react-hook-form@7.57.0(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.57.0(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -1622,6 +2252,10 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -1649,8 +2283,8 @@ snapshots: express-rate-limit: 7.5.0(express@5.1.0) pkce-challenge: 5.0.0 raw-body: 3.0.0 - zod: 3.24.4 - zod-to-json-schema: 3.24.5(zod@3.24.4) + zod: 3.25.50 + zod-to-json-schema: 3.24.5(zod@3.25.50) transitivePeerDependencies: - supports-color @@ -1726,6 +2360,94 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true + '@standard-schema/utils@0.3.0': {} + + '@tailwindcss/node@4.1.8': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.8 + + '@tailwindcss/oxide-android-arm64@4.1.8': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.8': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.8': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.8': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.8': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.8': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.8': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.8': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.8': + optional: true + + '@tailwindcss/oxide@4.1.8': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.8 + '@tailwindcss/oxide-darwin-arm64': 4.1.8 + '@tailwindcss/oxide-darwin-x64': 4.1.8 + '@tailwindcss/oxide-freebsd-x64': 4.1.8 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.8 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.8 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.8 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.8 + '@tailwindcss/oxide-linux-x64-musl': 4.1.8 + '@tailwindcss/oxide-wasm32-wasi': 4.1.8 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.8 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.8 + + '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1))': + dependencies: + '@tailwindcss/node': 4.1.8 + '@tailwindcss/oxide': 4.1.8 + tailwindcss: 4.1.8 + vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1) + + '@tanstack/query-core@5.80.2': {} + + '@tanstack/query-devtools@5.80.0': {} + + '@tanstack/react-query-devtools@5.80.2(@tanstack/react-query@5.80.2(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/query-devtools': 5.80.0 + '@tanstack/react-query': 5.80.2(react@19.1.0) + react: 19.1.0 + + '@tanstack/react-query@5.80.2(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.80.2 + react: 19.1.0 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.27.2 @@ -1749,25 +2471,46 @@ snapshots: '@types/estree@1.0.7': {} + '@types/history@4.7.11': {} + '@types/json-schema@7.0.15': {} + '@types/lodash@4.17.17': {} + + '@types/node@22.15.29': + dependencies: + undici-types: 6.21.0 + + '@types/parse-json@4.0.2': {} + '@types/react-dom@19.1.4(@types/react@19.1.4)': dependencies: '@types/react': 19.1.4 + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.1.4 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.1.4 + '@types/react@19.1.4': dependencies: csstype: 3.1.3 - '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.32.1(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.32.1 - '@typescript-eslint/type-utils': 8.32.1(eslint@9.26.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.1 - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.4 natural-compare: 1.4.0 @@ -1776,14 +2519,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.32.1(eslint@9.26.0)(typescript@5.8.3)': + '@typescript-eslint/parser@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.32.1 '@typescript-eslint/types': 8.32.1 '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.1 debug: 4.4.0 - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -1793,12 +2536,12 @@ snapshots: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/visitor-keys': 8.32.1 - '@typescript-eslint/type-utils@8.32.1(eslint@9.26.0)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.0 - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -1820,13 +2563,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.32.1(eslint@9.26.0)(typescript@5.8.3)': + '@typescript-eslint/utils@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.32.1 '@typescript-eslint/types': 8.32.1 '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -1836,14 +2579,14 @@ snapshots: '@typescript-eslint/types': 8.32.1 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react@4.4.1(vite@6.3.5)': + '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@babel/core': 7.27.1 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5 + vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1) transitivePeerDependencies: - supports-color @@ -1871,6 +2614,22 @@ snapshots: argparse@2.0.1: {} + asynckit@0.4.0: {} + + axios@1.9.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.27.4 + cosmiconfig: 7.1.0 + resolve: 1.22.10 + balanced-match@1.0.2: {} body-parser@2.2.0: @@ -1928,12 +2687,18 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chownr@3.0.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} content-disposition@1.0.0: @@ -1942,17 +2707,29 @@ snapshots: content-type@1.0.5: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} cookie-signature@1.2.2: {} cookie@0.7.2: {} + cookie@1.0.2: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 vary: 1.1.2 + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1967,8 +2744,14 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + depd@2.0.0: {} + dequal@2.0.3: {} + + detect-libc@2.0.4: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1981,6 +2764,15 @@ snapshots: encodeurl@2.0.0: {} + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -1989,6 +2781,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.25.4: optionalDependencies: '@esbuild/aix-ppc64': 0.25.4 @@ -2023,13 +2822,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.26.0): + eslint-plugin-react-hooks@5.2.0(eslint@9.26.0(jiti@2.4.2)): dependencies: - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.4.2) - eslint-plugin-react-refresh@0.4.20(eslint@9.26.0): + eslint-plugin-react-refresh@0.4.20(eslint@9.26.0(jiti@2.4.2)): dependencies: - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.4.2) eslint-scope@8.3.0: dependencies: @@ -2040,9 +2839,9 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.26.0: + eslint@9.26.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.0 '@eslint/config-helpers': 0.2.2 @@ -2078,7 +2877,9 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - zod: 3.24.4 + zod: 3.25.50 + optionalDependencies: + jiti: 2.4.2 transitivePeerDependencies: - supports-color @@ -2185,6 +2986,8 @@ snapshots: transitivePeerDependencies: - supports-color + find-root@1.1.0: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -2197,6 +3000,15 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -2242,16 +3054,26 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} has-flag@4.0.0: {} has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -2279,6 +3101,12 @@ snapshots: ipaddr.js@1.9.1: {} + is-arrayish@0.2.1: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -2291,6 +3119,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.4.2: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -2301,6 +3131,8 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -2316,16 +3148,73 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lines-and-columns@1.2.4: {} + + little-state-machine@4.8.1(react@19.1.0): + dependencies: + react: 19.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 lodash.merge@4.6.2: {} + lodash@4.17.21: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -2339,8 +3228,14 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime-types@3.0.1: dependencies: mime-db: 1.54.0 @@ -2353,6 +3248,14 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -2396,14 +3299,25 @@ snapshots: dependencies: callsites: 3.1.0 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parseurl@1.3.3: {} path-exists@4.0.0: {} path-key@3.1.1: {} + path-parse@1.0.7: {} + path-to-regexp@8.2.0: {} + path-type@4.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -2425,6 +3339,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} qs@6.14.0: @@ -2447,12 +3363,42 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-hook-form@7.57.0(react@19.1.0): + dependencies: + react: 19.1.0 + + react-is@16.13.1: {} + react-refresh@0.17.0: {} + react-router-dom@7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-router: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + + react-router@7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + cookie: 1.0.2 + react: 19.1.0 + set-cookie-parser: 2.7.1 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + + react-simple-animate@3.5.3(react-dom@19.1.0(react@19.1.0)): + dependencies: + react-dom: 19.1.0(react@19.1.0) + react@19.1.0: {} resolve-from@4.0.0: {} + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} rollup@4.40.2: @@ -2530,6 +3476,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.1: {} + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -2568,14 +3516,33 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.5.7: {} + statuses@2.0.1: {} strip-json-comments@3.1.1: {} + stylis@4.2.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@4.1.8: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + tinyglobby@0.2.13: dependencies: fdir: 6.4.4(picomatch@4.0.2) @@ -2601,18 +3568,20 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 - typescript-eslint@8.32.1(eslint@9.26.0)(typescript@5.8.3): + typescript-eslint@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3) - '@typescript-eslint/parser': 8.32.1(eslint@9.26.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.26.0)(typescript@5.8.3) - eslint: 9.26.0 + '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.26.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color typescript@5.8.3: {} + undici-types@6.21.0: {} + unpipe@1.0.0: {} update-browserslist-db@1.1.3(browserslist@4.24.5): @@ -2625,9 +3594,17 @@ snapshots: dependencies: punycode: 2.3.1 + use-deep-compare-effect@1.8.1(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.4 + dequal: 2.0.3 + react: 19.1.0 + + uuid@8.3.2: {} + vary@1.1.2: {} - vite@6.3.5: + vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) @@ -2636,7 +3613,10 @@ snapshots: rollup: 4.40.2 tinyglobby: 0.2.13 optionalDependencies: + '@types/node': 22.15.29 fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 which@2.0.2: dependencies: @@ -2648,10 +3628,14 @@ snapshots: yallist@3.1.1: {} + yallist@5.0.0: {} + + yaml@1.10.2: {} + yocto-queue@0.1.0: {} - zod-to-json-schema@3.24.5(zod@3.24.4): + zod-to-json-schema@3.24.5(zod@3.25.50): dependencies: - zod: 3.24.4 + zod: 3.25.50 - zod@3.24.4: {} + zod@3.25.50: {} diff --git a/src/App.tsx b/src/App.tsx index 56325b8..10d1f55 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ +import Router from './routers/Router'; + function App() { return ( - <> -

multi-step-form

- +
+ +
); } diff --git a/src/features/funnel/Funnel.tsx b/src/features/funnel/Funnel.tsx new file mode 100644 index 0000000..605ef5e --- /dev/null +++ b/src/features/funnel/Funnel.tsx @@ -0,0 +1,18 @@ +import { createContext } from 'react'; +import React from 'react'; +import { Step } from './Step'; + +export const FunnelContext = createContext<{ step?: string }>({}); + +interface FunnelProps { + children: React.ReactNode; + step: string; +} + +function Funnel({ children, step }: FunnelProps) { + return ( + {children} + ); +} + +export default Object.assign(Funnel, { Step }); diff --git a/src/features/funnel/Step.tsx b/src/features/funnel/Step.tsx new file mode 100644 index 0000000..6f5dd41 --- /dev/null +++ b/src/features/funnel/Step.tsx @@ -0,0 +1,15 @@ +import { useContext } from 'react'; +import { FunnelContext } from './Funnel'; + +interface StepProps { + children: React.ReactNode; + name: string; +} + +export function Step({ children, name }: StepProps) { + const context = useContext(FunnelContext); + if (context?.step === name) { + return <>{children}; + } + return null; +} diff --git a/src/features/funnel/use-funnel.ts b/src/features/funnel/use-funnel.ts new file mode 100644 index 0000000..ef5088b --- /dev/null +++ b/src/features/funnel/use-funnel.ts @@ -0,0 +1,78 @@ +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { useEffect } from 'react'; + +interface UseFunnelProps { + steps: T; +} + +interface UseFunnelReturn { + currentStep: T[number]; + goToStep: (step: T[number]) => void; + goToNextStep: () => void; + goToPrevStep: () => void; + stepIndex: number; + totalSteps: number; +} + +export function useFunnel({ + steps, +}: UseFunnelProps): UseFunnelReturn { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + // step 파라미터에 없는 값일 경우 첫 번째 스텝으로 초기 url 설정 + useEffect(() => { + const urlStep = searchParams.get('step'); + if (!urlStep) { + const newParams = new URLSearchParams(searchParams); + newParams.set('step', steps[0]); + navigate(`?${newParams.toString()}`, { replace: true }); + } + }, [searchParams, steps, navigate]); + + // 현재 스텝 가져오기 + const getCurrentStep = (): T[number] => { + const urlStep = searchParams.get('step'); + return urlStep && steps.includes(urlStep) ? urlStep : steps[0]; + }; + + const currentStep = getCurrentStep(); + const stepIndex = steps.indexOf(currentStep); + const totalSteps = steps.length; + + // step url 업데이트 함수 + const updateUrl = (step: string) => { + const newParams = new URLSearchParams(searchParams); + newParams.set('step', step); + navigate(`?${newParams.toString()}`, { replace: false }); + }; + + const goToStep = (step: T[number]) => { + if (steps.includes(step)) { + updateUrl(step); + } + }; + + const goToNextStep = () => { + const canGoNext = stepIndex < totalSteps - 1; + if (canGoNext) { + goToStep(steps[stepIndex + 1]); + } + }; + + const goToPrevStep = () => { + const canGoPrev = stepIndex > 0; + if (canGoPrev) { + goToStep(steps[stepIndex - 1]); + } + }; + + return { + currentStep, + goToStep, + goToNextStep, + goToPrevStep, + stepIndex, + totalSteps, + }; +} diff --git a/src/features/onboarding/components/FavoriteStep.tsx b/src/features/onboarding/components/FavoriteStep.tsx new file mode 100644 index 0000000..437cdb7 --- /dev/null +++ b/src/features/onboarding/components/FavoriteStep.tsx @@ -0,0 +1,84 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + FavoriteStepSchema, + type FavoriteFormData, + type OnboardingData, +} from '../onboarding.type'; +import Button from '@/shared/components/Button'; +import Header from '@/shared/components/Header'; +import Input from '@/shared/components/Input'; + +interface FavoriteStepProps { + data: OnboardingData; + onPrev: () => void; + onComplete: (finalData: OnboardingData) => void; + isSubmitting?: boolean; +} + +export default function FavoriteStep({ + data, + onPrev, + onComplete, + isSubmitting = false, +}: FavoriteStepProps) { + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(FavoriteStepSchema), + defaultValues: { + favorite_movie: data.favorite_movie || '', + }, + mode: 'onChange', + }); + + const onSubmit = async (formData: FavoriteFormData) => { + const finalData: OnboardingData = { + nickname: data.nickname, + gender: data.gender, + genre: data.genre, + favorite_movie: formData.favorite_movie, + }; + + // OnboardingPage의 handleFinalSubmit로 최종 데이터 전달 + onComplete(finalData); + }; + + return ( +
+
좋아하는 작품을 알려주세요
+
+
+ +
+ +
+ + +
+
+
+ ); +} diff --git a/src/features/onboarding/components/GenderStep.tsx b/src/features/onboarding/components/GenderStep.tsx new file mode 100644 index 0000000..824e840 --- /dev/null +++ b/src/features/onboarding/components/GenderStep.tsx @@ -0,0 +1,97 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + GenderStepSchema, + type GenderFormData, + type OnboardingData, +} from '../onboarding.type'; +import Button from '@/shared/components/Button'; +import Header from '@/shared/components/Header'; +import ErrorMessage from '@/shared/components/ErrorMessage'; + +interface GenderStepProps { + data: OnboardingData; + onNext: (data: Partial) => void; + onPrev: () => void; +} + +export default function GenderStep({ data, onNext, onPrev }: GenderStepProps) { + const { + register, + handleSubmit, + formState: { isValid, errors }, + watch, + } = useForm({ + resolver: zodResolver(GenderStepSchema), + defaultValues: { + gender: data.gender, + }, + mode: 'onChange', + }); + + const selectedGender = watch('gender'); + + const onSubmit = (formData: GenderFormData) => { + onNext({ gender: formData.gender }); + }; + + return ( +
+
성별을 선택해주세요
+ +
+
+
+ + + +
+ {errors.gender && ( + + )} +
+ +
+ + +
+
+
+ ); +} diff --git a/src/features/onboarding/components/GenresStep.tsx b/src/features/onboarding/components/GenresStep.tsx new file mode 100644 index 0000000..9b64b76 --- /dev/null +++ b/src/features/onboarding/components/GenresStep.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + GenresStepSchema, + type GenresFormData, + type OnboardingData, +} from '../onboarding.type'; +import { AVAILABLE_GENRES } from '../onboarding.constant'; +import Button from '@/shared/components/Button'; +import Header from '@/shared/components/Header'; +import ErrorMessage from '@/shared/components/ErrorMessage'; + +interface GenresStepProps { + data: OnboardingData; + onNext: (data: Partial) => void; + onPrev: () => void; +} + +export default function GenresStep({ data, onNext, onPrev }: GenresStepProps) { + const [selectedGenres, setSelectedGenres] = useState( + data.genre || [] + ); + + const { + handleSubmit, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(GenresStepSchema), + defaultValues: { + genre: data.genre || [], + }, + mode: 'onChange', + }); + + // 폼 초기화 시 유효성 검사 실행 + React.useEffect(() => { + setValue('genre', selectedGenres, { shouldValidate: true }); + }, [selectedGenres, setValue]); + + const toggleGenre = (genre: string) => { + setSelectedGenres((prev) => { + if (prev.includes(genre)) { + return prev.filter((g) => g !== genre); + } else { + return [...prev, genre]; + } + }); + }; + + const onSubmit = (formData: GenresFormData) => { + onNext({ genre: formData.genre }); + }; + + return ( +
+
관심 장르를 선택해주세요
+

1개 이상 5개 이하로 선택해주세요

+ +
+
+
+ {AVAILABLE_GENRES.map((genre) => ( + + ))} +
+ {errors.genre && ( + + )} +
+ +
+ + +
+
+
+ ); +} diff --git a/src/features/onboarding/components/NicknameStep.tsx b/src/features/onboarding/components/NicknameStep.tsx new file mode 100644 index 0000000..7b24857 --- /dev/null +++ b/src/features/onboarding/components/NicknameStep.tsx @@ -0,0 +1,61 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + NicknameStepSchema, + type NicknameFormData, + type OnboardingData, +} from '../onboarding.type'; +import Button from '@/shared/components/Button'; +import Header from '@/shared/components/Header'; +import Input from '@/shared/components/Input'; + +interface NicknameStepProps { + data: OnboardingData; + onNext: (data: Partial) => void; +} + +export default function NicknameStep({ data, onNext }: NicknameStepProps) { + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(NicknameStepSchema), + defaultValues: { + nickname: data.nickname || '', + }, + mode: 'onChange', + }); + + const onSubmit = (formData: NicknameFormData) => { + onNext({ nickname: formData.nickname }); + }; + + return ( +
+
닉네임을 입력해주세요
+ +
+
+ +
+ +
+ +
+
+
+ ); +} diff --git a/src/features/onboarding/hooks/mutations/use-post-onboarding-data.ts b/src/features/onboarding/hooks/mutations/use-post-onboarding-data.ts new file mode 100644 index 0000000..9273c9d --- /dev/null +++ b/src/features/onboarding/hooks/mutations/use-post-onboarding-data.ts @@ -0,0 +1,24 @@ +import { postOnboardingData } from '@/features/onboarding/onboarding.api'; +import { + OnboardingDataSchema, + type OnboardingData, + type OnboardingUserResponse, +} from '@/features/onboarding/onboarding.type'; +import { useMutation } from '@tanstack/react-query'; + +export const usePostOnboardingData = () => { + return useMutation({ + mutationFn: async (data: OnboardingData) => { + const result = OnboardingDataSchema.safeParse(data); + + if (!result.success) { + const errorResult = result.error.issues[0]; + const message = + errorResult?.message || '입력 데이터가 올바르지 않습니다.'; + throw new Error(message); + } + + return await postOnboardingData(result.data); + }, + }); +}; diff --git a/src/features/onboarding/hooks/queries/use-get-onboarding-user-data.ts b/src/features/onboarding/hooks/queries/use-get-onboarding-user-data.ts new file mode 100644 index 0000000..6b5084a --- /dev/null +++ b/src/features/onboarding/hooks/queries/use-get-onboarding-user-data.ts @@ -0,0 +1,13 @@ +import type { OnboardingUserResponse } from '../../onboarding.type'; +import { getOnboardingUserById } from '../../onboarding.api'; +import { useQuery } from '@tanstack/react-query'; +import { DEFAULT_STALE_TIME } from '@/shared/constants'; + +export const useGetOnboardingUserData = (userId: number) => { + return useQuery({ + queryKey: ['user', userId], + queryFn: () => getOnboardingUserById(userId), + enabled: !!userId, + staleTime: DEFAULT_STALE_TIME, + }); +}; diff --git a/src/features/onboarding/onboarding.api.ts b/src/features/onboarding/onboarding.api.ts new file mode 100644 index 0000000..e9ca50c --- /dev/null +++ b/src/features/onboarding/onboarding.api.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; +import type { OnboardingData, OnboardingUserResponse } from './onboarding.type'; + +const axiosInstance = axios.create({ + baseURL: 'https://onboarding-server-idpj.onrender.com/api', +}); + +export const postOnboardingData = async ( + data: OnboardingData +): Promise => { + const response = await axiosInstance.post( + '/users/onboarding', + data + ); + + return response.data; +}; + +export const getOnboardingUserById = async ( + userId: number +): Promise => { + const response = await axiosInstance.get( + `/users/${userId}` + ); + + return response.data; +}; diff --git a/src/features/onboarding/onboarding.constant.ts b/src/features/onboarding/onboarding.constant.ts new file mode 100644 index 0000000..2cc9e31 --- /dev/null +++ b/src/features/onboarding/onboarding.constant.ts @@ -0,0 +1,33 @@ +export const ONBOARDING_ERROR_MESSAGES = { + NICKNAME: { + MIN: '닉네임은 최소 2자 이상이어야 합니다', + MAX: '닉네임은 최대 10자까지 가능합니다', + PATTERN: '닉네임은 한글, 영문, 숫자만 가능합니다', + }, + GENRE: { + MIN: '최소 1개 이상의 장르를 선택해주세요', + MAX: '최대 5개까지 선택 가능합니다', + }, + FAVORITE: { + REQUIRED: '작품 제목을 입력해주세요', + MAX: '작품 제목은 최대 50자까지 가능합니다', + }, + GENDER: { + REQUIRED: '성별을 선택해주세요', + }, +} as const; + +export const STEPS = ['nickname', 'gender', 'genres', 'favorite'] as const; + +export const AVAILABLE_GENRES = [ + '액션', + '코미디', + '로맨스', + '스릴러', + '공포', + 'SF', + '판타지', + '드라마', + '애니메이션', + '다큐멘터리', +] as const; diff --git a/src/features/onboarding/onboarding.type.ts b/src/features/onboarding/onboarding.type.ts new file mode 100644 index 0000000..ce59849 --- /dev/null +++ b/src/features/onboarding/onboarding.type.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { ONBOARDING_ERROR_MESSAGES } from './onboarding.constant'; + +export type OnboardingStep = 'nickname' | 'gender' | 'genres' | 'favorite'; + +export const GenderSchema = z.enum(['남성', '여성']); +export type Gender = z.infer; + +export const NicknameStepSchema = z.object({ + nickname: z + .string() + .min(2, ONBOARDING_ERROR_MESSAGES.NICKNAME.MIN) + .max(10, ONBOARDING_ERROR_MESSAGES.NICKNAME.MAX) + .regex(/^[가-힣a-zA-Z0-9]+$/, ONBOARDING_ERROR_MESSAGES.NICKNAME.PATTERN), +}); + +export const GenderStepSchema = z.object({ + gender: GenderSchema, +}); + +export const GenresStepSchema = z.object({ + genre: z + .array(z.string()) + .min(1, ONBOARDING_ERROR_MESSAGES.GENRE.MIN) + .max(5, ONBOARDING_ERROR_MESSAGES.GENRE.MAX), +}); + +export const FavoriteStepSchema = z.object({ + favorite_movie: z + .string() + .min(1, ONBOARDING_ERROR_MESSAGES.FAVORITE.REQUIRED) + .max(50, ONBOARDING_ERROR_MESSAGES.FAVORITE.MAX), +}); + +export const OnboardingDataSchema = z.object({ + nickname: NicknameStepSchema.shape.nickname, + gender: GenderStepSchema.shape.gender, + genre: GenresStepSchema.shape.genre, + favorite_movie: FavoriteStepSchema.shape.favorite_movie, +}); + +export const OnboardingUserResponseSchema = OnboardingDataSchema.extend({ + id: z.number(), +}); + +export type OnboardingData = z.infer; +export type OnboardingUserResponse = z.infer< + typeof OnboardingUserResponseSchema +>; +export type NicknameFormData = z.infer; +export type GenderFormData = z.infer; +export type GenresFormData = z.infer; +export type FavoriteFormData = z.infer; diff --git a/src/index.css b/src/index.css index e69de29..fcf5a11 100644 --- a/src/index.css +++ b/src/index.css @@ -0,0 +1,50 @@ +@import 'tailwindcss'; + +@layer base { + body { + @apply bg-gray-50 text-gray-900 min-h-screen; + } + + #root { + @apply max-w-5xl mx-auto px-8 text-center; + } +} + +@layer components { + .btn { + @apply px-4 py-2 rounded-md font-medium transition-colors; + } + + .btn-primary { + @apply bg-blue-500 text-white hover:bg-blue-600; + } + + .btn-secondary { + @apply bg-gray-200 text-gray-800 hover:bg-gray-300; + } + + .btn-disabled { + @apply bg-gray-300 text-gray-500 cursor-not-allowed; + } + + .input-field { + @apply w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent; + } +} + +@layer utilities { + @keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + @media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } + } +} diff --git a/src/main.tsx b/src/main.tsx index bef5202..1219e76 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,25 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import './index.css'; +import App from './App.tsx'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); createRoot(document.getElementById('root')!).render( - - , -) + + + + + + +); diff --git a/src/pages/OnboardingPage.tsx b/src/pages/OnboardingPage.tsx new file mode 100644 index 0000000..525f1de --- /dev/null +++ b/src/pages/OnboardingPage.tsx @@ -0,0 +1,93 @@ +import Funnel from '@/features/funnel/Funnel'; +import { useFunnel } from '@/features/funnel/use-funnel'; +import FavoriteStep from '@/features/onboarding/components/FavoriteStep'; +import GenderStep from '@/features/onboarding/components/GenderStep'; +import GenresStep from '@/features/onboarding/components/GenresStep'; +import NicknameStep from '@/features/onboarding/components/NicknameStep'; +import { usePostOnboardingData } from '@/features/onboarding/hooks/mutations/use-post-onboarding-data'; +import { STEPS } from '@/features/onboarding/onboarding.constant'; +import type { + OnboardingData, + OnboardingUserResponse, +} from '@/features/onboarding/onboarding.type'; +import ProgressBar from '@/shared/components/ProgressBar'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export default function OnboardingPage() { + const navigate = useNavigate(); + const onboardingMutation = usePostOnboardingData(); + + const [onboardingData, setOnboardingData] = useState({ + nickname: '', + gender: '남성', + genre: [], + favorite_movie: '', + }); + + const { currentStep, stepIndex, totalSteps, goToNextStep, goToPrevStep } = + useFunnel({ + steps: STEPS, + }); + + const handleStepComplete = (newData: Partial) => { + setOnboardingData((prevData) => ({ + ...prevData, + ...newData, + })); + goToNextStep(); + }; + + // 최종 제출 처리 + const handleFinalSubmit = (finalData: OnboardingData) => { + onboardingMutation.mutate(finalData, { + onSuccess: (user: OnboardingUserResponse) => { + navigate('/onboarding/result', { + state: { userId: user.id }, + replace: true, + }); + }, + onError: (error: Error) => { + console.error('온보딩 제출 에러:', error); + }, + }); + }; + + return ( +
+ {/* 프로그레스 바 */} + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/pages/OnboardingResult.tsx b/src/pages/OnboardingResult.tsx new file mode 100644 index 0000000..0c786da --- /dev/null +++ b/src/pages/OnboardingResult.tsx @@ -0,0 +1,171 @@ +import { useGetOnboardingUserData } from '@/features/onboarding/hooks/queries/use-get-onboarding-user-data'; +import { useLocation, Link } from 'react-router-dom'; + +export default function OnboardingResultPage() { + const location = useLocation(); + + const state = location.state; + const userId = state?.userId; + + const { + data: user, + isLoading, + isError, + + refetch, + } = useGetOnboardingUserData(userId!); + + if (isLoading) { + return ( +
+
+
+

사용자 정보를 불러오는 중...

+
+
+ ); + } + + if (isError) { + return ( +
+
+
+ + + +

오류 발생

+

+ '사용자 정보를 불러오는데 실패했습니다.' +

+
+ + + 온보딩으로 돌아가기 + +
+
+
+
+ ); + } + + if (user) { + return ( +
+
+ {/* 성공 아이콘 */} +
+
+ + + +
+

+ 온보딩 완료! +

+

환영합니다, {user.nickname}님!

+
+ +
+

입력하신 정보

+ +
+
+ + 사용자 ID + + #{user.id} +
+ +
+ + 닉네임 + + {user.nickname} +
+ +
+ 성별 + {user.gender} +
+ +
+
+ + 관심 장르 + + + {user.genre.length}개 선택 + +
+
+ {user.genre.map((genre, index) => ( + + {genre} + + ))} +
+
+ +
+ + 좋아하는 작품 + + + {user.favorite_movie} + +
+
+
+ +
+ + 다시 시작하기 + +
+ +
+

+ 입력하신 정보는 언제든지 프로필 설정에서 수정하실 수 있습니다. +

+
+
+
+ ); + } + + return null; +} diff --git a/src/routers/Router.tsx b/src/routers/Router.tsx new file mode 100644 index 0000000..7e0d587 --- /dev/null +++ b/src/routers/Router.tsx @@ -0,0 +1,21 @@ +import OnboardingPage from '@/pages/OnboardingPage'; +import OnboardingResultPage from '@/pages/OnboardingResult'; +import OnboardingResultGuard from '@/shared/guards/OnboardingResultGuard'; +import { Navigate, Route, Routes } from 'react-router-dom'; + +export default function Router() { + return ( + + } /> + } /> + + + + } + /> + + ); +} diff --git a/src/shared/components/Button.tsx b/src/shared/components/Button.tsx new file mode 100644 index 0000000..abc2eb1 --- /dev/null +++ b/src/shared/components/Button.tsx @@ -0,0 +1,36 @@ +import type { ButtonHTMLAttributes, ReactNode } from 'react'; + +type ButtonVariant = 'primary' | 'secondary' | 'disabled'; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; + children: ReactNode; + isLoading?: boolean; +} + +export default function Button({ + variant = 'primary', + children, + isLoading = false, + disabled, + className = '', + ...props +}: ButtonProps) { + const isDisabled = disabled || isLoading || variant === 'disabled'; + + const getButtonClass = () => { + if (isDisabled) return 'btn btn-disabled'; + if (variant === 'secondary') return 'btn btn-secondary'; + return 'btn btn-primary'; + }; + + return ( + + ); +} diff --git a/src/shared/components/ErrorMessage.tsx b/src/shared/components/ErrorMessage.tsx new file mode 100644 index 0000000..595a6fa --- /dev/null +++ b/src/shared/components/ErrorMessage.tsx @@ -0,0 +1,13 @@ +interface ErrorMessageProps { + message: string; + className?: string; +} + +export default function ErrorMessage({ + message, + className = '3', +}: ErrorMessageProps) { + if (!message) return null; + + return

{message}

; +} diff --git a/src/shared/components/Header.tsx b/src/shared/components/Header.tsx new file mode 100644 index 0000000..f3506eb --- /dev/null +++ b/src/shared/components/Header.tsx @@ -0,0 +1,10 @@ +import type { ReactNode } from 'react'; + +interface HeaderProps { + children: ReactNode; + className?: string; +} + +export default function Header({ children, className = '' }: HeaderProps) { + return

{children}

; +} diff --git a/src/shared/components/Input.tsx b/src/shared/components/Input.tsx new file mode 100644 index 0000000..579b670 --- /dev/null +++ b/src/shared/components/Input.tsx @@ -0,0 +1,36 @@ +import type { InputHTMLAttributes } from 'react'; +import { forwardRef } from 'react'; + +interface InputProps extends InputHTMLAttributes { + label: string; + error?: string; + id: string; +} + +const Input = forwardRef( + ({ label, error, className = '', id, ...props }, ref) => { + return ( +
+ + + + + {error &&

{error}

} +
+ ); + } +); + +export default Input; diff --git a/src/shared/components/ProgressBar.tsx b/src/shared/components/ProgressBar.tsx new file mode 100644 index 0000000..dde5b5e --- /dev/null +++ b/src/shared/components/ProgressBar.tsx @@ -0,0 +1,31 @@ +interface ProgressBarProps { + currentStep: number; + totalSteps: number; + className?: string; +} + +export default function ProgressBar({ + currentStep, + totalSteps, + className, +}: ProgressBarProps) { + const progressPercentage = Math.round((currentStep / totalSteps) * 100); + + return ( +
+
+ + {currentStep} / {totalSteps} + + {progressPercentage}% +
+ +
+
+
+
+ ); +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 0000000..10a5b8d --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_STALE_TIME = 5 * 60 * 1000; diff --git a/src/shared/guards/OnboardingResultGuard.tsx b/src/shared/guards/OnboardingResultGuard.tsx new file mode 100644 index 0000000..638f19d --- /dev/null +++ b/src/shared/guards/OnboardingResultGuard.tsx @@ -0,0 +1,21 @@ +import { type ReactNode } from 'react'; +import { useLocation, Navigate } from 'react-router-dom'; + +interface OnboardingResultGuardProps { + children: ReactNode; + fallbackPath?: string; +} + +export default function OnboardingResultGuard({ + children, + fallbackPath, +}: OnboardingResultGuardProps) { + const location = useLocation(); + const userId = location.state?.userId; + + return !userId ? ( + + ) : ( + <>{children} + ); +} diff --git a/tsconfig.app.json b/tsconfig.app.json index 9c8a3c4..390bda2 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -15,6 +15,11 @@ "noEmit": true, "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + /* Linting */ "strict": true, "noUnusedLocals": true, diff --git a/tsconfig.node.json b/tsconfig.node.json index 9728af2..d9aa8a8 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -17,7 +17,6 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..69347ed 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,13 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': '/src', + }, + }, +});