diff --git a/Week8/wantkdd/mission1/.gitignore b/Week8/wantkdd/mission1/.gitignore
new file mode 100644
index 00000000..3b0b4037
--- /dev/null
+++ b/Week8/wantkdd/mission1/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env
\ No newline at end of file
diff --git a/Week8/wantkdd/mission1/README.md b/Week8/wantkdd/mission1/README.md
new file mode 100644
index 00000000..40ede56e
--- /dev/null
+++ b/Week8/wantkdd/mission1/README.md
@@ -0,0 +1,54 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default tseslint.config({
+ extends: [
+ // Remove ...tseslint.configs.recommended and replace with this
+ ...tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ ...tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ ...tseslint.configs.stylisticTypeChecked,
+ ],
+ languageOptions: {
+ // other options...
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+})
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default tseslint.config({
+ plugins: {
+ // Add the react-x and react-dom plugins
+ 'react-x': reactX,
+ 'react-dom': reactDom,
+ },
+ rules: {
+ // other rules...
+ // Enable its recommended typescript rules
+ ...reactX.configs['recommended-typescript'].rules,
+ ...reactDom.configs.recommended.rules,
+ },
+})
+```
diff --git a/Week8/wantkdd/mission1/eslint.config.js b/Week8/wantkdd/mission1/eslint.config.js
new file mode 100644
index 00000000..092408a9
--- /dev/null
+++ b/Week8/wantkdd/mission1/eslint.config.js
@@ -0,0 +1,28 @@
+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'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+ },
+)
diff --git a/Week8/wantkdd/mission1/index.html b/Week8/wantkdd/mission1/index.html
new file mode 100644
index 00000000..30d6aa6a
--- /dev/null
+++ b/Week8/wantkdd/mission1/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ WAN LP
+
+
+
+
+
+
diff --git a/Week8/wantkdd/mission1/package.json b/Week8/wantkdd/mission1/package.json
new file mode 100644
index 00000000..d6ebd5d0
--- /dev/null
+++ b/Week8/wantkdd/mission1/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "mission1",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^5.0.1",
+ "@prisma/client": "^6.6.0",
+ "@tailwindcss/vite": "^4.1.3",
+ "@tanstack/react-query": "^5.75.0",
+ "@tanstack/react-query-devtools": "^5.75.1",
+ "axios": "^1.8.4",
+ "hook-form": "^0.0.1",
+ "js-cookie": "^3.0.5",
+ "prisma": "^6.6.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hook-form": "^7.55.0",
+ "react-icons": "^5.5.0",
+ "react-intersection-observer": "^9.16.0",
+ "react-router-dom": "^7.5.0",
+ "tailwindcss": "^4.1.3",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.21.0",
+ "@types/react": "^19.0.10",
+ "@types/react-dom": "^19.0.4",
+ "@vitejs/plugin-react-swc": "^3.8.0",
+ "eslint": "^9.21.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^15.15.0",
+ "typescript": "~5.7.2",
+ "typescript-eslint": "^8.24.1",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/Week8/wantkdd/mission1/pnpm-lock.yaml b/Week8/wantkdd/mission1/pnpm-lock.yaml
new file mode 100644
index 00000000..1fc76050
--- /dev/null
+++ b/Week8/wantkdd/mission1/pnpm-lock.yaml
@@ -0,0 +1,2535 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@hookform/resolvers':
+ specifier: ^5.0.1
+ version: 5.0.1(react-hook-form@7.55.0(react@19.1.0))
+ '@prisma/client':
+ specifier: ^6.6.0
+ version: 6.6.0(prisma@6.6.0(typescript@5.7.3))(typescript@5.7.3)
+ '@tailwindcss/vite':
+ specifier: ^4.1.3
+ version: 4.1.3(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))
+ '@tanstack/react-query':
+ specifier: ^5.75.0
+ version: 5.75.0(react@19.1.0)
+ '@tanstack/react-query-devtools':
+ specifier: ^5.75.1
+ version: 5.75.1(@tanstack/react-query@5.75.0(react@19.1.0))(react@19.1.0)
+ axios:
+ specifier: ^1.8.4
+ version: 1.8.4
+ hook-form:
+ specifier: ^0.0.1
+ version: 0.0.1(react@19.1.0)
+ js-cookie:
+ specifier: ^3.0.5
+ version: 3.0.5
+ prisma:
+ specifier: ^6.6.0
+ version: 6.6.0(typescript@5.7.3)
+ react:
+ specifier: ^19.0.0
+ version: 19.1.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.1.0(react@19.1.0)
+ react-hook-form:
+ specifier: ^7.55.0
+ version: 7.55.0(react@19.1.0)
+ react-icons:
+ specifier: ^5.5.0
+ version: 5.5.0(react@19.1.0)
+ react-intersection-observer:
+ specifier: ^9.16.0
+ version: 9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react-router-dom:
+ specifier: ^7.5.0
+ version: 7.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ tailwindcss:
+ specifier: ^4.1.3
+ version: 4.1.3
+ zod:
+ specifier: ^3.24.2
+ version: 3.24.2
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.21.0
+ version: 9.24.0
+ '@types/react':
+ specifier: ^19.0.10
+ version: 19.1.0
+ '@types/react-dom':
+ specifier: ^19.0.4
+ version: 19.1.1(@types/react@19.1.0)
+ '@vitejs/plugin-react-swc':
+ specifier: ^3.8.0
+ version: 3.8.1(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))
+ eslint:
+ specifier: ^9.21.0
+ version: 9.24.0(jiti@2.4.2)
+ eslint-plugin-react-hooks:
+ specifier: ^5.1.0
+ version: 5.2.0(eslint@9.24.0(jiti@2.4.2))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.19
+ version: 0.4.19(eslint@9.24.0(jiti@2.4.2))
+ globals:
+ specifier: ^15.15.0
+ version: 15.15.0
+ typescript:
+ specifier: ~5.7.2
+ version: 5.7.3
+ typescript-eslint:
+ specifier: ^8.24.1
+ version: 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ vite:
+ specifier: ^6.2.0
+ version: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2)
+
+packages:
+
+ '@esbuild/aix-ppc64@0.25.2':
+ resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.2':
+ resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.2':
+ resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.2':
+ resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.2':
+ resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.2':
+ resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.2':
+ resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.2':
+ resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.2':
+ resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.2':
+ resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.2':
+ resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.2':
+ resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.2':
+ resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.2':
+ resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.2':
+ resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.2':
+ resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.2':
+ resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.2':
+ resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.2':
+ resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.2':
+ resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.2':
+ resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.25.2':
+ resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.2':
+ resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.2':
+ resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.2':
+ resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.5.1':
+ resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.1':
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.20.0':
+ resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.2.1':
+ resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.12.0':
+ resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.13.0':
+ resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.1':
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.24.0':
+ resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.6':
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.2.8':
+ resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@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'}
+
+ '@humanfs/node@0.16.6':
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.3.1':
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+ engines: {node: '>=18.18'}
+
+ '@humanwhocodes/retry@0.4.2':
+ resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
+ engines: {node: '>=18.18'}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@prisma/client@6.6.0':
+ resolution: {integrity: sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==}
+ engines: {node: '>=18.18'}
+ peerDependencies:
+ prisma: '*'
+ typescript: '>=5.1.0'
+ peerDependenciesMeta:
+ prisma:
+ optional: true
+ typescript:
+ optional: true
+
+ '@prisma/config@6.6.0':
+ resolution: {integrity: sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA==}
+
+ '@prisma/debug@6.6.0':
+ resolution: {integrity: sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ==}
+
+ '@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a':
+ resolution: {integrity: sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw==}
+
+ '@prisma/engines@6.6.0':
+ resolution: {integrity: sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg==}
+
+ '@prisma/fetch-engine@6.6.0':
+ resolution: {integrity: sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw==}
+
+ '@prisma/get-platform@6.6.0':
+ resolution: {integrity: sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA==}
+
+ '@rollup/rollup-android-arm-eabi@4.39.0':
+ resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.39.0':
+ resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.39.0':
+ resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.39.0':
+ resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.39.0':
+ resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.39.0':
+ resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
+ resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.39.0':
+ resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.39.0':
+ resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.39.0':
+ resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
+ resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
+ resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.39.0':
+ resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.39.0':
+ resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.39.0':
+ resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.39.0':
+ resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.39.0':
+ resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.39.0':
+ resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.39.0':
+ resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.39.0':
+ resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==}
+ cpu: [x64]
+ os: [win32]
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
+ '@swc/core-darwin-arm64@1.11.16':
+ resolution: {integrity: sha512-l6uWMU+MUdfLHCl3dJgtVEdsUHPskoA4BSu0L1hh9SGBwPZ8xeOz8iLIqZM27lTuXxL4KsYH6GQR/OdQ/vhLtg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@swc/core-darwin-x64@1.11.16':
+ resolution: {integrity: sha512-TH0IW8Ao1WZ4ARFHIh29dAQHYBEl4YnP74n++rjppmlCjY+8v3s5nXMA7IqxO3b5LVHyggWtU4+46DXTyMJM7g==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@swc/core-linux-arm-gnueabihf@1.11.16':
+ resolution: {integrity: sha512-2IxD9t09oNZrbv37p4cJ9cTHMUAK6qNiShi9s2FJ9LcqSnZSN4iS4hvaaX6KZuG54d58vWnMU7yycjkdOTQcMg==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@swc/core-linux-arm64-gnu@1.11.16':
+ resolution: {integrity: sha512-AYkN23DOiPh1bf3XBf/xzZQDKSsgZTxlbyTyUIhprLJpAAAT0ZCGAUcS5mHqydk0nWQ13ABUymodvHoroutNzw==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-arm64-musl@1.11.16':
+ resolution: {integrity: sha512-n/nWXDRCIhM51dDGELfBcTMNnCiFatE7LDvsbYxb7DJt1HGjaCNvHHCKURb/apJTh/YNtWfgFap9dbsTgw8yPA==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-x64-gnu@1.11.16':
+ resolution: {integrity: sha512-xr182YQrF47n7Awxj+/ruI21bYw+xO/B26KFVnb+i3ezF9NOhqoqTX+33RL1ZLA/uFTq8ksPZO/y+ZVS/odtQA==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-linux-x64-musl@1.11.16':
+ resolution: {integrity: sha512-k2JBfiwWfXCIKrBRjFO9/vEdLSYq0QLJ+iNSLdfrejZ/aENNkbEg8O7O2GKUSb30RBacn6k8HMfJrcPLFiEyCQ==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-win32-arm64-msvc@1.11.16':
+ resolution: {integrity: sha512-taOb5U+abyEhQgex+hr6cI48BoqSvSdfmdirWcxprIEUBHCxa1dSriVwnJRAJOFI9T+5BEz88by6rgbB9MjbHA==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@swc/core-win32-ia32-msvc@1.11.16':
+ resolution: {integrity: sha512-b7yYggM9LBDiMY+XUt5kYWvs5sn0U3PXSOGvF3CbLufD/N/YQiDcYON2N3lrWHYL8aYnwbuZl45ojmQHSQPcdA==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@swc/core-win32-x64-msvc@1.11.16':
+ resolution: {integrity: sha512-/ibq/YDc3B5AROkpOKPGxVkSyCKOg+ml8k11RxrW7FAPy6a9y5y9KPcWIqV74Ahq4RuaMNslTQqHWAGSm0xJsQ==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/core@1.11.16':
+ resolution: {integrity: sha512-wgjrJqVUss8Lxqilg0vkiE0tkEKU3mZkoybQM1Ehy+PKWwwB6lFAwKi20cAEFlSSWo8jFR8hRo19ZELAoLDowg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@swc/helpers': '*'
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+
+ '@swc/counter@0.1.3':
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+
+ '@swc/types@0.1.21':
+ resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==}
+
+ '@tailwindcss/node@4.1.3':
+ resolution: {integrity: sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA==}
+
+ '@tailwindcss/oxide-android-arm64@4.1.3':
+ resolution: {integrity: sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.3':
+ resolution: {integrity: sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.1.3':
+ resolution: {integrity: sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.3':
+ resolution: {integrity: sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3':
+ resolution: {integrity: sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.3':
+ resolution: {integrity: sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.3':
+ resolution: {integrity: sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.3':
+ resolution: {integrity: sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.3':
+ resolution: {integrity: sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.3':
+ resolution: {integrity: sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.3':
+ resolution: {integrity: sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.1.3':
+ resolution: {integrity: sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ==}
+ engines: {node: '>= 10'}
+
+ '@tailwindcss/vite@4.1.3':
+ resolution: {integrity: sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6
+
+ '@tanstack/query-core@5.75.0':
+ resolution: {integrity: sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg==}
+
+ '@tanstack/query-devtools@5.74.7':
+ resolution: {integrity: sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==}
+
+ '@tanstack/react-query-devtools@5.75.1':
+ resolution: {integrity: sha512-6S71fJRBlb0adlG5z/OHRqZqtHoPYVCklf/KCnOoZ3vdx9O/K9BLvqeeDeMUkZ6Rak/IupbEbvBsFoUiMHwZiQ==}
+ peerDependencies:
+ '@tanstack/react-query': ^5.75.1
+ react: ^18 || ^19
+
+ '@tanstack/react-query@5.75.0':
+ resolution: {integrity: sha512-H+TNgxmTbzH8qQ5MT5xsZEhQ8BG1tUYduDSfeAOzroVZgd/AEjg1rRYSP/9Tl9/hPobZ7iZzV401n77kStrbKw==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@types/cookie@0.6.0':
+ resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+
+ '@types/estree@1.0.7':
+ resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/react-dom@19.1.1':
+ resolution: {integrity: sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+
+ '@types/react@19.1.0':
+ resolution: {integrity: sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==}
+
+ '@typescript-eslint/eslint-plugin@8.29.0':
+ resolution: {integrity: sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/parser@8.29.0':
+ resolution: {integrity: sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/scope-manager@8.29.0':
+ resolution: {integrity: sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/type-utils@8.29.0':
+ resolution: {integrity: sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/types@8.29.0':
+ resolution: {integrity: sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.29.0':
+ resolution: {integrity: sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/utils@8.29.0':
+ resolution: {integrity: sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/visitor-keys@8.29.0':
+ resolution: {integrity: sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react-swc@3.8.1':
+ resolution: {integrity: sha512-aEUPCckHDcFyxpwFm0AIkbtv6PpUp3xTb9wYGFjtABynXjCYKkWoxX0AOK9NT9XCrdk6mBBUOeHQS+RKdcNO1A==}
+ peerDependencies:
+ vite: ^4 || ^5 || ^6
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.14.1:
+ resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ 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.8.4:
+ resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+
+ brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ 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==}
+
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ debug@4.4.0:
+ resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ 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'}
+
+ detect-libc@2.0.3:
+ resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ enhanced-resolve@5.18.1:
+ resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
+ engines: {node: '>=10.13.0'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ 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-register@3.6.0:
+ resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
+ peerDependencies:
+ esbuild: '>=0.12 <1'
+
+ esbuild@0.25.2:
+ resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@5.2.0:
+ resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react-refresh@0.4.19:
+ resolution: {integrity: sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==}
+ peerDependencies:
+ eslint: '>=8.40'
+
+ eslint-scope@8.3.0:
+ resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.0:
+ resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@9.24.0:
+ resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.3.0:
+ resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fastq@1.19.1:
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ 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'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@15.15.0:
+ resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
+ engines: {node: '>=18'}
+
+ gopd@1.2.0:
+ 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==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-symbols@1.1.0:
+ 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'}
+
+ hook-form@0.0.1:
+ resolution: {integrity: sha512-jusNZfTsHBSqvIgrhG8a1iwdeN1UT3HenQwg3snzd5ArwpNgDQlN2O0XdPqEE2oPUdMr/k16WmCccO8jRgiRYA==}
+ peerDependencies:
+ react: '>=15.0.0 || ^16.0.0'
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jiti@2.4.2:
+ resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
+ hasBin: true
+
+ js-cookie@3.0.5:
+ resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
+ engines: {node: '>=14'}
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-darwin-arm64@1.29.2:
+ resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.29.2:
+ resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.29.2:
+ resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.29.2:
+ resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.29.2:
+ resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.29.2:
+ resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.29.2:
+ resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.29.2:
+ resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.29.2:
+ resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.29.2:
+ resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.29.2:
+ resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromatch@4.0.8:
+ 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-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ postcss@8.5.3:
+ resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prisma@6.6.0:
+ resolution: {integrity: sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg==}
+ engines: {node: '>=18.18'}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.1.0'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ 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'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ react-dom@19.1.0:
+ resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
+ peerDependencies:
+ react: ^19.1.0
+
+ react-hook-form@7.55.0:
+ resolution: {integrity: sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-icons@5.5.0:
+ resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
+ peerDependencies:
+ react: '*'
+
+ react-intersection-observer@9.16.0:
+ resolution: {integrity: sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==}
+ peerDependencies:
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react-router-dom@7.5.0:
+ resolution: {integrity: sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.5.0:
+ resolution: {integrity: sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react@19.1.0:
+ resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rollup@4.39.0:
+ resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ scheduler@0.26.0:
+ resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
+
+ semver@7.7.1:
+ resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ tailwindcss@4.1.3:
+ resolution: {integrity: sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==}
+
+ tapable@2.2.1:
+ resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+ engines: {node: '>=6'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ ts-api-utils@2.1.0:
+ resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ turbo-stream@2.4.0:
+ resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.29.0:
+ resolution: {integrity: sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ typescript@5.7.3:
+ resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite@6.2.5:
+ resolution: {integrity: sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ 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
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod@3.24.2:
+ resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
+
+snapshots:
+
+ '@esbuild/aix-ppc64@0.25.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/android-arm@0.25.2':
+ optional: true
+
+ '@esbuild/android-x64@0.25.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.2':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.5.1(eslint@9.24.0(jiti@2.4.2))':
+ dependencies:
+ eslint: 9.24.0(jiti@2.4.2)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.1': {}
+
+ '@eslint/config-array@0.20.0':
+ dependencies:
+ '@eslint/object-schema': 2.1.6
+ debug: 4.4.0
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.2.1': {}
+
+ '@eslint/core@0.12.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/core@0.13.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.1':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.4.0
+ espree: 10.3.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.24.0': {}
+
+ '@eslint/object-schema@2.1.6': {}
+
+ '@eslint/plugin-kit@0.2.8':
+ dependencies:
+ '@eslint/core': 0.13.0
+ levn: 0.4.1
+
+ '@hookform/resolvers@5.0.1(react-hook-form@7.55.0(react@19.1.0))':
+ dependencies:
+ '@standard-schema/utils': 0.3.0
+ react-hook-form: 7.55.0(react@19.1.0)
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.6':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.3.1
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.3.1': {}
+
+ '@humanwhocodes/retry@0.4.2': {}
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.19.1
+
+ '@prisma/client@6.6.0(prisma@6.6.0(typescript@5.7.3))(typescript@5.7.3)':
+ optionalDependencies:
+ prisma: 6.6.0(typescript@5.7.3)
+ typescript: 5.7.3
+
+ '@prisma/config@6.6.0':
+ dependencies:
+ esbuild: 0.25.2
+ esbuild-register: 3.6.0(esbuild@0.25.2)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@prisma/debug@6.6.0': {}
+
+ '@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a': {}
+
+ '@prisma/engines@6.6.0':
+ dependencies:
+ '@prisma/debug': 6.6.0
+ '@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a
+ '@prisma/fetch-engine': 6.6.0
+ '@prisma/get-platform': 6.6.0
+
+ '@prisma/fetch-engine@6.6.0':
+ dependencies:
+ '@prisma/debug': 6.6.0
+ '@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a
+ '@prisma/get-platform': 6.6.0
+
+ '@prisma/get-platform@6.6.0':
+ dependencies:
+ '@prisma/debug': 6.6.0
+
+ '@rollup/rollup-android-arm-eabi@4.39.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.39.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.39.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.39.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.39.0':
+ optional: true
+
+ '@standard-schema/utils@0.3.0': {}
+
+ '@swc/core-darwin-arm64@1.11.16':
+ optional: true
+
+ '@swc/core-darwin-x64@1.11.16':
+ optional: true
+
+ '@swc/core-linux-arm-gnueabihf@1.11.16':
+ optional: true
+
+ '@swc/core-linux-arm64-gnu@1.11.16':
+ optional: true
+
+ '@swc/core-linux-arm64-musl@1.11.16':
+ optional: true
+
+ '@swc/core-linux-x64-gnu@1.11.16':
+ optional: true
+
+ '@swc/core-linux-x64-musl@1.11.16':
+ optional: true
+
+ '@swc/core-win32-arm64-msvc@1.11.16':
+ optional: true
+
+ '@swc/core-win32-ia32-msvc@1.11.16':
+ optional: true
+
+ '@swc/core-win32-x64-msvc@1.11.16':
+ optional: true
+
+ '@swc/core@1.11.16':
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.21
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.11.16
+ '@swc/core-darwin-x64': 1.11.16
+ '@swc/core-linux-arm-gnueabihf': 1.11.16
+ '@swc/core-linux-arm64-gnu': 1.11.16
+ '@swc/core-linux-arm64-musl': 1.11.16
+ '@swc/core-linux-x64-gnu': 1.11.16
+ '@swc/core-linux-x64-musl': 1.11.16
+ '@swc/core-win32-arm64-msvc': 1.11.16
+ '@swc/core-win32-ia32-msvc': 1.11.16
+ '@swc/core-win32-x64-msvc': 1.11.16
+
+ '@swc/counter@0.1.3': {}
+
+ '@swc/types@0.1.21':
+ dependencies:
+ '@swc/counter': 0.1.3
+
+ '@tailwindcss/node@4.1.3':
+ dependencies:
+ enhanced-resolve: 5.18.1
+ jiti: 2.4.2
+ lightningcss: 1.29.2
+ tailwindcss: 4.1.3
+
+ '@tailwindcss/oxide-android-arm64@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide@4.1.3':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.1.3
+ '@tailwindcss/oxide-darwin-arm64': 4.1.3
+ '@tailwindcss/oxide-darwin-x64': 4.1.3
+ '@tailwindcss/oxide-freebsd-x64': 4.1.3
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.3
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.1.3
+ '@tailwindcss/oxide-linux-arm64-musl': 4.1.3
+ '@tailwindcss/oxide-linux-x64-gnu': 4.1.3
+ '@tailwindcss/oxide-linux-x64-musl': 4.1.3
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.1.3
+ '@tailwindcss/oxide-win32-x64-msvc': 4.1.3
+
+ '@tailwindcss/vite@4.1.3(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))':
+ dependencies:
+ '@tailwindcss/node': 4.1.3
+ '@tailwindcss/oxide': 4.1.3
+ tailwindcss: 4.1.3
+ vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2)
+
+ '@tanstack/query-core@5.75.0': {}
+
+ '@tanstack/query-devtools@5.74.7': {}
+
+ '@tanstack/react-query-devtools@5.75.1(@tanstack/react-query@5.75.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@tanstack/query-devtools': 5.74.7
+ '@tanstack/react-query': 5.75.0(react@19.1.0)
+ react: 19.1.0
+
+ '@tanstack/react-query@5.75.0(react@19.1.0)':
+ dependencies:
+ '@tanstack/query-core': 5.75.0
+ react: 19.1.0
+
+ '@types/cookie@0.6.0': {}
+
+ '@types/estree@1.0.7': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/react-dom@19.1.1(@types/react@19.1.0)':
+ dependencies:
+ '@types/react': 19.1.0
+
+ '@types/react@19.1.0':
+ dependencies:
+ csstype: 3.1.3
+
+ '@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/scope-manager': 8.29.0
+ '@typescript-eslint/type-utils': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.29.0
+ eslint: 9.24.0(jiti@2.4.2)
+ graphemer: 1.4.0
+ ignore: 5.3.2
+ natural-compare: 1.4.0
+ ts-api-utils: 2.1.0(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.29.0
+ '@typescript-eslint/types': 8.29.0
+ '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.29.0
+ debug: 4.4.0
+ eslint: 9.24.0(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.29.0':
+ dependencies:
+ '@typescript-eslint/types': 8.29.0
+ '@typescript-eslint/visitor-keys': 8.29.0
+
+ '@typescript-eslint/type-utils@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ debug: 4.4.0
+ eslint: 9.24.0(jiti@2.4.2)
+ ts-api-utils: 2.1.0(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.29.0': {}
+
+ '@typescript-eslint/typescript-estree@8.29.0(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.29.0
+ '@typescript-eslint/visitor-keys': 8.29.0
+ debug: 4.4.0
+ fast-glob: 3.3.3
+ is-glob: 4.0.3
+ minimatch: 9.0.5
+ semver: 7.7.1
+ ts-api-utils: 2.1.0(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@2.4.2))
+ '@typescript-eslint/scope-manager': 8.29.0
+ '@typescript-eslint/types': 8.29.0
+ '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.7.3)
+ eslint: 9.24.0(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.29.0':
+ dependencies:
+ '@typescript-eslint/types': 8.29.0
+ eslint-visitor-keys: 4.2.0
+
+ '@vitejs/plugin-react-swc@3.8.1(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))':
+ dependencies:
+ '@swc/core': 1.11.16
+ vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2)
+ transitivePeerDependencies:
+ - '@swc/helpers'
+
+ acorn-jsx@5.3.2(acorn@8.14.1):
+ dependencies:
+ acorn: 8.14.1
+
+ acorn@8.14.1: {}
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ asynckit@0.4.0: {}
+
+ axios@1.8.4:
+ dependencies:
+ follow-redirects: 1.15.9
+ form-data: 4.0.2
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ balanced-match@1.0.2: {}
+
+ brace-expansion@1.1.11:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.1:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ callsites@3.1.0: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.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: {}
+
+ cookie@1.0.2: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.1.3: {}
+
+ debug@4.4.0:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ delayed-stream@1.0.0: {}
+
+ detect-libc@2.0.3: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ enhanced-resolve@5.18.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.2.1
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ 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-register@3.6.0(esbuild@0.25.2):
+ dependencies:
+ debug: 4.4.0
+ esbuild: 0.25.2
+ transitivePeerDependencies:
+ - supports-color
+
+ esbuild@0.25.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.2
+ '@esbuild/android-arm': 0.25.2
+ '@esbuild/android-arm64': 0.25.2
+ '@esbuild/android-x64': 0.25.2
+ '@esbuild/darwin-arm64': 0.25.2
+ '@esbuild/darwin-x64': 0.25.2
+ '@esbuild/freebsd-arm64': 0.25.2
+ '@esbuild/freebsd-x64': 0.25.2
+ '@esbuild/linux-arm': 0.25.2
+ '@esbuild/linux-arm64': 0.25.2
+ '@esbuild/linux-ia32': 0.25.2
+ '@esbuild/linux-loong64': 0.25.2
+ '@esbuild/linux-mips64el': 0.25.2
+ '@esbuild/linux-ppc64': 0.25.2
+ '@esbuild/linux-riscv64': 0.25.2
+ '@esbuild/linux-s390x': 0.25.2
+ '@esbuild/linux-x64': 0.25.2
+ '@esbuild/netbsd-arm64': 0.25.2
+ '@esbuild/netbsd-x64': 0.25.2
+ '@esbuild/openbsd-arm64': 0.25.2
+ '@esbuild/openbsd-x64': 0.25.2
+ '@esbuild/sunos-x64': 0.25.2
+ '@esbuild/win32-arm64': 0.25.2
+ '@esbuild/win32-ia32': 0.25.2
+ '@esbuild/win32-x64': 0.25.2
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@5.2.0(eslint@9.24.0(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.24.0(jiti@2.4.2)
+
+ eslint-plugin-react-refresh@0.4.19(eslint@9.24.0(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.24.0(jiti@2.4.2)
+
+ eslint-scope@8.3.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.0: {}
+
+ eslint@9.24.0(jiti@2.4.2):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@2.4.2))
+ '@eslint-community/regexpp': 4.12.1
+ '@eslint/config-array': 0.20.0
+ '@eslint/config-helpers': 0.2.1
+ '@eslint/core': 0.12.0
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.24.0
+ '@eslint/plugin-kit': 0.2.8
+ '@humanfs/node': 0.16.6
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.2
+ '@types/estree': 1.0.7
+ '@types/json-schema': 7.0.15
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.3.0
+ eslint-visitor-keys: 4.2.0
+ espree: 10.3.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.4.2
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.3.0:
+ dependencies:
+ acorn: 8.14.1
+ acorn-jsx: 5.3.2(acorn@8.14.1)
+ eslint-visitor-keys: 4.2.0
+
+ esquery@1.6.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fastq@1.19.1:
+ dependencies:
+ reusify: 1.1.0
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
+ 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
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ globals@15.15.0: {}
+
+ 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
+
+ hook-form@0.0.1(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ ignore@5.3.2: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-number@7.0.0: {}
+
+ isexe@2.0.0: {}
+
+ jiti@2.4.2: {}
+
+ js-cookie@3.0.5: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-darwin-arm64@1.29.2:
+ optional: true
+
+ lightningcss-darwin-x64@1.29.2:
+ optional: true
+
+ lightningcss-freebsd-x64@1.29.2:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.29.2:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.29.2:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.29.2:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.29.2:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.29.2:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.29.2:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.29.2:
+ optional: true
+
+ lightningcss@1.29.2:
+ dependencies:
+ detect-libc: 2.0.3
+ optionalDependencies:
+ lightningcss-darwin-arm64: 1.29.2
+ lightningcss-darwin-x64: 1.29.2
+ lightningcss-freebsd-x64: 1.29.2
+ lightningcss-linux-arm-gnueabihf: 1.29.2
+ lightningcss-linux-arm64-gnu: 1.29.2
+ lightningcss-linux-arm64-musl: 1.29.2
+ lightningcss-linux-x64-gnu: 1.29.2
+ lightningcss-linux-x64-musl: 1.29.2
+ lightningcss-win32-arm64-msvc: 1.29.2
+ lightningcss-win32-x64-msvc: 1.29.2
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.merge@4.6.2: {}
+
+ math-intrinsics@1.1.0: {}
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.11
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.1
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.11: {}
+
+ natural-compare@1.4.0: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ postcss@8.5.3:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ prisma@6.6.0(typescript@5.7.3):
+ dependencies:
+ '@prisma/config': 6.6.0
+ '@prisma/engines': 6.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ proxy-from-env@1.1.0: {}
+
+ punycode@2.3.1: {}
+
+ queue-microtask@1.2.3: {}
+
+ react-dom@19.1.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ scheduler: 0.26.0
+
+ react-hook-form@7.55.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ react-icons@5.5.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ react-intersection-observer@9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ react-dom: 19.1.0(react@19.1.0)
+
+ react-router-dom@7.5.0(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.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+
+ react-router@7.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ '@types/cookie': 0.6.0
+ cookie: 1.0.2
+ react: 19.1.0
+ set-cookie-parser: 2.7.1
+ turbo-stream: 2.4.0
+ optionalDependencies:
+ react-dom: 19.1.0(react@19.1.0)
+
+ react@19.1.0: {}
+
+ resolve-from@4.0.0: {}
+
+ reusify@1.1.0: {}
+
+ rollup@4.39.0:
+ dependencies:
+ '@types/estree': 1.0.7
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.39.0
+ '@rollup/rollup-android-arm64': 4.39.0
+ '@rollup/rollup-darwin-arm64': 4.39.0
+ '@rollup/rollup-darwin-x64': 4.39.0
+ '@rollup/rollup-freebsd-arm64': 4.39.0
+ '@rollup/rollup-freebsd-x64': 4.39.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.39.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.39.0
+ '@rollup/rollup-linux-arm64-gnu': 4.39.0
+ '@rollup/rollup-linux-arm64-musl': 4.39.0
+ '@rollup/rollup-linux-loongarch64-gnu': 4.39.0
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.39.0
+ '@rollup/rollup-linux-riscv64-musl': 4.39.0
+ '@rollup/rollup-linux-s390x-gnu': 4.39.0
+ '@rollup/rollup-linux-x64-gnu': 4.39.0
+ '@rollup/rollup-linux-x64-musl': 4.39.0
+ '@rollup/rollup-win32-arm64-msvc': 4.39.0
+ '@rollup/rollup-win32-ia32-msvc': 4.39.0
+ '@rollup/rollup-win32-x64-msvc': 4.39.0
+ fsevents: 2.3.3
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ scheduler@0.26.0: {}
+
+ semver@7.7.1: {}
+
+ set-cookie-parser@2.7.1: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ strip-json-comments@3.1.1: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ tailwindcss@4.1.3: {}
+
+ tapable@2.2.1: {}
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ ts-api-utils@2.1.0(typescript@5.7.3):
+ dependencies:
+ typescript: 5.7.3
+
+ turbo-stream@2.4.0: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/parser': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ eslint: 9.24.0(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.7.3: {}
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2):
+ dependencies:
+ esbuild: 0.25.2
+ postcss: 8.5.3
+ rollup: 4.39.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ jiti: 2.4.2
+ lightningcss: 1.29.2
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod@3.24.2: {}
diff --git a/Week8/wantkdd/mission1/public/vite.svg b/Week8/wantkdd/mission1/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/Week8/wantkdd/mission1/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Week8/wantkdd/mission1/src/App.css b/Week8/wantkdd/mission1/src/App.css
new file mode 100644
index 00000000..e69de29b
diff --git a/Week8/wantkdd/mission1/src/App.tsx b/Week8/wantkdd/mission1/src/App.tsx
new file mode 100644
index 00000000..75662b2b
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/App.tsx
@@ -0,0 +1,73 @@
+import './App.css';
+import {
+ createBrowserRouter,
+ RouteObject,
+ RouterProvider,
+} from 'react-router-dom';
+import RootLayout from './layout/root-layout';
+import HomePage from './pages/home-page';
+import LoginPage from './pages/login-page';
+import SignupPage from './pages/signup-page';
+import { AuthProvider } from './context/AuthContext';
+import MyPage from './pages/my-page';
+import ProtectedLayout from './layout/ProtectedLayout';
+import GoogleLoginRedirectPage from './pages/googleLoginRedirect-page';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import LpDetailPage from './pages/detail-page';
+import SearchPage from './pages/search-page';
+
+const publicRoutes: RouteObject[] = [
+ {
+ path: '/',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: '/search-page',
+ element: ,
+ },
+ {
+ path: '/login-page',
+ element: ,
+ },
+ {
+ path: '/signup-page',
+ element: ,
+ },
+ { path: 'v1/auth/google/callback', element: },
+ ],
+ },
+];
+
+const protectedRoutes: RouteObject[] = [
+ {
+ element: ,
+ children: [
+ { path: '/my-page', element: },
+ { path: '/lp/:id', element: },
+ ],
+ },
+];
+
+const router = createBrowserRouter([...publicRoutes, ...protectedRoutes]);
+
+export const queryClient = new QueryClient();
+
+function App() {
+ return (
+ <>
+
+
+
+
+ {import.meta.env.DEV && }
+
+ >
+ );
+}
+
+export default App;
diff --git a/Week8/wantkdd/mission1/src/apis/addLp.ts b/Week8/wantkdd/mission1/src/apis/addLp.ts
new file mode 100644
index 00000000..cae52948
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/apis/addLp.ts
@@ -0,0 +1,30 @@
+import { CreateLpDto, Tag } from '../types/lp';
+import { axiosInstance } from './axios';
+
+export async function postLp(lpData: CreateLpDto) {
+ const preparedData = {
+ ...lpData,
+ tags: lpData.tags.map((tag: Tag) => tag.name),
+ };
+
+ // console.log('전송 데이터:', preparedData);
+ const response = await axiosInstance.post('/v1/lps', preparedData, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ return response.data;
+}
+
+export async function uploadImage(file: File): Promise {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await axiosInstance.post('/v1/uploads', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ return response.data.data.imageUrl;
+}
diff --git a/Week8/wantkdd/mission1/src/apis/auth.ts b/Week8/wantkdd/mission1/src/apis/auth.ts
new file mode 100644
index 00000000..338026df
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/apis/auth.ts
@@ -0,0 +1,48 @@
+import { LOCAL_STORAGE_KEY } from '../constants/key';
+import {
+ PatchUserInfoDto,
+ RequestSigninDto,
+ RequestSignupDto,
+ ResponseMyInfoDto,
+ ResponseSigninDto,
+} from '../types/auth';
+import { axiosInstance } from './axios';
+
+export const postSignup = async (
+ body: RequestSignupDto
+): Promise => {
+ const { data } = await axiosInstance.post('/v1/auth/signup', body);
+
+ return data;
+};
+
+export const postSignin = async (
+ body: RequestSigninDto
+): Promise => {
+ const { data } = await axiosInstance.post('/v1/auth/signin', body);
+
+ return data;
+};
+
+export const getMyInfo = async (): Promise => {
+ const { data } = await axiosInstance.get('/v1/users/me');
+ return data;
+};
+
+export const postLogout = async () => {
+ const refreshToken = localStorage.getItem(LOCAL_STORAGE_KEY.refreshToken);
+
+ const { data } = await axiosInstance.post('/v1/auth/signout', {
+ refreshToken,
+ });
+ return data;
+};
+
+export const patchUserInfo = async (body: PatchUserInfoDto) => {
+ const { data } = await axiosInstance.patch('/v1/users', body);
+ return data;
+};
+
+export const deleteUser = async () => {
+ return await axiosInstance.delete('/v1/users/');
+};
diff --git a/Week8/wantkdd/mission1/src/apis/axios.ts b/Week8/wantkdd/mission1/src/apis/axios.ts
new file mode 100644
index 00000000..b3fab508
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/apis/axios.ts
@@ -0,0 +1,105 @@
+import axios, { InternalAxiosRequestConfig } from 'axios';
+import { LOCAL_STORAGE_KEY } from '../constants/key';
+
+interface CustomInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
+ _retry?: boolean;
+}
+
+let refreshPromise: Promise | null = null;
+
+export const axiosInstance = axios.create({
+ baseURL: import.meta.env.VITE_SERVER_API_URL,
+ headers: {
+ accept: 'application/json',
+ },
+});
+
+axiosInstance.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('accessToken');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+axiosInstance.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ const originalRequest: CustomInternalAxiosRequestConfig = error.config;
+
+ if (originalRequest.url === '/v1/auth/signin') {
+ return Promise.reject(error);
+ }
+ if (
+ error.response &&
+ error.response.status === 401 &&
+ !originalRequest._retry
+ ) {
+ if (originalRequest.url === '/v1/auth/refresh') {
+ localStorage.removeItem(LOCAL_STORAGE_KEY.accessToken);
+ localStorage.removeItem(LOCAL_STORAGE_KEY.refreshToken);
+ window.location.href = '/login-page';
+ return Promise.reject(error);
+ }
+
+ originalRequest._retry = true;
+
+ if (!refreshPromise) {
+ refreshPromise = (async () => {
+ try {
+ const refreshToken = localStorage.getItem(
+ LOCAL_STORAGE_KEY.refreshToken
+ );
+
+ if (!refreshToken) {
+ throw new Error('리프레시 토큰이 없습니다');
+ }
+
+ const { data } = await axiosInstance.post('/v1/auth/refresh', {
+ refresh: refreshToken,
+ });
+
+ localStorage.setItem(
+ LOCAL_STORAGE_KEY.accessToken,
+ data.data.accessToken
+ );
+ localStorage.setItem(
+ LOCAL_STORAGE_KEY.refreshToken,
+ data.data.refreshToken
+ );
+
+ return data.data.accessToken;
+ } catch (refreshError) {
+ localStorage.removeItem(LOCAL_STORAGE_KEY.accessToken);
+ localStorage.removeItem(LOCAL_STORAGE_KEY.refreshToken);
+
+ window.location.href = '/login-page';
+ throw refreshError;
+ } finally {
+ refreshPromise = null;
+ }
+ })();
+ }
+
+ try {
+ const newAccessToken = await refreshPromise;
+ if (newAccessToken) {
+ axiosInstance.defaults.headers.common[
+ 'Authorization'
+ ] = `Bearer ${newAccessToken}`;
+ originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
+ return axiosInstance(originalRequest);
+ }
+ return Promise.reject(error);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+ return Promise.reject(error);
+ }
+);
diff --git a/Week8/wantkdd/mission1/src/apis/like.ts b/Week8/wantkdd/mission1/src/apis/like.ts
new file mode 100644
index 00000000..1c6eef10
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/apis/like.ts
@@ -0,0 +1,11 @@
+import { ResponseLikeLpDto } from '../types/lp-detail';
+import { axiosInstance } from './axios';
+
+export const postLike = async (lpId: number): Promise => {
+ const { data } = await axiosInstance.post(`/v1/lps/${lpId}/likes`);
+ return data;
+};
+export const deleteLike = async (lpId: number): Promise => {
+ const { data } = await axiosInstance.delete(`/v1/lps/${lpId}/likes`);
+ return data;
+};
diff --git a/Week8/wantkdd/mission1/src/apis/lp-detail.ts b/Week8/wantkdd/mission1/src/apis/lp-detail.ts
new file mode 100644
index 00000000..c9925f68
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/apis/lp-detail.ts
@@ -0,0 +1,72 @@
+import { axiosInstance } from './axios';
+import {
+ LpDetailResponse,
+ CommentsResponse,
+ LpDetailData,
+ UpdateLpRequest,
+} from '../types/lp-detail';
+import { PaginationDto } from '../types/common';
+
+export const getLpDetail = async (id: number): Promise => {
+ const { data } = await axiosInstance.get(`/v1/lps/${id}`);
+ return data.data;
+};
+
+export const getComments = async ({
+ lpId,
+ cursor,
+ limit,
+ order,
+}: {
+ lpId: number;
+} & PaginationDto): Promise => {
+ const { data } = await axiosInstance.get(
+ `/v1/lps/${lpId}/comments`,
+ {
+ params: { cursor, limit, order },
+ }
+ );
+ return data;
+};
+
+export const createComment = async (
+ lpId: string,
+ content: string
+): Promise => {
+ const { data } = await axiosInstance.post(`/v1/lps/${lpId}/comments`, {
+ content,
+ });
+ return data.data;
+};
+
+export const updateComment = async (
+ lpId: number,
+ commentId: number,
+ content: string
+) => {
+ const { data } = await axiosInstance.patch(
+ `/v1/lps/${lpId}/comments/${commentId}`,
+ {
+ content,
+ }
+ );
+ return data.data;
+};
+
+export const deleteComment = async (lpId: number, commentId: number) => {
+ const { data } = await axiosInstance.delete(
+ `/v1/lps/${lpId}/comments/${commentId}`
+ );
+ return data.data;
+};
+
+export const updateLp = async ({
+ lpId,
+ payload,
+}: {
+ lpId: number;
+ payload: UpdateLpRequest;
+}): Promise => {
+ const { data } = await axiosInstance.patch(`/v1/lps/${lpId}`, payload);
+ return data.data;
+};
diff --git a/Week8/wantkdd/mission1/src/apis/lp.ts b/Week8/wantkdd/mission1/src/apis/lp.ts
new file mode 100644
index 00000000..5d47c46a
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/apis/lp.ts
@@ -0,0 +1,17 @@
+import { PaginationDto } from '../types/common';
+import { ResponseLpListDto } from '../types/lp';
+import { axiosInstance } from './axios';
+
+export const getLpList = async (
+ paginationDto: PaginationDto
+): Promise => {
+ const { data } = await axiosInstance.get('/v1/lps', {
+ params: paginationDto,
+ });
+ return data;
+};
+
+export const deleteLp = async (lpId: number): Promise => {
+ const { data } = await axiosInstance.delete(`/v1/lps/${lpId}`);
+ return data.data;
+};
diff --git a/Week8/wantkdd/mission1/src/assets/default.png b/Week8/wantkdd/mission1/src/assets/default.png
new file mode 100644
index 00000000..aa051290
Binary files /dev/null and b/Week8/wantkdd/mission1/src/assets/default.png differ
diff --git a/Week8/wantkdd/mission1/src/assets/googleLogo.svg b/Week8/wantkdd/mission1/src/assets/googleLogo.svg
new file mode 100644
index 00000000..eb339320
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/assets/googleLogo.svg
@@ -0,0 +1,16 @@
+
diff --git a/Week8/wantkdd/mission1/src/assets/react.svg b/Week8/wantkdd/mission1/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Week8/wantkdd/mission1/src/components/add-lp/addButton.tsx b/Week8/wantkdd/mission1/src/components/add-lp/addButton.tsx
new file mode 100644
index 00000000..1a72ef1a
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/add-lp/addButton.tsx
@@ -0,0 +1,20 @@
+import { useState } from 'react';
+import AddLpModal from './addLp';
+
+const AddButton = () => {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+
+ {open && setOpen(false)} />}
+ >
+ );
+};
+
+export default AddButton;
diff --git a/Week8/wantkdd/mission1/src/components/add-lp/addLp.tsx b/Week8/wantkdd/mission1/src/components/add-lp/addLp.tsx
new file mode 100644
index 00000000..28cdea54
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/add-lp/addLp.tsx
@@ -0,0 +1,188 @@
+import React, { useState, useRef } from 'react';
+import { CreateLpDto, Tag } from '../../types/lp';
+import usePostLp from '../../hooks/mutations/lp/usePostLp';
+import { uploadImage } from '../../apis/addLp';
+
+const AddLp = ({ onClose }: { onClose: () => void }) => {
+ const modalRef = useRef(null);
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [thumbnail, setThumbnail] = useState('');
+ const [tags, setTags] = useState([]);
+ const [currentTag, setCurrentTag] = useState('');
+
+ const { mutate: createLp, isPending } = usePostLp();
+
+ const handleBackdropClick = (e: React.MouseEvent) => {
+ if (e.target === modalRef.current) {
+ onClose();
+ }
+ };
+
+ const handleAddTag = () => {
+ const trimmed = currentTag.trim();
+ if (trimmed) {
+ const exists = tags.some(
+ (tag) => tag.name.toLowerCase() === trimmed.toLowerCase()
+ );
+ if (!exists) {
+ const newTag: Tag = {
+ id: tags.length > 0 ? Math.max(...tags.map((t) => t.id)) + 1 : 1,
+ name: trimmed,
+ };
+ setTags((prev) => [...prev, newTag]);
+ setCurrentTag('');
+ }
+ }
+ };
+
+ const handleRemoveTag = (tagToRemove: Tag) => {
+ setTags((prev) => prev.filter((tag) => tag.id !== tagToRemove.id));
+ };
+
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ try {
+ const uploadedUrl = await uploadImage(file);
+ setThumbnail(uploadedUrl);
+ } catch (error) {
+ console.error('이미지 업로드 실패:', error);
+ alert('이미지 업로드 중 오류가 발생했습니다.');
+ }
+ };
+
+ const handleCreateLp = () => {
+ if (!title.trim() || !content.trim()) {
+ alert('제목과 내용을 입력해주세요.');
+ return;
+ }
+
+ const lpData: CreateLpDto = {
+ title,
+ content,
+ thumbnail,
+ published: true,
+ tags,
+ };
+
+ createLp(lpData, {
+ onSuccess: () => {
+ onClose();
+ },
+ onError: (error) => {
+ console.error('LP 생성 실패:', error);
+ alert('LP 생성 중 오류가 발생했습니다.');
+ },
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default AddLp;
diff --git a/Week8/wantkdd/mission1/src/components/footer.tsx b/Week8/wantkdd/mission1/src/components/footer.tsx
new file mode 100644
index 00000000..ed1090d9
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/footer.tsx
@@ -0,0 +1,20 @@
+import { Link } from 'react-router-dom';
+
+const footer = () => {
+ return (
+
+ );
+};
+
+export default footer;
diff --git a/Week8/wantkdd/mission1/src/components/googleLogin.tsx b/Week8/wantkdd/mission1/src/components/googleLogin.tsx
new file mode 100644
index 00000000..70c36b5e
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/googleLogin.tsx
@@ -0,0 +1,19 @@
+import googleLogo from '../assets/googleLogo.svg';
+
+const GoogleLoginButton = () => {
+ const handleGoogleLogin = () => {
+ window.location.href =
+ import.meta.env.VITE_SERVER_API_URL + '/v1/auth/google/login';
+ };
+
+ return (
+
+ );
+};
+
+export default GoogleLoginButton;
diff --git a/Week8/wantkdd/mission1/src/components/loading-spinner.tsx b/Week8/wantkdd/mission1/src/components/loading-spinner.tsx
new file mode 100644
index 00000000..9da4333a
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/loading-spinner.tsx
@@ -0,0 +1,10 @@
+export const LoadingSpinner = () => {
+ return (
+
+ 로딩 중...
+
+ );
+};
diff --git a/Week8/wantkdd/mission1/src/components/login-form.tsx b/Week8/wantkdd/mission1/src/components/login-form.tsx
new file mode 100644
index 00000000..4cc0a265
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/login-form.tsx
@@ -0,0 +1,111 @@
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { signinSchema, SigninFormFields } from '../utils/validate';
+import { useNavigate } from 'react-router-dom';
+// import { useAuth } from '../context/AuthContext';
+import { useLogin } from '../hooks/mutations/user/useLogin';
+const LoginForm = () => {
+ // const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid, isDirty },
+ } = useForm({
+ resolver: zodResolver(signinSchema),
+ mode: 'onBlur',
+ defaultValues: {
+ email: '',
+ password: '',
+ },
+ });
+ const { mutateAsync: login } = useLogin();
+ const onSubmit = async (data: SigninFormFields) => {
+ try {
+ await login(data);
+ navigate('/my-page');
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+ );
+};
+
+interface EmailInputProps {
+ register: ReturnType>['register'];
+ error?: {
+ message?: string;
+ };
+}
+
+const EmailInput = ({ register, error }: EmailInputProps) => {
+ return (
+
+
+ {error && (
+
{error.message}
+ )}
+
+ );
+};
+
+interface PasswordInputProps {
+ register: ReturnType>['register'];
+ error?: {
+ message?: string;
+ };
+}
+
+const PasswordInput = ({ register, error }: PasswordInputProps) => {
+ return (
+
+
+ {error && (
+
{error.message}
+ )}
+
+ );
+};
+
+interface SubmitButtonProps {
+ isDisabled: boolean;
+}
+
+const SubmitButton = ({ isDisabled }: SubmitButtonProps) => {
+ return (
+
+ );
+};
+
+export default LoginForm;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/DeleteModal.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/DeleteModal.tsx
new file mode 100644
index 00000000..eac51846
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/DeleteModal.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+interface DeleteModalProps {
+ onCancel: () => void;
+ onConfirm: () => void;
+}
+
+const DeleteModal: React.FC = ({ onCancel, onConfirm }) => {
+ return (
+
+
+
LP 삭제 확인
+
정말 이 LP를 삭제하시겠습니까?
+
+
+
+
+
+
+ );
+};
+
+export default DeleteModal;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/LpComments.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/LpComments.tsx
new file mode 100644
index 00000000..41f5da70
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/LpComments.tsx
@@ -0,0 +1,131 @@
+import React, { forwardRef } from 'react';
+import { Comment } from '../../types/lp-detail';
+import CommentSkeleton from './comment-skeleton';
+import CommentItem from './comment';
+import { useAuth } from '../../context/AuthContext';
+import useGetMyInfo from '../../hooks/queries/useGetMyInfo';
+
+interface LpCommentsProps {
+ lpId: number;
+ comments: Comment[];
+ commentContent: string;
+ setCommentContent: (content: string) => void;
+ commentOrder: 'desc' | 'asc';
+ setCommentOrder: (order: 'desc' | 'asc') => void;
+ onSubmitComment: (e: React.FormEvent) => void;
+ onUpdateComment: (commentId: number, content: string) => void;
+ onDeleteComment: (commentId: number) => void;
+ isLoading: boolean;
+ hasError: unknown;
+ hasNextPage: boolean | undefined;
+ isFetchingNextPage: boolean;
+ ref: any;
+}
+
+const LpComments: React.FC = forwardRef(
+ (
+ {
+ comments,
+ commentContent,
+ setCommentContent,
+ commentOrder,
+ setCommentOrder,
+ onSubmitComment,
+ onUpdateComment,
+ onDeleteComment,
+ isLoading,
+ hasError,
+ hasNextPage,
+ },
+ ref
+ ) => {
+ const { accessToken } = useAuth();
+ const { data: me } = useGetMyInfo(accessToken);
+
+ return (
+
+
+
+
댓글
+
+
+
+
+
+
+
+
+
+
+
+
+ {isLoading && (
+ <>
+
+
+
+ >
+ )}
+
+ {!isLoading && !hasError && (
+ <>
+ {comments.map((comment) => (
+
+ ))}
+ {hasNextPage &&
}
+ >
+ )}
+
+
+ );
+ }
+);
+
+export default LpComments;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/LpEdit.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/LpEdit.tsx
new file mode 100644
index 00000000..bdd7b4d5
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/LpEdit.tsx
@@ -0,0 +1,266 @@
+import React, { useState, useRef } from 'react';
+import { LpDetailData } from '../../types/lp-detail';
+import { Tag } from '../../types/lp';
+import { useUpdateLp } from '../../hooks/mutations/lp/useUpdateLp';
+import { queryClient } from '../../App';
+
+interface LpEditProps {
+ lpData: LpDetailData;
+ onCancel: () => void;
+ onDelete: () => void;
+}
+
+const LpEdit: React.FC = ({ lpData, onCancel, onDelete }) => {
+ const [editingField, setEditingField] = useState(null);
+ const [title, setTitle] = useState(lpData.title);
+ const [content, setContent] = useState(lpData.content);
+ const [thumbnail, setThumbnail] = useState(lpData.thumbnail);
+ const [previewThumbnail, setPreviewThumbnail] = useState(lpData.thumbnail);
+ const [tags, setTags] = useState(lpData.tags || []);
+ const [currentTag, setCurrentTag] = useState('');
+
+ const fileInputRef = useRef(null);
+ const { mutate: updateLp, isPending: isUpdating } = useUpdateLp();
+
+ const startEditing = (field: string) => {
+ setEditingField(field);
+ };
+
+ const handleThumbnailClick = () => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const result = reader.result as string;
+ setPreviewThumbnail(result);
+ setThumbnail(result);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleAddTag = () => {
+ const trimmed = currentTag.trim();
+ if (trimmed) {
+ const exists = tags.some(
+ (tag) => tag.name.toLowerCase() === trimmed.toLowerCase()
+ );
+ if (!exists) {
+ const newTag: Tag = {
+ id: tags.length > 0 ? Math.max(...tags.map((t) => t.id)) + 1 : 1,
+ name: trimmed,
+ };
+ setTags((prev) => [...prev, newTag]);
+ setCurrentTag('');
+ }
+ }
+ };
+
+ const handleRemoveTag = (tagToRemove: Tag) => {
+ setTags((prev) => prev.filter((tag) => tag.id !== tagToRemove.id));
+ };
+
+ const handleSaveChanges = () => {
+ if (!title.trim()) {
+ alert('제목은 빈칸일 수 없습니다.');
+ return;
+ }
+
+ updateLp(
+ {
+ lpId: lpData.id,
+ payload: {
+ title,
+ content,
+ thumbnail,
+ tags: tags.map((tag) => tag.name),
+ },
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpDetail', lpData.id] });
+ alert('LP가 성공적으로 업데이트되었습니다.');
+ onCancel();
+ },
+ }
+ );
+ };
+
+ return (
+
+
+
+
+ {lpData.author?.name?.charAt(0) || '?'}
+
+
+ {lpData.author?.name || '익명'}
+
+
+
+
+
+
+
+
+
+
+ {editingField === 'title' ? (
+
setTitle(e.target.value)}
+ onBlur={() => setEditingField(null)}
+ autoFocus
+ />
+ ) : (
+
startEditing('title')}
+ >
+ {title}
+
+ )}
+
+ {new Date(lpData.createdAt).toLocaleDateString()}
+
+
+
+
+
+

+
+
+
+
+ 클릭하여 LP 이미지 변경
+
+
+
+
+ {editingField === 'content' ? (
+
+
+ );
+};
+
+export default LpEdit;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/LpInfo.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/LpInfo.tsx
new file mode 100644
index 00000000..b96cf609
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/LpInfo.tsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import { LpDetailData } from '../../types/lp-detail';
+
+interface LpInfoProps {
+ lpData: LpDetailData;
+ isLiked: boolean | undefined;
+ onLike: () => void;
+ onDislike: () => void;
+ onEdit: () => void;
+ onDelete: () => void;
+}
+
+const LpInfo: React.FC = ({
+ lpData,
+ isLiked,
+ onLike,
+ onDislike,
+ onEdit,
+ onDelete,
+}) => {
+ return (
+
+
+
+
+ {lpData.author?.avatar ? (
+

+ ) : (
+ lpData.author?.name.charAt(0)
+ )}
+
+
+ {lpData.author?.name || '익명'}
+
+
+
+
+
+
+
+
+
+
{lpData.title}
+
+ {new Date(lpData.createdAt).toLocaleDateString()}
+
+
+
+
+
+

+
+
+
+
+
+
{lpData.content}
+
+
+ {lpData.tags.length > 0 ? (
+ lpData.tags.map((tag) => (
+
+ #{tag.name}
+
+ ))
+ ) : (
+ 태그 없음
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default LpInfo;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/comment-skeleton.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/comment-skeleton.tsx
new file mode 100644
index 00000000..c67eb435
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/comment-skeleton.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const CommentSkeleton: React.FC = () => (
+
+);
+
+export default CommentSkeleton;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/comment.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/comment.tsx
new file mode 100644
index 00000000..f3b81302
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/comment.tsx
@@ -0,0 +1,135 @@
+import React, { useState, useRef, useEffect } from 'react';
+import type { Comment } from '../../types/lp-detail';
+
+interface CommentProps {
+ comment: Comment;
+ isMyComment: boolean;
+ onUpdate: (id: number, content: string) => void;
+ onDelete: (id: number) => void;
+}
+
+const Comment: React.FC = ({
+ comment,
+ isMyComment,
+ onUpdate,
+ onDelete,
+}) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedContent, setEditedContent] = useState(comment.content);
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const menuRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
+ setIsMenuOpen(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const handleSubmitEdit = () => {
+ if (!editedContent.trim()) return;
+ onUpdate(comment.id, editedContent);
+ setIsEditing(false);
+ };
+
+ return (
+
+
+
+ {comment.author.avatar ? (
+

+ ) : (
+ comment.author.name.charAt(0)
+ )}
+
+
+
+
{comment.author.name}
+
+
+ {new Date(comment.createdAt).toLocaleDateString()}
+
+
+ {isMyComment && (
+
+
+
+ {isMenuOpen && (
+
+
+
+
+
+
+ )}
+
+ )}
+
+
+
+ {isEditing ? (
+ <>
+
+
+
+ );
+};
+
+export default Comment;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/lp-card-skeleton-list.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/lp-card-skeleton-list.tsx
new file mode 100644
index 00000000..82b79b53
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/lp-card-skeleton-list.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import LpCardSkeleton from './lp-card-skeleton';
+
+interface LpCardSkeletonListProps {
+ count: number;
+}
+
+const LpCardSkeletonList: React.FC = ({ count }) => {
+ return (
+ <>
+ {new Array(count).fill(0).map((_, idx: number) => (
+
+ ))}
+ >
+ );
+};
+
+export default LpCardSkeletonList;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/lp-card-skeleton.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/lp-card-skeleton.tsx
new file mode 100644
index 00000000..ee8af18c
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/lp-card-skeleton.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+const LpCardSkeleton: React.FC = () => {
+ return (
+
+ );
+};
+
+export default LpCardSkeleton;
diff --git a/Week8/wantkdd/mission1/src/components/lp-detail/lp-card.tsx b/Week8/wantkdd/mission1/src/components/lp-detail/lp-card.tsx
new file mode 100644
index 00000000..b4322eab
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/lp-detail/lp-card.tsx
@@ -0,0 +1,71 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+import { Lp } from '../../types/lp';
+
+interface LpCardProps {
+ lp: Lp;
+}
+
+const LpCard: React.FC = ({ lp }) => {
+ const navigate = useNavigate();
+ const { accessToken } = useAuth();
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ const handleImageLoad = () => {
+ setImageLoaded(true);
+ };
+ const handleCardClick = () => {
+ if (!accessToken) {
+ if (window.confirm('로그인이 필요합니다. 로그인 하시겠습니까?')) {
+ navigate('/login-page');
+ }
+ return;
+ }
+ navigate(`/lp/${lp.id}`);
+ };
+
+ return (
+
+
+
+ {!imageLoaded && (
+
+ )}
+

+
+
+
+
+
+
+
+
+ {lp.title}
+
+
+ {new Date(lp.createdAt).toLocaleDateString()}
+
+
+
+ ♥
+
+ {lp.likes?.length ?? 0}
+
+
+
+
+
+
+ );
+};
+
+export default LpCard;
diff --git a/Week8/wantkdd/mission1/src/components/navbar.tsx b/Week8/wantkdd/mission1/src/components/navbar.tsx
new file mode 100644
index 00000000..6bd1cda3
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/navbar.tsx
@@ -0,0 +1,81 @@
+import { useEffect, useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { useLogout } from '../hooks/mutations/user/useLogout';
+
+interface NavbarProps {
+ onMenuClick: () => void;
+}
+
+const Navbar = ({ onMenuClick }: NavbarProps) => {
+ const { accessToken, userInfo } = useAuth();
+ const { mutateAsync: logout } = useLogout();
+ const navigate = useNavigate();
+
+ const [userName, setUserName] = useState(userInfo?.name);
+
+ useEffect(() => {
+ if (userInfo?.name) {
+ setUserName(userInfo.name);
+ }
+ }, [userInfo]);
+
+ const handleLogout = async () => {
+ await logout();
+ navigate('/login-page');
+ };
+
+ return (
+
+ );
+};
+
+export default Navbar;
diff --git a/Week8/wantkdd/mission1/src/components/sidebar.tsx b/Week8/wantkdd/mission1/src/components/sidebar.tsx
new file mode 100644
index 00000000..1535a26a
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/components/sidebar.tsx
@@ -0,0 +1,117 @@
+import React, { useState } from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import useDeleteUser from '../hooks/mutations/user/useDeleteUser';
+
+interface SidebarProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+const Sidebar: React.FC = ({ open, onClose }) => {
+ const location = useLocation();
+ const { accessToken } = useAuth();
+ const [showModal, setShowModal] = useState(false);
+
+ const { mutate: deleteUserAccount, isPending } = useDeleteUser();
+
+ const isActive = (path: string) => {
+ return location.pathname === path;
+ };
+
+ const handleDeleteClick = () => {
+ setShowModal(true);
+ };
+
+ const handleConfirmDelete = () => {
+ deleteUserAccount();
+ setShowModal(false);
+ };
+
+ const handleCancelDelete = () => {
+ setShowModal(false);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ 🔍 찾기
+
+
+
+ 👤 마이페이지
+
+
+
+ {accessToken && (
+
+ )}
+
+
+
+ {showModal && (
+
+
+
+ 회원 탈퇴 확인
+
+
정말 탈퇴하시겠습니까?
+
+
+
+
+
+
+ )}
+ >
+ );
+};
+
+export default Sidebar;
diff --git a/Week8/wantkdd/mission1/src/constants/key.ts b/Week8/wantkdd/mission1/src/constants/key.ts
new file mode 100644
index 00000000..41326a26
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/constants/key.ts
@@ -0,0 +1,9 @@
+export const LOCAL_STORAGE_KEY = {
+ accessToken: 'accessToken',
+ refreshToken: 'refreshToken',
+};
+
+export const QUERY_KEY = {
+ lps: 'lps',
+ myInfo: 'myInfo',
+};
diff --git a/Week8/wantkdd/mission1/src/context/AuthContext.tsx b/Week8/wantkdd/mission1/src/context/AuthContext.tsx
new file mode 100644
index 00000000..b1d4444f
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/context/AuthContext.tsx
@@ -0,0 +1,138 @@
+import {
+ createContext,
+ PropsWithChildren,
+ useState,
+ useContext,
+ useEffect,
+} from 'react';
+import { RequestSigninDto } from '../types/auth';
+import { LOCAL_STORAGE_KEY } from '../constants/key';
+import { postLogout, postSignin } from '../apis/auth';
+import { useLocalStorage } from '../hooks/custom/useLocalStorage';
+import axios from 'axios';
+import { axiosInstance } from '../apis/axios';
+
+interface UserInfo {
+ id: number;
+ name: string;
+ email: string;
+ bio?: string | null;
+ avatar?: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+interface AuthContextType {
+ accessToken: string | null;
+ refreshToken: string | null;
+ userInfo: UserInfo | null;
+ setUserInfo: (userInfo: UserInfo) => void;
+ login: (signinData: RequestSigninDto) => Promise;
+ logout: () => Promise;
+}
+
+export const AuthContext = createContext({
+ accessToken: null,
+ refreshToken: null,
+ userInfo: null,
+ setUserInfo: () => {},
+ login: async () => {},
+ logout: async () => {},
+});
+
+export const AuthProvider = ({ children }: PropsWithChildren) => {
+ const {
+ getItem: getAccessTokenFromStorage,
+ setItem: setAccessTokenInStorage,
+ removeItem: removeAccessTokenFromStorage,
+ } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken);
+ const {
+ getItem: getRefreshTokenFromStorage,
+ setItem: setRefreshTokenInStorage,
+ removeItem: removeRefreshTokenFromStorage,
+ } = useLocalStorage(LOCAL_STORAGE_KEY.refreshToken);
+
+ const [accessToken, setAccessToken] = useState(
+ getAccessTokenFromStorage()
+ );
+ const [refreshToken, setRefreshToken] = useState(
+ getRefreshTokenFromStorage()
+ );
+ const [userInfo, setUserInfo] = useState(null);
+ useEffect(() => {
+ const fetchUserInfo = async () => {
+ if (accessToken) {
+ try {
+ const response = await axiosInstance.get('/v1/users/me');
+ setUserInfo(response.data.data);
+ } catch (error) {
+ console.error('사용자 정보를 가져오는데 실패했습니다.', error);
+ }
+ }
+ };
+
+ fetchUserInfo();
+ }, [accessToken]);
+
+ const login = async (signinData: RequestSigninDto) => {
+ try {
+ const { data } = await postSignin(signinData);
+ if (data) {
+ const newAccessToken = data.accessToken;
+ const newRefreshToken = data.refreshToken;
+
+ setAccessTokenInStorage(newAccessToken);
+ setRefreshTokenInStorage(newRefreshToken);
+
+ setAccessToken(newAccessToken);
+ setRefreshToken(newRefreshToken);
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response?.status === 401) {
+ alert('이메일 또는 비밀번호가 올바르지 않습니다.');
+ } else {
+ alert('로그인 중 오류가 발생했습니다.');
+ }
+ }
+ }
+ };
+
+ const logout = async () => {
+ try {
+ await postLogout();
+
+ removeAccessTokenFromStorage();
+ removeRefreshTokenFromStorage();
+
+ setAccessToken(null);
+ setRefreshToken(null);
+
+ alert('로그아웃 성공!');
+ } catch (error) {
+ console.error(error);
+ }
+ };
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('AuthContext 찾을 수 없음');
+ }
+ return context;
+};
diff --git a/Week8/wantkdd/mission1/src/enums/commons.ts b/Week8/wantkdd/mission1/src/enums/commons.ts
new file mode 100644
index 00000000..2190b698
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/enums/commons.ts
@@ -0,0 +1,4 @@
+export enum PAGINATION_ORDER {
+ 'asc' = 'asc',
+ 'desc' = 'desc',
+}
diff --git a/Week8/wantkdd/mission1/src/hooks/custom/useCustomFetch.ts b/Week8/wantkdd/mission1/src/hooks/custom/useCustomFetch.ts
new file mode 100644
index 00000000..42621d80
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/custom/useCustomFetch.ts
@@ -0,0 +1,59 @@
+import axios from 'axios';
+import { useState, useEffect } from 'react';
+
+interface ApiResponse {
+ data: T | null;
+ isLoading: boolean;
+ error: string | null;
+}
+
+interface UseFetchResult extends ApiResponse {
+ refetch: () => Promise;
+}
+
+export const useCustomFetch = (
+ url: string,
+ dependencies: React.DependencyList = []
+): UseFetchResult => {
+ const [state, setState] = useState>({
+ data: null,
+ isLoading: true,
+ error: null,
+ });
+
+ const fetchData = async () => {
+ setState((prev) => ({ ...prev, isLoading: true }));
+ try {
+ const response = await axios(url, {
+ headers: {
+ Authorization: `Bearer ${import.meta.env.VITE_TMDB_KEY}`,
+ },
+ });
+ setState({
+ data: response.data,
+ isLoading: false,
+ error: null,
+ });
+ } catch (err) {
+ setState({
+ data: null,
+ isLoading: false,
+ error: '🚨요청에 실패했습니다. 무언가가 잘못 된 것이 분명하다..🚨',
+ });
+ console.error(err);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, [...dependencies, url]);
+
+ const refetch = async () => {
+ await fetchData();
+ };
+
+ return {
+ ...state,
+ refetch,
+ };
+};
diff --git a/Week8/wantkdd/mission1/src/hooks/custom/useDebounce.ts b/Week8/wantkdd/mission1/src/hooks/custom/useDebounce.ts
new file mode 100644
index 00000000..322d5ea3
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/custom/useDebounce.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from 'react';
+
+function useDebounce(value: T, delay: number) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+ //value, delay 변경될 때마다 실행
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ //delay시간 이후 value를 debouncedValue로 업데이트하는 타이머 시작
+ setDebouncedValue(value);
+ }, delay);
+ //value 변경되면 기존 타이머를 지워서 업데이트 취소
+ //값이 계속 바뀔 때마다 마지막에 멈춘 값만 업데이트
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, delay]);
+ //최종적으로 잠시 기다린 후의 값을 반환
+ return debouncedValue;
+}
+
+export default useDebounce;
diff --git a/Week8/wantkdd/mission1/src/hooks/custom/useForm.ts b/Week8/wantkdd/mission1/src/hooks/custom/useForm.ts
new file mode 100644
index 00000000..e8202d71
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/custom/useForm.ts
@@ -0,0 +1,53 @@
+import { ChangeEvent, useEffect, useState } from 'react';
+
+interface UseFormProps {
+ initialValue: T;
+ validate: (values: T) => Record;
+}
+
+function useForm({ initialValue, validate }: UseFormProps) {
+ const [values, setValues] = useState(initialValue);
+ const [touched, setTouched] = useState>();
+ const [errors, setErrors] = useState>();
+
+ const handleChange = (name: keyof T, text: string) => {
+ setValues({
+ ...values,
+ [name]: text,
+ });
+ };
+
+ const handleBlur = (name: keyof T) => {
+ setTouched({
+ ...touched,
+ [name]: true,
+ });
+ };
+
+ const getInputProps = (name: keyof T) => {
+ const value = values[name];
+ const onChange = (e: ChangeEvent) =>
+ handleChange(name, e.target.value);
+
+ const onBlur = () => handleBlur(name);
+
+ return { value, onChange, onBlur };
+ };
+
+ useEffect(() => {
+ const someTouched = touched ? Object.values(touched).some((t) => t) : false;
+ if (someTouched) {
+ const newErrors = validate(values);
+ setErrors(newErrors);
+ }
+ }, [values, validate, touched]);
+
+ return {
+ values,
+ errors,
+ touched,
+ getInputProps,
+ };
+}
+
+export default useForm;
diff --git a/Week8/wantkdd/mission1/src/hooks/custom/useLocalStorage.ts b/Week8/wantkdd/mission1/src/hooks/custom/useLocalStorage.ts
new file mode 100644
index 00000000..a0058c67
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/custom/useLocalStorage.ts
@@ -0,0 +1,39 @@
+export const useLocalStorage = (key: string) => {
+ const setItem = (value: unknown) => {
+ try {
+ // 문자열이면 그대로 저장, 아니면 JSON.stringify 사용
+ const valueToStore =
+ typeof value === 'string' ? value : JSON.stringify(value);
+ window.localStorage.setItem(key, valueToStore);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ const getItem = () => {
+ try {
+ const item = window.localStorage.getItem(key);
+ if (!item) return null;
+
+ // 문자열인지 JSON인지 판단하여 적절히 처리
+ try {
+ return JSON.parse(item);
+ } catch {
+ // JSON 파싱에 실패하면 일반 문자열로 반환
+ return item;
+ }
+ } catch (error) {
+ console.log(error);
+ return null;
+ }
+ };
+
+ const removeItem = () => {
+ try {
+ window.localStorage.removeItem(key);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+ return { setItem, getItem, removeItem };
+};
diff --git a/Week8/wantkdd/mission1/src/hooks/custom/useLpComments.ts b/Week8/wantkdd/mission1/src/hooks/custom/useLpComments.ts
new file mode 100644
index 00000000..4995f78d
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/custom/useLpComments.ts
@@ -0,0 +1,82 @@
+import { useState } from 'react';
+import { useInView } from 'react-intersection-observer';
+import { useGetInfiniteComments } from '../queries/useGetInfiniteLpDetail';
+import usePostComment from '../mutations/comments/usePostComment';
+import useUpdateComment from '../mutations/comments/useUpdateComment';
+import useDeleteComment from '../mutations/comments/useDeleteComment';
+
+const useLpComments = (lpId: number) => {
+ const [commentOrder, setCommentOrder] = useState<'desc' | 'asc'>('desc');
+ const [commentContent, setCommentContent] = useState('');
+ const COMMENTS_PER_PAGE = 5;
+
+ const { ref, inView } = useInView({
+ threshold: 0.5,
+ triggerOnce: false,
+ });
+
+ const {
+ data: commentsData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading: isCommentsLoading,
+ error: commentsError,
+ refetch,
+ } = useGetInfiniteComments(lpId, COMMENTS_PER_PAGE, '', commentOrder);
+
+ const { mutate: postComment } = usePostComment(lpId);
+ const { mutate: updateComment } = useUpdateComment(lpId);
+ const { mutate: deleteCommentMutate } = useDeleteComment(lpId);
+
+ const handleSubmitComment = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!commentContent.trim()) return;
+
+ postComment(commentContent, {
+ onSuccess: () => {
+ setCommentContent('');
+ refetch();
+ },
+ });
+ };
+
+ const handleUpdateComment = (commentId: number, content: string) => {
+ updateComment(
+ { commentId, content },
+ {
+ onSuccess: () => {
+ refetch();
+ },
+ }
+ );
+ };
+
+ const handleDeleteComment = (commentId: number) => {
+ deleteCommentMutate(commentId, {
+ onSuccess: () => {
+ refetch();
+ },
+ });
+ };
+
+ return {
+ commentsData,
+ isCommentsLoading,
+ commentsError,
+ commentOrder,
+ setCommentOrder,
+ commentContent,
+ setCommentContent,
+ handleSubmitComment,
+ handleUpdateComment,
+ handleDeleteComment,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ ref,
+ inView,
+ };
+};
+
+export default useLpComments;
diff --git a/Week8/wantkdd/mission1/src/hooks/custom/useLpDetail.ts b/Week8/wantkdd/mission1/src/hooks/custom/useLpDetail.ts
new file mode 100644
index 00000000..9a0ae8ec
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/custom/useLpDetail.ts
@@ -0,0 +1,48 @@
+import { useState } from 'react';
+import { useGetLpDetail } from '../queries/useGetLpDetail';
+import useGetMyInfo from '../queries/useGetMyInfo';
+import usePostLike from '../mutations/like/usePostLike';
+import useDeleteLike from '../mutations/like/useDeleteLikte';
+import { useDeleteLp } from '../mutations/lp/useDeleteLp';
+
+const useLpDetail = (lpId: number, accessToken: string | null) => {
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [isPlaying, setIsPlaying] = useState(true);
+
+ const {
+ data: lpData,
+ isLoading: isLpLoading,
+ error: lpError,
+ } = useGetLpDetail(lpId);
+
+ const { data: me } = useGetMyInfo(accessToken);
+ const { mutate: likeMutate } = usePostLike();
+ const { mutate: disLikeMutate } = useDeleteLike();
+ const { mutate: deleteLp } = useDeleteLp();
+
+ const isLiked = lpData?.likes.some((like) => like.userId === me?.data.id);
+
+ const handleLikeLp = () => {
+ likeMutate(lpId);
+ };
+
+ const handleDislikeLp = () => {
+ disLikeMutate(lpId);
+ };
+
+ return {
+ lpData,
+ isLpLoading,
+ lpError,
+ isEditMode,
+ setIsEditMode,
+ isPlaying,
+ setIsPlaying,
+ handleLikeLp,
+ handleDislikeLp,
+ isLiked,
+ deleteLp,
+ };
+};
+
+export default useLpDetail;
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/comments/useDeleteComment.ts b/Week8/wantkdd/mission1/src/hooks/mutations/comments/useDeleteComment.ts
new file mode 100644
index 00000000..fcf73daf
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/comments/useDeleteComment.ts
@@ -0,0 +1,14 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deleteComment } from '../../../apis/lp-detail';
+
+const useDeleteComment = (lpId: number) => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (commentId: number) => deleteComment(lpId, commentId),
+ onSuccess: () => {
+ queryClient.invalidateQueries(['comments', lpId]);
+ },
+ });
+};
+
+export default useDeleteComment;
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/comments/usePostComment.ts b/Week8/wantkdd/mission1/src/hooks/mutations/comments/usePostComment.ts
new file mode 100644
index 00000000..658c993b
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/comments/usePostComment.ts
@@ -0,0 +1,15 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { createComment } from '../../../apis/lp-detail';
+
+const usePostComment = (lpId: number) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (content: string) => createComment(String(lpId), content),
+ onSuccess: () => {
+ queryClient.invalidateQueries(['comments', lpId]);
+ },
+ });
+};
+
+export default usePostComment;
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/comments/useUpdateComment.ts b/Week8/wantkdd/mission1/src/hooks/mutations/comments/useUpdateComment.ts
new file mode 100644
index 00000000..2ae1ea36
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/comments/useUpdateComment.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateComment } from '../../../apis/lp-detail';
+
+const useUpdateComment = (lpId: number) => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({
+ commentId,
+ content,
+ }: {
+ commentId: number;
+ content: string;
+ }) => updateComment(lpId, commentId, content),
+ onSuccess: () => {
+ queryClient.invalidateQueries(['comments', lpId]);
+ },
+ });
+};
+
+export default useUpdateComment;
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/like/useDeleteLikte.ts b/Week8/wantkdd/mission1/src/hooks/mutations/like/useDeleteLikte.ts
new file mode 100644
index 00000000..ff2793f8
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/like/useDeleteLikte.ts
@@ -0,0 +1,52 @@
+import { useMutation } from '@tanstack/react-query';
+import { deleteLike } from '../../../apis/like';
+import { queryClient } from '../../../App';
+import { QUERY_KEY } from '../../../constants/key';
+import { LpDetailData } from '../../../types/lp-detail';
+import useGetMyInfo from '../../queries/useGetMyInfo';
+import { useAuth } from '../../../context/AuthContext';
+
+function useDeleteLike() {
+ const { accessToken } = useAuth();
+ const { data: me } = useGetMyInfo(accessToken);
+
+ return useMutation({
+ mutationFn: deleteLike,
+
+ onMutate: async (lpId: number) => {
+ //Optimistic update
+ await queryClient.cancelQueries({ queryKey: [QUERY_KEY.lps, lpId] });
+ const previousData = queryClient.getQueryData([
+ QUERY_KEY.lps,
+ lpId,
+ ]);
+
+ if (previousData && me?.data.id) {
+ queryClient.setQueryData([QUERY_KEY.lps, lpId], {
+ ...previousData,
+ likes: previousData.likes.filter(
+ (like) => like.userId !== me.data.id
+ ),
+ });
+ }
+
+ return { previousData };
+ },
+
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.lps, data.data.lpId],
+ exact: true,
+ });
+ },
+ onError: (error, lpId, context) => {
+ console.error('좋아요 취소 실패:', error);
+
+ if (context?.previousData) {
+ queryClient.setQueryData([QUERY_KEY.lps, lpId], context.previousData);
+ }
+ },
+ });
+}
+
+export default useDeleteLike;
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/like/usePostLike.ts b/Week8/wantkdd/mission1/src/hooks/mutations/like/usePostLike.ts
new file mode 100644
index 00000000..b3b2590d
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/like/usePostLike.ts
@@ -0,0 +1,50 @@
+import { useMutation } from '@tanstack/react-query';
+import { postLike } from '../../../apis/like';
+import { QUERY_KEY } from '../../../constants/key';
+import { queryClient } from '../../../App';
+import { LpDetailData } from '../../../types/lp-detail';
+import useGetMyInfo from '../../queries/useGetMyInfo';
+import { useAuth } from '../../../context/AuthContext';
+
+function usePostLike() {
+ const { accessToken } = useAuth();
+ const { data: me } = useGetMyInfo(accessToken);
+
+ return useMutation({
+ mutationFn: postLike,
+
+ onMutate: async (lpId: number) => {
+ await queryClient.cancelQueries({ queryKey: [QUERY_KEY.lps, lpId] });
+
+ const previousData = queryClient.getQueryData([
+ QUERY_KEY.lps,
+ lpId,
+ ]);
+
+ if (previousData && me?.data.id) {
+ queryClient.setQueryData([QUERY_KEY.lps, lpId], {
+ ...previousData,
+ likes: [
+ ...previousData.likes,
+ {
+ id: Date.now(),
+ userId: me.data.id,
+ lpId: lpId,
+ },
+ ],
+ });
+ }
+
+ return { previousData };
+ },
+
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.lps, data.data.lpId],
+ exact: true,
+ });
+ },
+ });
+}
+
+export default usePostLike;
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/lp/useDeleteLp.ts b/Week8/wantkdd/mission1/src/hooks/mutations/lp/useDeleteLp.ts
new file mode 100644
index 00000000..76ff1350
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/lp/useDeleteLp.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deleteLp } from '../../../apis/lp';
+import { QUERY_KEY } from '../../../constants/key';
+import { useNavigate } from 'react-router-dom';
+
+export const useDeleteLp = () => {
+ const queryClient = useQueryClient();
+ const navigate = useNavigate();
+
+ return useMutation({
+ mutationFn: (lpId: number) => deleteLp(lpId),
+ onSuccess: (_, lpId) => {
+ queryClient.removeQueries({
+ queryKey: [QUERY_KEY.lps, lpId],
+ });
+
+ navigate('/');
+ },
+ });
+};
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/lp/usePostLp.ts b/Week8/wantkdd/mission1/src/hooks/mutations/lp/usePostLp.ts
new file mode 100644
index 00000000..4b87463c
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/lp/usePostLp.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { postLp } from '../../../apis/addLp';
+import { QUERY_KEY } from '../../../constants/key';
+
+export default function usePostLp() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: postLp,
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY.lps] });
+
+ if (data?.data?.id) {
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.lps, data.data.id],
+ });
+ }
+ },
+ });
+}
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/lp/useUpdateLp.ts b/Week8/wantkdd/mission1/src/hooks/mutations/lp/useUpdateLp.ts
new file mode 100644
index 00000000..899e95d0
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/lp/useUpdateLp.ts
@@ -0,0 +1,23 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateLp } from '../../../apis/lp-detail';
+import { UpdateLpRequest } from '../../../types/lp-detail';
+
+export const useUpdateLp = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ lpId,
+ payload,
+ }: {
+ lpId: number;
+ payload: UpdateLpRequest;
+ }) => updateLp({ lpId, payload }),
+
+ onSuccess: (_data, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ['lpDetail', variables.lpId],
+ });
+ },
+ });
+};
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/user/useDeleteUser.ts b/Week8/wantkdd/mission1/src/hooks/mutations/user/useDeleteUser.ts
new file mode 100644
index 00000000..a0a7b26c
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/user/useDeleteUser.ts
@@ -0,0 +1,20 @@
+import { useMutation } from '@tanstack/react-query';
+import { useNavigate } from 'react-router-dom';
+import { deleteUser } from '../../../apis/auth';
+import { useAuth } from '../../../context/AuthContext';
+
+const useDeleteUser = () => {
+ const navigate = useNavigate();
+ const { logout } = useAuth();
+
+ return useMutation({
+ mutationFn: deleteUser,
+ onSuccess: async () => {
+ alert('회원 탈퇴가 완료되었습니다.');
+ await logout();
+ navigate('/');
+ },
+ });
+};
+
+export default useDeleteUser;
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/user/useLogin.ts b/Week8/wantkdd/mission1/src/hooks/mutations/user/useLogin.ts
new file mode 100644
index 00000000..f7eab0c7
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/user/useLogin.ts
@@ -0,0 +1,21 @@
+import { useMutation } from '@tanstack/react-query';
+import { SigninFormFields } from '../../../utils/validate';
+import { useAuth } from '../../../context/AuthContext';
+import axios from 'axios';
+
+export const useLogin = () => {
+ const { login } = useAuth();
+
+ return useMutation({
+ mutationFn: (data: SigninFormFields) => {
+ return login(data);
+ },
+ onError: (error) => {
+ if (axios.isAxiosError(error) && error.response?.status === 401) {
+ alert('이메일 또는 비밀번호가 올바르지 않습니다.');
+ } else {
+ alert('로그인 중 오류가 발생했습니다.');
+ }
+ },
+ });
+};
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/user/useLogout.ts b/Week8/wantkdd/mission1/src/hooks/mutations/user/useLogout.ts
new file mode 100644
index 00000000..cccfd892
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/user/useLogout.ts
@@ -0,0 +1,13 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { useAuth } from '../../../context/AuthContext';
+
+export const useLogout = () => {
+ const { logout } = useAuth();
+
+ return useMutation({
+ mutationFn: () => {
+ return logout();
+ },
+ });
+};
diff --git a/Week8/wantkdd/mission1/src/hooks/mutations/user/useUpdateMyInfo.ts b/Week8/wantkdd/mission1/src/hooks/mutations/user/useUpdateMyInfo.ts
new file mode 100644
index 00000000..6a5d7446
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/mutations/user/useUpdateMyInfo.ts
@@ -0,0 +1,57 @@
+import { useMutation } from '@tanstack/react-query';
+import { patchUserInfo } from '../../../apis/auth';
+import { queryClient } from '../../../App';
+import { QUERY_KEY } from '../../../constants/key';
+import { PatchUserInfoDto, ResponseMyInfoDto } from '../../../types/auth';
+import { useAuth } from '../../../context/AuthContext';
+
+function useUpdateUserInfo() {
+ const { setUserInfo } = useAuth();
+
+ return useMutation({
+ mutationFn: (body: PatchUserInfoDto) => patchUserInfo(body),
+
+ onMutate: async (newUserData: PatchUserInfoDto) => {
+ await queryClient.cancelQueries({ queryKey: [QUERY_KEY.myInfo] });
+
+ const previousUserInfo = queryClient.getQueryData([
+ QUERY_KEY.myInfo,
+ ]);
+
+ if (previousUserInfo) {
+ queryClient.setQueryData([QUERY_KEY.myInfo], {
+ ...previousUserInfo,
+ data: {
+ ...previousUserInfo.data,
+ name: newUserData.name,
+ bio: newUserData.bio || previousUserInfo.data.bio,
+ avatar: newUserData.avatar || previousUserInfo.data.avatar,
+ },
+ });
+
+ setUserInfo({
+ ...previousUserInfo.data,
+ name: newUserData.name,
+ bio: newUserData.bio || previousUserInfo.data.bio,
+ avatar: newUserData.avatar || previousUserInfo.data.avatar,
+ });
+ }
+
+ return { previousUserInfo };
+ },
+
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY.myInfo] });
+ },
+ onError: (error, _, context) => {
+ console.error('사용자 정보 업데이트 실패:', error);
+
+ if (context?.previousUserInfo) {
+ queryClient.setQueryData([QUERY_KEY.myInfo], context.previousUserInfo);
+ setUserInfo(context.previousUserInfo.data);
+ }
+ },
+ });
+}
+
+export default useUpdateUserInfo;
diff --git a/Week8/wantkdd/mission1/src/hooks/queries/useGetInfiniteLpDetail.ts b/Week8/wantkdd/mission1/src/hooks/queries/useGetInfiniteLpDetail.ts
new file mode 100644
index 00000000..76e0ad55
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/queries/useGetInfiniteLpDetail.ts
@@ -0,0 +1,29 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { getComments } from '../../apis/lp-detail';
+import { QUERY_KEY } from '../../constants/key';
+import { CommentsResponse } from '../../types/lp-detail';
+
+export function useGetInfiniteComments(
+ lpId: number,
+ limit: number,
+ search: string,
+ order: 'desc' | 'asc'
+) {
+ return useInfiniteQuery({
+ queryKey: [QUERY_KEY.lps, lpId, 'comments', order, limit],
+ queryFn: ({ pageParam }) =>
+ getComments({
+ lpId,
+ cursor: pageParam,
+ limit,
+ order,
+ search,
+ }),
+ initialPageParam: 0,
+ getNextPageParam: (lastPage) =>
+ lastPage.data.hasNext ? lastPage.data.nextCursor : undefined,
+ staleTime: 1000 * 60 * 5,
+ gcTime: 1000 * 60 * 10,
+ enabled: !!lpId,
+ });
+}
diff --git a/Week8/wantkdd/mission1/src/hooks/queries/useGetInfiniteLpList.ts b/Week8/wantkdd/mission1/src/hooks/queries/useGetInfiniteLpList.ts
new file mode 100644
index 00000000..f7640b94
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/queries/useGetInfiniteLpList.ts
@@ -0,0 +1,23 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { getLpList } from '../../apis/lp';
+import { QUERY_KEY } from '../../constants/key';
+
+function useGetInfiniteLpList(
+ limit: number,
+ order: 'desc' | 'asc',
+ search?: string
+) {
+ return useInfiniteQuery({
+ queryFn: ({ pageParam }) =>
+ getLpList({ cursor: pageParam, limit, order, search }),
+ queryKey: [QUERY_KEY.lps, order, limit, search],
+ initialPageParam: 0,
+ getNextPageParam: (lastPage) => {
+ return lastPage.data.hasNext ? lastPage.data.nextCursor : undefined;
+ },
+ staleTime: 1000 * 60 * 5,
+ gcTime: 1000 * 60 * 10,
+ });
+}
+
+export default useGetInfiniteLpList;
diff --git a/Week8/wantkdd/mission1/src/hooks/queries/useGetLpDetail.ts b/Week8/wantkdd/mission1/src/hooks/queries/useGetLpDetail.ts
new file mode 100644
index 00000000..a1e90017
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/queries/useGetLpDetail.ts
@@ -0,0 +1,12 @@
+import { useQuery } from '@tanstack/react-query';
+import { getLpDetail } from '../../apis/lp-detail';
+import { QUERY_KEY } from '../../constants/key';
+import { LpDetailData } from '../../types/lp-detail';
+
+export function useGetLpDetail(id: number) {
+ return useQuery({
+ queryKey: [QUERY_KEY.lps, id],
+ queryFn: () => getLpDetail(id),
+ staleTime: 1000 * 60 * 5,
+ });
+}
diff --git a/Week8/wantkdd/mission1/src/hooks/queries/useGetLpList.ts b/Week8/wantkdd/mission1/src/hooks/queries/useGetLpList.ts
new file mode 100644
index 00000000..6c5a59d1
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/queries/useGetLpList.ts
@@ -0,0 +1,23 @@
+import { useQuery } from '@tanstack/react-query';
+import { PaginationDto } from '../../types/common';
+import { getLpList } from '../../apis/lp';
+import { QUERY_KEY } from '../../constants/key';
+
+import { ResponseLpListDto } from '../../types/lp';
+
+function useGetLpList({
+ order = 'desc',
+ limit = 40,
+ search = '',
+}: PaginationDto) {
+ return useQuery({
+ queryKey: [QUERY_KEY.lps, order, limit, search],
+ queryFn: () => getLpList({ order, limit, search }),
+ staleTime: 1000 * 60 * 5,
+ gcTime: 1000 * 60 * 10,
+ retry: 3,
+ select: (data: ResponseLpListDto) => data.data.data,
+ });
+}
+
+export default useGetLpList;
diff --git a/Week8/wantkdd/mission1/src/hooks/queries/useGetMyInfo.ts b/Week8/wantkdd/mission1/src/hooks/queries/useGetMyInfo.ts
new file mode 100644
index 00000000..4ed74bc2
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/queries/useGetMyInfo.ts
@@ -0,0 +1,14 @@
+import { useQuery } from '@tanstack/react-query';
+import { QUERY_KEY } from '../../constants/key';
+
+import { getMyInfo } from '../../apis/auth';
+
+function useGetMyInfo(accessToken: string | null) {
+ return useQuery({
+ queryKey: [QUERY_KEY.myInfo],
+ queryFn: getMyInfo,
+ enabled: !!accessToken,
+ });
+}
+
+export default useGetMyInfo;
diff --git a/Week8/wantkdd/mission1/src/hooks/queries/useSearchLp.ts b/Week8/wantkdd/mission1/src/hooks/queries/useSearchLp.ts
new file mode 100644
index 00000000..1542d091
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/hooks/queries/useSearchLp.ts
@@ -0,0 +1,5 @@
+import useGetInfiniteLpList from './useGetInfiniteLpList';
+
+export function useSearchLps(search: string) {
+ return useGetInfiniteLpList(10, 'desc', search);
+}
diff --git a/Week8/wantkdd/mission1/src/index.css b/Week8/wantkdd/mission1/src/index.css
new file mode 100644
index 00000000..ca24e35c
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/index.css
@@ -0,0 +1,11 @@
+@import 'tailwindcss';
+@layer utilities {
+ .animate-spin-slow {
+ animation: spin 3s linear infinite;
+ }
+}
+@layer utilities {
+ .animate-spin-slow {
+ animation: spin 3s linear infinite;
+ }
+}
diff --git a/Week8/wantkdd/mission1/src/layout/ProtectedLayout.tsx b/Week8/wantkdd/mission1/src/layout/ProtectedLayout.tsx
new file mode 100644
index 00000000..3db94f82
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/layout/ProtectedLayout.tsx
@@ -0,0 +1,18 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import RootLayout from './root-layout';
+
+const ProtectedLayout = () => {
+ const { accessToken } = useAuth();
+ if (!accessToken) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default ProtectedLayout;
diff --git a/Week8/wantkdd/mission1/src/layout/root-layout.tsx b/Week8/wantkdd/mission1/src/layout/root-layout.tsx
new file mode 100644
index 00000000..fca461f1
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/layout/root-layout.tsx
@@ -0,0 +1,32 @@
+import { Outlet } from 'react-router-dom';
+import Navbar from '../components/navbar.tsx';
+import { PropsWithChildren, useState } from 'react';
+import Footer from '../components/footer.tsx';
+import Sidebar from '../components/sidebar.tsx';
+import AddButton from '../components/add-lp/addButton.tsx';
+
+const RootLayout: React.FC = ({ children }) => {
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+
+ const toggleSidebar = () => setSidebarOpen((prev) => !prev);
+
+ return (
+
+
+
+
+
setSidebarOpen(false)} />
+
+ {children || }
+
+
+
+
+
+
+ );
+};
+
+export default RootLayout;
diff --git a/Week8/wantkdd/mission1/src/main.tsx b/Week8/wantkdd/mission1/src/main.tsx
new file mode 100644
index 00000000..df655eae
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import './index.css';
+import App from './App.tsx';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/Week8/wantkdd/mission1/src/pages/detail-page.tsx b/Week8/wantkdd/mission1/src/pages/detail-page.tsx
new file mode 100644
index 00000000..ca385420
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/detail-page.tsx
@@ -0,0 +1,115 @@
+import React, { useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import useLpDetail from '../hooks/custom/useLpDetail';
+import useLpComments from '../hooks/custom/useLpComments';
+import LpInfo from '../components/lp-detail/LpInfo';
+import LpEdit from '../components/lp-detail/LpEdit';
+import LpComments from '../components/lp-detail/LpComments';
+import DeleteModal from '../components/lp-detail/DeleteModal';
+
+const LpDetailPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ const { accessToken } = useAuth();
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const lpId = Number(id);
+
+ const {
+ lpData,
+ isLpLoading,
+ lpError,
+ isEditMode,
+ setIsEditMode,
+ handleLikeLp,
+ handleDislikeLp,
+ isLiked,
+ deleteLp,
+ } = useLpDetail(lpId, accessToken);
+
+ const {
+ commentsData,
+ isCommentsLoading,
+ commentsError,
+ commentOrder,
+ setCommentOrder,
+ commentContent,
+ setCommentContent,
+ handleSubmitComment,
+ handleUpdateComment,
+ handleDeleteComment,
+ hasNextPage,
+ isFetchingNextPage,
+ ref,
+ } = useLpComments(lpId);
+
+ if (isLpLoading) {
+ return (
+
+
로딩 중...
+
+ );
+ }
+
+ if (lpError || !lpData) {
+ return (
+
+
LP를 불러오지 못했습니다.
+
+ );
+ }
+
+ const handleDeleteClick = () => setShowDeleteModal(true);
+ const handleCancelDelete = () => setShowDeleteModal(false);
+ const handleConfirmDelete = () => deleteLp(lpId);
+
+ const comments = commentsData?.pages.flatMap((page) => page.data.data) || [];
+
+ return (
+
+
+ {isEditMode ? (
+ setIsEditMode(false)}
+ onDelete={handleDeleteClick}
+ />
+ ) : (
+ setIsEditMode(true)}
+ onDelete={handleDeleteClick}
+ />
+ )}
+
+
+
+ {showDeleteModal && (
+
+ )}
+
+
+ );
+};
+
+export default LpDetailPage;
diff --git a/Week8/wantkdd/mission1/src/pages/googleLoginRedirect-page.tsx b/Week8/wantkdd/mission1/src/pages/googleLoginRedirect-page.tsx
new file mode 100644
index 00000000..5eec08de
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/googleLoginRedirect-page.tsx
@@ -0,0 +1,27 @@
+import { useEffect } from 'react';
+import { useLocalStorage } from '../hooks/custom/useLocalStorage';
+import { LOCAL_STORAGE_KEY } from '../constants/key';
+
+const GoogleLoginRedirectPage = () => {
+ const { setItem: setAccessToken } = useLocalStorage(
+ LOCAL_STORAGE_KEY.accessToken
+ );
+ const { setItem: setRefreshToken } = useLocalStorage(
+ LOCAL_STORAGE_KEY.refreshToken
+ );
+
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const accessToken = urlParams.get(LOCAL_STORAGE_KEY.accessToken);
+ const refreshToken = urlParams.get(LOCAL_STORAGE_KEY.refreshToken);
+
+ if (accessToken) {
+ setAccessToken(accessToken);
+ setRefreshToken(refreshToken);
+ window.location.href = '/my-page';
+ }
+ }, [setAccessToken, setRefreshToken]);
+ return 구글 리다이렉 화면
;
+};
+
+export default GoogleLoginRedirectPage;
diff --git a/Week8/wantkdd/mission1/src/pages/home-page.tsx b/Week8/wantkdd/mission1/src/pages/home-page.tsx
new file mode 100644
index 00000000..b756b823
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/home-page.tsx
@@ -0,0 +1,92 @@
+import React, { useEffect, useState } from 'react';
+import { useInView } from 'react-intersection-observer';
+import useGetInfiniteLpList from '../hooks/queries/useGetInfiniteLpList';
+import LpCard from '../components/lp-detail/lp-card';
+import LpCardSkeletonList from '../components/lp-detail/lp-card-skeleton-list';
+
+const HomePage: React.FC = () => {
+ const [order, setOrder] = useState<'desc' | 'asc'>('desc');
+
+ const { ref, inView } = useInView({
+ threshold: 0,
+ });
+
+ const {
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading,
+ status,
+ error,
+ refetch,
+ } = useGetInfiniteLpList(20, order, '');
+
+ useEffect(() => {
+ refetch();
+ }, [order, refetch]);
+
+ useEffect(() => {
+ if (inView && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
+
+ const allLps = data?.pages.flatMap((page) => page.data.data) || [];
+
+ if (status === 'error') {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {allLps.length === 0 ? (
+
+ LP가 없습니다.
+
+ ) : (
+ <>
+ {allLps.map((lp) => (
+
+ ))}
+
+ {!isLoading &&
}
+ >
+ )}
+
+
+
+ );
+};
+
+export default HomePage;
diff --git a/Week8/wantkdd/mission1/src/pages/login-page.tsx b/Week8/wantkdd/mission1/src/pages/login-page.tsx
new file mode 100644
index 00000000..26650f85
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/login-page.tsx
@@ -0,0 +1,43 @@
+import { Link, useNavigate } from 'react-router-dom';
+import { useEffect } from 'react';
+import { useAuth } from '../context/AuthContext';
+import GoogleLoginButton from '../components/googleLogin';
+import LoginForm from '../components/login-form';
+
+const LoginPage = () => {
+ const { accessToken } = useAuth();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (accessToken) {
+ navigate('/my-page');
+ }
+ }, [accessToken, navigate]);
+
+ return (
+
+
+
+
+ <
+
+
+ 로그인
+
+
+
+
+
+
+
+ OR
+
+
+
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/Week8/wantkdd/mission1/src/pages/my-page.tsx b/Week8/wantkdd/mission1/src/pages/my-page.tsx
new file mode 100644
index 00000000..4486c266
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/my-page.tsx
@@ -0,0 +1,188 @@
+import { useEffect, useState, useRef } from 'react';
+import { getMyInfo } from '../apis/auth';
+import { ResponseMyInfoDto } from '../types/auth';
+import useUpdateUserInfo from '../hooks/mutations/user/useUpdateMyInfo';
+import { useAuth } from '../context/AuthContext';
+
+const MyPage = () => {
+ const [data, setData] = useState({} as ResponseMyInfoDto);
+ const [editMode, setEditMode] = useState(false);
+ const [name, setName] = useState('');
+ const [bio, setBio] = useState('');
+ const [avatar, setAvatar] = useState('');
+ const [previewAvatar, setPreviewAvatar] = useState('');
+ const fileInputRef = useRef(null);
+
+ const { mutate: updateUserInfo, isPending } = useUpdateUserInfo();
+ const { setUserInfo } = useAuth();
+
+ useEffect(() => {
+ const getData = async () => {
+ try {
+ const response = await getMyInfo();
+ setData(response);
+
+ setName(response.data.name);
+ setBio(response.data.bio || '');
+ setAvatar(response.data.avatar || '');
+ setPreviewAvatar(response.data.avatar || '');
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ getData();
+ }, []);
+
+ const handleAvatarClick = () => {
+ if (editMode && fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const result = reader.result as string;
+ setPreviewAvatar(result);
+ setAvatar(result);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleSubmit = () => {
+ if (!name.trim()) {
+ alert('이름은 빈칸일 수 없습니다.');
+ return;
+ }
+
+ updateUserInfo(
+ { name, bio: bio || undefined, avatar: avatar || undefined },
+ {
+ onSuccess: () => {
+ alert('정보가 수정되었습니다.');
+ setData((prev) => ({
+ ...prev,
+ data: {
+ ...prev.data,
+ name,
+ bio: bio || '',
+ avatar: avatar || '',
+ },
+ }));
+ setUserInfo({
+ ...data.data,
+ name,
+ bio: bio || null,
+ avatar: avatar || null,
+ });
+ setEditMode(false);
+ },
+ }
+ );
+ };
+
+ return (
+
+
+
+
+
+ {previewAvatar ? (
+

+ ) : (
+
없음
+ )}
+ {editMode && (
+
+ )}
+
+
+
+
+ {!editMode ? (
+ <>
+
+ {data.data?.name}님 환영합니다
+
+
+
이메일
+
{data.data?.email}
+
+
+
소개
+
{data.data?.bio || '소개가 없습니다.'}
+
+
+ >
+ ) : (
+ <>
+
+
+ setName(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default MyPage;
diff --git a/Week8/wantkdd/mission1/src/pages/search-page.tsx b/Week8/wantkdd/mission1/src/pages/search-page.tsx
new file mode 100644
index 00000000..11926591
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/search-page.tsx
@@ -0,0 +1,90 @@
+import React, { useState, useEffect } from 'react';
+import { useInView } from 'react-intersection-observer';
+import useDebounce from '../hooks/custom/useDebounce';
+import useGetInfiniteLpList from '../hooks/queries/useGetInfiniteLpList';
+import LpCard from '../components/lp-detail/lp-card';
+import LpCardSkeletonList from '../components/lp-detail/lp-card-skeleton-list';
+
+const SearchPage: React.FC = () => {
+ const [search, setSearch] = useState('');
+ const [order, setOrder] = useState<'desc' | 'asc'>('desc');
+ const debouncedSearch = useDebounce(search, 500);
+ const { ref, inView } = useInView({ threshold: 0 });
+
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
+ useGetInfiniteLpList(20, order, debouncedSearch);
+
+ useEffect(() => {
+ if (inView && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
+
+ const lps = data?.pages.flatMap((page) => page.data.data) || [];
+
+ return (
+
+
LP 검색
+
+
+ setSearch(e.target.value)}
+ placeholder="검색어를 입력하세요"
+ className="w-full px-4 py-2 rounded-md bg-gray-700 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-500"
+ />
+
+
+
+
+
+
+
+
+
+
+ {debouncedSearch === '' ? (
+
+ 검색어를 입력해주세요.
+
+ ) : isLoading ? (
+
+ ) : lps.length === 0 ? (
+
+ 검색 결과가 없습니다.
+
+ ) : (
+ <>
+ {lps.map((lp) => (
+
+ ))}
+ {isFetchingNextPage &&
}
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default SearchPage;
diff --git a/Week8/wantkdd/mission1/src/pages/signup-page.tsx b/Week8/wantkdd/mission1/src/pages/signup-page.tsx
new file mode 100644
index 00000000..06d26325
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/signup-page.tsx
@@ -0,0 +1,125 @@
+import { z } from 'zod';
+import {
+ SubmitHandler,
+ useForm,
+ UseFormRegister,
+ FieldErrors,
+} from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { postSignup } from '../apis/auth';
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { signupSchema } from '../utils/validate';
+import EmailStep from './signup/email-step';
+import PasswordStep from './signup/password-step';
+import NameStep from './signup/name-step';
+
+export type FormFields = z.infer;
+
+export type StepProps = {
+ register: UseFormRegister;
+ errors: FieldErrors;
+ watchedValues: FormFields;
+};
+
+const SignupPage = () => {
+ const [step, setStep] = useState(1);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showPasswordCheck, setShowPasswordCheck] = useState(false);
+
+ const togglePassword = () => setShowPassword((prev) => !prev);
+ const togglePasswordCheck = () => setShowPasswordCheck((prev) => !prev);
+
+ const navigate = useNavigate();
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ formState: { errors, isSubmitting },
+ trigger,
+ } = useForm({
+ defaultValues: {
+ name: '',
+ email: '',
+ password: '',
+ passwordCheck: '',
+ },
+ resolver: zodResolver(signupSchema),
+ mode: 'onBlur',
+ });
+
+ const watchedValues = watch();
+
+ const stepValidationFields = {
+ 1: ['email'] as const,
+ 2: ['password', 'passwordCheck'] as const,
+ 3: ['name'] as const,
+ };
+
+ const onSubmit: SubmitHandler = async (data) => {
+ try {
+ const response = await postSignup(data);
+ if (response) {
+ navigate('/login-page');
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const nextStep = async () => {
+ const fieldsToValidate =
+ stepValidationFields[step as keyof typeof stepValidationFields];
+ const isStepValid = await trigger(fieldsToValidate);
+
+ if (isStepValid) {
+ setStep((prev) => prev + 1);
+ }
+ };
+ const prevStep = () => {
+ setStep((prev) => (prev - 1 < 1 ? 1 : prev - 1));
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default SignupPage;
diff --git a/Week8/wantkdd/mission1/src/pages/signup/email-step.tsx b/Week8/wantkdd/mission1/src/pages/signup/email-step.tsx
new file mode 100644
index 00000000..27b487ec
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/signup/email-step.tsx
@@ -0,0 +1,40 @@
+import { StepProps } from '../signup-page';
+
+type EmailStepProps = StepProps & {
+ nextStep: () => void;
+};
+const ErrorMessage = ({ message }: { message: string | undefined }) => (
+ {message}
+);
+
+const EmailStep = ({
+ register,
+ errors,
+ watchedValues,
+ nextStep,
+}: EmailStepProps) => (
+ <>
+
+
+ {errors?.email && }
+
+
+ >
+);
+
+export default EmailStep;
diff --git a/Week8/wantkdd/mission1/src/pages/signup/name-step.tsx b/Week8/wantkdd/mission1/src/pages/signup/name-step.tsx
new file mode 100644
index 00000000..8e793d99
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/signup/name-step.tsx
@@ -0,0 +1,58 @@
+import { StepProps } from '../signup-page';
+
+type NameStepProps = StepProps & {
+ isSubmitting: boolean;
+ prevStep: () => void;
+ onSubmit: () => void;
+};
+
+const ErrorMessage = ({ message }: { message: string | undefined }) => (
+ {message}
+);
+
+const NameStep = ({
+ register,
+ errors,
+ watchedValues,
+ isSubmitting,
+ prevStep,
+ onSubmit,
+}: NameStepProps) => (
+ <>
+
+
이메일: {watchedValues.email}
+
비밀번호: {watchedValues.password}
+
+
+
+ {errors?.name && }
+
+
+
+
+
+
+ >
+);
+
+export default NameStep;
diff --git a/Week8/wantkdd/mission1/src/pages/signup/password-step.tsx b/Week8/wantkdd/mission1/src/pages/signup/password-step.tsx
new file mode 100644
index 00000000..3cfb5143
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/pages/signup/password-step.tsx
@@ -0,0 +1,99 @@
+import { FiEye, FiEyeOff } from 'react-icons/fi';
+import { StepProps } from '../signup-page';
+
+type PasswordStepProps = StepProps & {
+ showPassword: boolean;
+ showPasswordCheck: boolean;
+ togglePassword: () => void;
+ togglePasswordCheck: () => void;
+ nextStep: () => void;
+ prevStep: () => void;
+};
+
+const ErrorMessage = ({ message }: { message: string | undefined }) => (
+ {message}
+);
+
+const PasswordStep = ({
+ register,
+ errors,
+ watchedValues,
+ showPassword,
+ showPasswordCheck,
+ togglePassword,
+ togglePasswordCheck,
+ nextStep,
+ prevStep,
+}: PasswordStepProps) => (
+ <>
+
+
이메일: {watchedValues.email}
+
+
+
+
+ {errors?.password && }
+
+
+
+
+
+ {errors?.passwordCheck && (
+
+ )}
+
+
+
+
+
+
+ >
+);
+
+export default PasswordStep;
diff --git a/Week8/wantkdd/mission1/src/types/auth.ts b/Week8/wantkdd/mission1/src/types/auth.ts
new file mode 100644
index 00000000..fdf5ec18
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/types/auth.ts
@@ -0,0 +1,46 @@
+import { CommonRespense } from './common';
+
+export type RequestSignupDto = {
+ name: string;
+ email: string;
+ bio?: string;
+ avatar?: string;
+ password: string;
+};
+
+export type ResponseSignupDto = CommonRespense<{
+ id: number;
+ name: string;
+ email: string;
+ bio: null | string;
+ avatar: null | string;
+ createdAt: Date;
+ updatedAt: Date;
+}>;
+
+export type RequestSigninDto = {
+ email: string;
+ password: string;
+};
+export type ResponseSigninDto = CommonRespense<{
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+}>;
+
+export type ResponseMyInfoDto = CommonRespense<{
+ id: number;
+ name: string;
+ email: string;
+ bio: null | string;
+ avatar: null | string;
+ createdAt: Date;
+ updatedAt: Date;
+}>;
+
+export type PatchUserInfoDto = {
+ name: string;
+ bio?: string;
+ avatar?: string;
+};
diff --git a/Week8/wantkdd/mission1/src/types/common.ts b/Week8/wantkdd/mission1/src/types/common.ts
new file mode 100644
index 00000000..d0d37369
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/types/common.ts
@@ -0,0 +1,20 @@
+// import { PAGINATION_ORDER } from '../enums/commons';
+
+export type CommonRespense = {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: T;
+};
+
+export type PaginationDto = {
+ cursor?: number;
+ limit?: number;
+ search?: string;
+ order?: 'asc' | 'desc';
+};
+export type CursorBasedResponse = CommonRespense<{
+ data: T;
+ nextCursor: number | null;
+ hasNext: boolean;
+}>;
diff --git a/Week8/wantkdd/mission1/src/types/lp-detail.ts b/Week8/wantkdd/mission1/src/types/lp-detail.ts
new file mode 100644
index 00000000..77fab240
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/types/lp-detail.ts
@@ -0,0 +1,54 @@
+import { Lp } from './lp';
+import { CommonRespense, CursorBasedResponse } from './common';
+
+export interface Author {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface LpDetailData extends Lp {
+ author?: Author;
+}
+
+export type LpDetailResponse = CommonRespense;
+
+export interface CommentAuthor {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Comment {
+ id: number;
+ content: string;
+ lpId: number;
+ authorId: number;
+ createdAt: string;
+ updatedAt: string;
+ author: CommentAuthor;
+}
+
+export type CommentsResponse = CursorBasedResponse;
+
+export type ResponseLikeLpDto = CommonRespense<{
+ id: number;
+ userId: number;
+ lpId: number;
+}>;
+
+export interface UpdateLpRequest {
+ title?: string;
+ content?: string;
+ thumbnail?: string;
+ tags?: string[];
+ published?: boolean;
+}
diff --git a/Week8/wantkdd/mission1/src/types/lp.ts b/Week8/wantkdd/mission1/src/types/lp.ts
new file mode 100644
index 00000000..a16ba04e
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/types/lp.ts
@@ -0,0 +1,34 @@
+import { CursorBasedResponse } from './common';
+
+export type Tag = {
+ id: number;
+ name: string;
+};
+
+export type Likes = {
+ id: number;
+ userId: number;
+ lpId: number;
+};
+
+export type Lp = {
+ id: number;
+ title: string;
+ content: string;
+ thumbnail: string;
+ published: boolean;
+ authorId: number;
+ createdAt: Date;
+ updatedAt: Date;
+ tags: Tag[];
+ likes: Likes[];
+};
+export type ResponseLpListDto = CursorBasedResponse;
+
+export type CreateLpDto = {
+ title: string;
+ content: string;
+ thumbnail: string;
+ tags: Tag[];
+ published: boolean;
+};
diff --git a/Week8/wantkdd/mission1/src/utils/validate.ts b/Week8/wantkdd/mission1/src/utils/validate.ts
new file mode 100644
index 00000000..fb67d6ab
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/utils/validate.ts
@@ -0,0 +1,33 @@
+import { z } from 'zod';
+
+export const signupSchema = z
+ .object({
+ email: z.string().email({ message: '올바른 이메일 형식이 아닙니다.' }),
+ password: z
+ .string()
+ .min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
+ .max(20, { message: '비밀번호는 20자 이하여야 합니다.' }),
+ passwordCheck: z
+ .string()
+ .min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
+ .max(20, {
+ message: '비밀번호는 20자 이하여야 합니다.',
+ }),
+ name: z.string().min(1, { message: '이름을 입력해주세요.' }),
+ })
+ .refine((data) => data.password === data.passwordCheck, {
+ message: '비밀번호가 일치하지 않습니다.',
+ path: ['passwordCheck'],
+ });
+
+export type SignupFormFields = z.infer;
+
+export const signinSchema = z.object({
+ email: z.string().email({ message: '올바른 이메일 형식이 아닙니다.' }),
+ password: z
+ .string()
+ .min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
+ .max(20, { message: '비밀번호는 20자 이하여야 합니다.' }),
+});
+
+export type SigninFormFields = z.infer;
diff --git a/Week8/wantkdd/mission1/src/vite-env.d.ts b/Week8/wantkdd/mission1/src/vite-env.d.ts
new file mode 100644
index 00000000..b48ad87b
--- /dev/null
+++ b/Week8/wantkdd/mission1/src/vite-env.d.ts
@@ -0,0 +1,8 @@
+///
+interface ImportMetaEnv {
+ readonly VITE_TMDB_KEY: string;
+ readonly VITE_SERVER_API_URL: string;
+}
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/Week8/wantkdd/mission1/tsconfig.app.json b/Week8/wantkdd/mission1/tsconfig.app.json
new file mode 100644
index 00000000..358ca9ba
--- /dev/null
+++ b/Week8/wantkdd/mission1/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/Week8/wantkdd/mission1/tsconfig.json b/Week8/wantkdd/mission1/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/Week8/wantkdd/mission1/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/Week8/wantkdd/mission1/tsconfig.node.json b/Week8/wantkdd/mission1/tsconfig.node.json
new file mode 100644
index 00000000..db0becc8
--- /dev/null
+++ b/Week8/wantkdd/mission1/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/Week8/wantkdd/mission1/vite.config.ts b/Week8/wantkdd/mission1/vite.config.ts
new file mode 100644
index 00000000..e598a4a6
--- /dev/null
+++ b/Week8/wantkdd/mission1/vite.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite';
+import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react-swc';
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+});
diff --git a/Week8/wantkdd/mission2/.gitignore b/Week8/wantkdd/mission2/.gitignore
new file mode 100644
index 00000000..3b0b4037
--- /dev/null
+++ b/Week8/wantkdd/mission2/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env
\ No newline at end of file
diff --git a/Week8/wantkdd/mission2/README.md b/Week8/wantkdd/mission2/README.md
new file mode 100644
index 00000000..40ede56e
--- /dev/null
+++ b/Week8/wantkdd/mission2/README.md
@@ -0,0 +1,54 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default tseslint.config({
+ extends: [
+ // Remove ...tseslint.configs.recommended and replace with this
+ ...tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ ...tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ ...tseslint.configs.stylisticTypeChecked,
+ ],
+ languageOptions: {
+ // other options...
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+})
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default tseslint.config({
+ plugins: {
+ // Add the react-x and react-dom plugins
+ 'react-x': reactX,
+ 'react-dom': reactDom,
+ },
+ rules: {
+ // other rules...
+ // Enable its recommended typescript rules
+ ...reactX.configs['recommended-typescript'].rules,
+ ...reactDom.configs.recommended.rules,
+ },
+})
+```
diff --git a/Week8/wantkdd/mission2/eslint.config.js b/Week8/wantkdd/mission2/eslint.config.js
new file mode 100644
index 00000000..092408a9
--- /dev/null
+++ b/Week8/wantkdd/mission2/eslint.config.js
@@ -0,0 +1,28 @@
+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'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+ },
+)
diff --git a/Week8/wantkdd/mission2/index.html b/Week8/wantkdd/mission2/index.html
new file mode 100644
index 00000000..30d6aa6a
--- /dev/null
+++ b/Week8/wantkdd/mission2/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ WAN LP
+
+
+
+
+
+
diff --git a/Week8/wantkdd/mission2/package.json b/Week8/wantkdd/mission2/package.json
new file mode 100644
index 00000000..d6ebd5d0
--- /dev/null
+++ b/Week8/wantkdd/mission2/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "mission1",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^5.0.1",
+ "@prisma/client": "^6.6.0",
+ "@tailwindcss/vite": "^4.1.3",
+ "@tanstack/react-query": "^5.75.0",
+ "@tanstack/react-query-devtools": "^5.75.1",
+ "axios": "^1.8.4",
+ "hook-form": "^0.0.1",
+ "js-cookie": "^3.0.5",
+ "prisma": "^6.6.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hook-form": "^7.55.0",
+ "react-icons": "^5.5.0",
+ "react-intersection-observer": "^9.16.0",
+ "react-router-dom": "^7.5.0",
+ "tailwindcss": "^4.1.3",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.21.0",
+ "@types/react": "^19.0.10",
+ "@types/react-dom": "^19.0.4",
+ "@vitejs/plugin-react-swc": "^3.8.0",
+ "eslint": "^9.21.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^15.15.0",
+ "typescript": "~5.7.2",
+ "typescript-eslint": "^8.24.1",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/Week8/wantkdd/mission2/pnpm-lock.yaml b/Week8/wantkdd/mission2/pnpm-lock.yaml
new file mode 100644
index 00000000..1fc76050
--- /dev/null
+++ b/Week8/wantkdd/mission2/pnpm-lock.yaml
@@ -0,0 +1,2535 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@hookform/resolvers':
+ specifier: ^5.0.1
+ version: 5.0.1(react-hook-form@7.55.0(react@19.1.0))
+ '@prisma/client':
+ specifier: ^6.6.0
+ version: 6.6.0(prisma@6.6.0(typescript@5.7.3))(typescript@5.7.3)
+ '@tailwindcss/vite':
+ specifier: ^4.1.3
+ version: 4.1.3(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))
+ '@tanstack/react-query':
+ specifier: ^5.75.0
+ version: 5.75.0(react@19.1.0)
+ '@tanstack/react-query-devtools':
+ specifier: ^5.75.1
+ version: 5.75.1(@tanstack/react-query@5.75.0(react@19.1.0))(react@19.1.0)
+ axios:
+ specifier: ^1.8.4
+ version: 1.8.4
+ hook-form:
+ specifier: ^0.0.1
+ version: 0.0.1(react@19.1.0)
+ js-cookie:
+ specifier: ^3.0.5
+ version: 3.0.5
+ prisma:
+ specifier: ^6.6.0
+ version: 6.6.0(typescript@5.7.3)
+ react:
+ specifier: ^19.0.0
+ version: 19.1.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.1.0(react@19.1.0)
+ react-hook-form:
+ specifier: ^7.55.0
+ version: 7.55.0(react@19.1.0)
+ react-icons:
+ specifier: ^5.5.0
+ version: 5.5.0(react@19.1.0)
+ react-intersection-observer:
+ specifier: ^9.16.0
+ version: 9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react-router-dom:
+ specifier: ^7.5.0
+ version: 7.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ tailwindcss:
+ specifier: ^4.1.3
+ version: 4.1.3
+ zod:
+ specifier: ^3.24.2
+ version: 3.24.2
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.21.0
+ version: 9.24.0
+ '@types/react':
+ specifier: ^19.0.10
+ version: 19.1.0
+ '@types/react-dom':
+ specifier: ^19.0.4
+ version: 19.1.1(@types/react@19.1.0)
+ '@vitejs/plugin-react-swc':
+ specifier: ^3.8.0
+ version: 3.8.1(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))
+ eslint:
+ specifier: ^9.21.0
+ version: 9.24.0(jiti@2.4.2)
+ eslint-plugin-react-hooks:
+ specifier: ^5.1.0
+ version: 5.2.0(eslint@9.24.0(jiti@2.4.2))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.19
+ version: 0.4.19(eslint@9.24.0(jiti@2.4.2))
+ globals:
+ specifier: ^15.15.0
+ version: 15.15.0
+ typescript:
+ specifier: ~5.7.2
+ version: 5.7.3
+ typescript-eslint:
+ specifier: ^8.24.1
+ version: 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ vite:
+ specifier: ^6.2.0
+ version: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2)
+
+packages:
+
+ '@esbuild/aix-ppc64@0.25.2':
+ resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.2':
+ resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.2':
+ resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.2':
+ resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.2':
+ resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.2':
+ resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.2':
+ resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.2':
+ resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.2':
+ resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.2':
+ resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.2':
+ resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.2':
+ resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.2':
+ resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.2':
+ resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.2':
+ resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.2':
+ resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.2':
+ resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.2':
+ resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.2':
+ resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.2':
+ resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.2':
+ resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.25.2':
+ resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.2':
+ resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.2':
+ resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.2':
+ resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.5.1':
+ resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.1':
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.20.0':
+ resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.2.1':
+ resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.12.0':
+ resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.13.0':
+ resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.1':
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.24.0':
+ resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.6':
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.2.8':
+ resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@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'}
+
+ '@humanfs/node@0.16.6':
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.3.1':
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+ engines: {node: '>=18.18'}
+
+ '@humanwhocodes/retry@0.4.2':
+ resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
+ engines: {node: '>=18.18'}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@prisma/client@6.6.0':
+ resolution: {integrity: sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==}
+ engines: {node: '>=18.18'}
+ peerDependencies:
+ prisma: '*'
+ typescript: '>=5.1.0'
+ peerDependenciesMeta:
+ prisma:
+ optional: true
+ typescript:
+ optional: true
+
+ '@prisma/config@6.6.0':
+ resolution: {integrity: sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA==}
+
+ '@prisma/debug@6.6.0':
+ resolution: {integrity: sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ==}
+
+ '@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a':
+ resolution: {integrity: sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw==}
+
+ '@prisma/engines@6.6.0':
+ resolution: {integrity: sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg==}
+
+ '@prisma/fetch-engine@6.6.0':
+ resolution: {integrity: sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw==}
+
+ '@prisma/get-platform@6.6.0':
+ resolution: {integrity: sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA==}
+
+ '@rollup/rollup-android-arm-eabi@4.39.0':
+ resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.39.0':
+ resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.39.0':
+ resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.39.0':
+ resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.39.0':
+ resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.39.0':
+ resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
+ resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.39.0':
+ resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.39.0':
+ resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.39.0':
+ resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
+ resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
+ resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.39.0':
+ resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.39.0':
+ resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.39.0':
+ resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.39.0':
+ resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.39.0':
+ resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.39.0':
+ resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.39.0':
+ resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.39.0':
+ resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==}
+ cpu: [x64]
+ os: [win32]
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
+ '@swc/core-darwin-arm64@1.11.16':
+ resolution: {integrity: sha512-l6uWMU+MUdfLHCl3dJgtVEdsUHPskoA4BSu0L1hh9SGBwPZ8xeOz8iLIqZM27lTuXxL4KsYH6GQR/OdQ/vhLtg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@swc/core-darwin-x64@1.11.16':
+ resolution: {integrity: sha512-TH0IW8Ao1WZ4ARFHIh29dAQHYBEl4YnP74n++rjppmlCjY+8v3s5nXMA7IqxO3b5LVHyggWtU4+46DXTyMJM7g==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@swc/core-linux-arm-gnueabihf@1.11.16':
+ resolution: {integrity: sha512-2IxD9t09oNZrbv37p4cJ9cTHMUAK6qNiShi9s2FJ9LcqSnZSN4iS4hvaaX6KZuG54d58vWnMU7yycjkdOTQcMg==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@swc/core-linux-arm64-gnu@1.11.16':
+ resolution: {integrity: sha512-AYkN23DOiPh1bf3XBf/xzZQDKSsgZTxlbyTyUIhprLJpAAAT0ZCGAUcS5mHqydk0nWQ13ABUymodvHoroutNzw==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-arm64-musl@1.11.16':
+ resolution: {integrity: sha512-n/nWXDRCIhM51dDGELfBcTMNnCiFatE7LDvsbYxb7DJt1HGjaCNvHHCKURb/apJTh/YNtWfgFap9dbsTgw8yPA==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-x64-gnu@1.11.16':
+ resolution: {integrity: sha512-xr182YQrF47n7Awxj+/ruI21bYw+xO/B26KFVnb+i3ezF9NOhqoqTX+33RL1ZLA/uFTq8ksPZO/y+ZVS/odtQA==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-linux-x64-musl@1.11.16':
+ resolution: {integrity: sha512-k2JBfiwWfXCIKrBRjFO9/vEdLSYq0QLJ+iNSLdfrejZ/aENNkbEg8O7O2GKUSb30RBacn6k8HMfJrcPLFiEyCQ==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-win32-arm64-msvc@1.11.16':
+ resolution: {integrity: sha512-taOb5U+abyEhQgex+hr6cI48BoqSvSdfmdirWcxprIEUBHCxa1dSriVwnJRAJOFI9T+5BEz88by6rgbB9MjbHA==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@swc/core-win32-ia32-msvc@1.11.16':
+ resolution: {integrity: sha512-b7yYggM9LBDiMY+XUt5kYWvs5sn0U3PXSOGvF3CbLufD/N/YQiDcYON2N3lrWHYL8aYnwbuZl45ojmQHSQPcdA==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@swc/core-win32-x64-msvc@1.11.16':
+ resolution: {integrity: sha512-/ibq/YDc3B5AROkpOKPGxVkSyCKOg+ml8k11RxrW7FAPy6a9y5y9KPcWIqV74Ahq4RuaMNslTQqHWAGSm0xJsQ==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/core@1.11.16':
+ resolution: {integrity: sha512-wgjrJqVUss8Lxqilg0vkiE0tkEKU3mZkoybQM1Ehy+PKWwwB6lFAwKi20cAEFlSSWo8jFR8hRo19ZELAoLDowg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@swc/helpers': '*'
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+
+ '@swc/counter@0.1.3':
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+
+ '@swc/types@0.1.21':
+ resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==}
+
+ '@tailwindcss/node@4.1.3':
+ resolution: {integrity: sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA==}
+
+ '@tailwindcss/oxide-android-arm64@4.1.3':
+ resolution: {integrity: sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.3':
+ resolution: {integrity: sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.1.3':
+ resolution: {integrity: sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.3':
+ resolution: {integrity: sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3':
+ resolution: {integrity: sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.3':
+ resolution: {integrity: sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.3':
+ resolution: {integrity: sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.3':
+ resolution: {integrity: sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.3':
+ resolution: {integrity: sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.3':
+ resolution: {integrity: sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.3':
+ resolution: {integrity: sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.1.3':
+ resolution: {integrity: sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ==}
+ engines: {node: '>= 10'}
+
+ '@tailwindcss/vite@4.1.3':
+ resolution: {integrity: sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6
+
+ '@tanstack/query-core@5.75.0':
+ resolution: {integrity: sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg==}
+
+ '@tanstack/query-devtools@5.74.7':
+ resolution: {integrity: sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==}
+
+ '@tanstack/react-query-devtools@5.75.1':
+ resolution: {integrity: sha512-6S71fJRBlb0adlG5z/OHRqZqtHoPYVCklf/KCnOoZ3vdx9O/K9BLvqeeDeMUkZ6Rak/IupbEbvBsFoUiMHwZiQ==}
+ peerDependencies:
+ '@tanstack/react-query': ^5.75.1
+ react: ^18 || ^19
+
+ '@tanstack/react-query@5.75.0':
+ resolution: {integrity: sha512-H+TNgxmTbzH8qQ5MT5xsZEhQ8BG1tUYduDSfeAOzroVZgd/AEjg1rRYSP/9Tl9/hPobZ7iZzV401n77kStrbKw==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@types/cookie@0.6.0':
+ resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+
+ '@types/estree@1.0.7':
+ resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/react-dom@19.1.1':
+ resolution: {integrity: sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+
+ '@types/react@19.1.0':
+ resolution: {integrity: sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==}
+
+ '@typescript-eslint/eslint-plugin@8.29.0':
+ resolution: {integrity: sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/parser@8.29.0':
+ resolution: {integrity: sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/scope-manager@8.29.0':
+ resolution: {integrity: sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/type-utils@8.29.0':
+ resolution: {integrity: sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/types@8.29.0':
+ resolution: {integrity: sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.29.0':
+ resolution: {integrity: sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/utils@8.29.0':
+ resolution: {integrity: sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/visitor-keys@8.29.0':
+ resolution: {integrity: sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react-swc@3.8.1':
+ resolution: {integrity: sha512-aEUPCckHDcFyxpwFm0AIkbtv6PpUp3xTb9wYGFjtABynXjCYKkWoxX0AOK9NT9XCrdk6mBBUOeHQS+RKdcNO1A==}
+ peerDependencies:
+ vite: ^4 || ^5 || ^6
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.14.1:
+ resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ 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.8.4:
+ resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+
+ brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ 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==}
+
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ debug@4.4.0:
+ resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ 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'}
+
+ detect-libc@2.0.3:
+ resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ enhanced-resolve@5.18.1:
+ resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
+ engines: {node: '>=10.13.0'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ 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-register@3.6.0:
+ resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
+ peerDependencies:
+ esbuild: '>=0.12 <1'
+
+ esbuild@0.25.2:
+ resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@5.2.0:
+ resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react-refresh@0.4.19:
+ resolution: {integrity: sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==}
+ peerDependencies:
+ eslint: '>=8.40'
+
+ eslint-scope@8.3.0:
+ resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.0:
+ resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@9.24.0:
+ resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.3.0:
+ resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fastq@1.19.1:
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ 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'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@15.15.0:
+ resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
+ engines: {node: '>=18'}
+
+ gopd@1.2.0:
+ 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==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-symbols@1.1.0:
+ 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'}
+
+ hook-form@0.0.1:
+ resolution: {integrity: sha512-jusNZfTsHBSqvIgrhG8a1iwdeN1UT3HenQwg3snzd5ArwpNgDQlN2O0XdPqEE2oPUdMr/k16WmCccO8jRgiRYA==}
+ peerDependencies:
+ react: '>=15.0.0 || ^16.0.0'
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jiti@2.4.2:
+ resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
+ hasBin: true
+
+ js-cookie@3.0.5:
+ resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
+ engines: {node: '>=14'}
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-darwin-arm64@1.29.2:
+ resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.29.2:
+ resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.29.2:
+ resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.29.2:
+ resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.29.2:
+ resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.29.2:
+ resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.29.2:
+ resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.29.2:
+ resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.29.2:
+ resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.29.2:
+ resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.29.2:
+ resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromatch@4.0.8:
+ 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-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ postcss@8.5.3:
+ resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prisma@6.6.0:
+ resolution: {integrity: sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg==}
+ engines: {node: '>=18.18'}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.1.0'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ 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'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ react-dom@19.1.0:
+ resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
+ peerDependencies:
+ react: ^19.1.0
+
+ react-hook-form@7.55.0:
+ resolution: {integrity: sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-icons@5.5.0:
+ resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
+ peerDependencies:
+ react: '*'
+
+ react-intersection-observer@9.16.0:
+ resolution: {integrity: sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==}
+ peerDependencies:
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react-router-dom@7.5.0:
+ resolution: {integrity: sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.5.0:
+ resolution: {integrity: sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react@19.1.0:
+ resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rollup@4.39.0:
+ resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ scheduler@0.26.0:
+ resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
+
+ semver@7.7.1:
+ resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ tailwindcss@4.1.3:
+ resolution: {integrity: sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==}
+
+ tapable@2.2.1:
+ resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+ engines: {node: '>=6'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ ts-api-utils@2.1.0:
+ resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ turbo-stream@2.4.0:
+ resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.29.0:
+ resolution: {integrity: sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ typescript@5.7.3:
+ resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite@6.2.5:
+ resolution: {integrity: sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ 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
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod@3.24.2:
+ resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
+
+snapshots:
+
+ '@esbuild/aix-ppc64@0.25.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/android-arm@0.25.2':
+ optional: true
+
+ '@esbuild/android-x64@0.25.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.2':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.5.1(eslint@9.24.0(jiti@2.4.2))':
+ dependencies:
+ eslint: 9.24.0(jiti@2.4.2)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.1': {}
+
+ '@eslint/config-array@0.20.0':
+ dependencies:
+ '@eslint/object-schema': 2.1.6
+ debug: 4.4.0
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.2.1': {}
+
+ '@eslint/core@0.12.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/core@0.13.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.1':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.4.0
+ espree: 10.3.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.24.0': {}
+
+ '@eslint/object-schema@2.1.6': {}
+
+ '@eslint/plugin-kit@0.2.8':
+ dependencies:
+ '@eslint/core': 0.13.0
+ levn: 0.4.1
+
+ '@hookform/resolvers@5.0.1(react-hook-form@7.55.0(react@19.1.0))':
+ dependencies:
+ '@standard-schema/utils': 0.3.0
+ react-hook-form: 7.55.0(react@19.1.0)
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.6':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.3.1
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.3.1': {}
+
+ '@humanwhocodes/retry@0.4.2': {}
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.19.1
+
+ '@prisma/client@6.6.0(prisma@6.6.0(typescript@5.7.3))(typescript@5.7.3)':
+ optionalDependencies:
+ prisma: 6.6.0(typescript@5.7.3)
+ typescript: 5.7.3
+
+ '@prisma/config@6.6.0':
+ dependencies:
+ esbuild: 0.25.2
+ esbuild-register: 3.6.0(esbuild@0.25.2)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@prisma/debug@6.6.0': {}
+
+ '@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a': {}
+
+ '@prisma/engines@6.6.0':
+ dependencies:
+ '@prisma/debug': 6.6.0
+ '@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a
+ '@prisma/fetch-engine': 6.6.0
+ '@prisma/get-platform': 6.6.0
+
+ '@prisma/fetch-engine@6.6.0':
+ dependencies:
+ '@prisma/debug': 6.6.0
+ '@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a
+ '@prisma/get-platform': 6.6.0
+
+ '@prisma/get-platform@6.6.0':
+ dependencies:
+ '@prisma/debug': 6.6.0
+
+ '@rollup/rollup-android-arm-eabi@4.39.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.39.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.39.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.39.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.39.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.39.0':
+ optional: true
+
+ '@standard-schema/utils@0.3.0': {}
+
+ '@swc/core-darwin-arm64@1.11.16':
+ optional: true
+
+ '@swc/core-darwin-x64@1.11.16':
+ optional: true
+
+ '@swc/core-linux-arm-gnueabihf@1.11.16':
+ optional: true
+
+ '@swc/core-linux-arm64-gnu@1.11.16':
+ optional: true
+
+ '@swc/core-linux-arm64-musl@1.11.16':
+ optional: true
+
+ '@swc/core-linux-x64-gnu@1.11.16':
+ optional: true
+
+ '@swc/core-linux-x64-musl@1.11.16':
+ optional: true
+
+ '@swc/core-win32-arm64-msvc@1.11.16':
+ optional: true
+
+ '@swc/core-win32-ia32-msvc@1.11.16':
+ optional: true
+
+ '@swc/core-win32-x64-msvc@1.11.16':
+ optional: true
+
+ '@swc/core@1.11.16':
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.21
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.11.16
+ '@swc/core-darwin-x64': 1.11.16
+ '@swc/core-linux-arm-gnueabihf': 1.11.16
+ '@swc/core-linux-arm64-gnu': 1.11.16
+ '@swc/core-linux-arm64-musl': 1.11.16
+ '@swc/core-linux-x64-gnu': 1.11.16
+ '@swc/core-linux-x64-musl': 1.11.16
+ '@swc/core-win32-arm64-msvc': 1.11.16
+ '@swc/core-win32-ia32-msvc': 1.11.16
+ '@swc/core-win32-x64-msvc': 1.11.16
+
+ '@swc/counter@0.1.3': {}
+
+ '@swc/types@0.1.21':
+ dependencies:
+ '@swc/counter': 0.1.3
+
+ '@tailwindcss/node@4.1.3':
+ dependencies:
+ enhanced-resolve: 5.18.1
+ jiti: 2.4.2
+ lightningcss: 1.29.2
+ tailwindcss: 4.1.3
+
+ '@tailwindcss/oxide-android-arm64@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.3':
+ optional: true
+
+ '@tailwindcss/oxide@4.1.3':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.1.3
+ '@tailwindcss/oxide-darwin-arm64': 4.1.3
+ '@tailwindcss/oxide-darwin-x64': 4.1.3
+ '@tailwindcss/oxide-freebsd-x64': 4.1.3
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.3
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.1.3
+ '@tailwindcss/oxide-linux-arm64-musl': 4.1.3
+ '@tailwindcss/oxide-linux-x64-gnu': 4.1.3
+ '@tailwindcss/oxide-linux-x64-musl': 4.1.3
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.1.3
+ '@tailwindcss/oxide-win32-x64-msvc': 4.1.3
+
+ '@tailwindcss/vite@4.1.3(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))':
+ dependencies:
+ '@tailwindcss/node': 4.1.3
+ '@tailwindcss/oxide': 4.1.3
+ tailwindcss: 4.1.3
+ vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2)
+
+ '@tanstack/query-core@5.75.0': {}
+
+ '@tanstack/query-devtools@5.74.7': {}
+
+ '@tanstack/react-query-devtools@5.75.1(@tanstack/react-query@5.75.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@tanstack/query-devtools': 5.74.7
+ '@tanstack/react-query': 5.75.0(react@19.1.0)
+ react: 19.1.0
+
+ '@tanstack/react-query@5.75.0(react@19.1.0)':
+ dependencies:
+ '@tanstack/query-core': 5.75.0
+ react: 19.1.0
+
+ '@types/cookie@0.6.0': {}
+
+ '@types/estree@1.0.7': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/react-dom@19.1.1(@types/react@19.1.0)':
+ dependencies:
+ '@types/react': 19.1.0
+
+ '@types/react@19.1.0':
+ dependencies:
+ csstype: 3.1.3
+
+ '@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/scope-manager': 8.29.0
+ '@typescript-eslint/type-utils': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.29.0
+ eslint: 9.24.0(jiti@2.4.2)
+ graphemer: 1.4.0
+ ignore: 5.3.2
+ natural-compare: 1.4.0
+ ts-api-utils: 2.1.0(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.29.0
+ '@typescript-eslint/types': 8.29.0
+ '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.29.0
+ debug: 4.4.0
+ eslint: 9.24.0(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.29.0':
+ dependencies:
+ '@typescript-eslint/types': 8.29.0
+ '@typescript-eslint/visitor-keys': 8.29.0
+
+ '@typescript-eslint/type-utils@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ debug: 4.4.0
+ eslint: 9.24.0(jiti@2.4.2)
+ ts-api-utils: 2.1.0(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.29.0': {}
+
+ '@typescript-eslint/typescript-estree@8.29.0(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.29.0
+ '@typescript-eslint/visitor-keys': 8.29.0
+ debug: 4.4.0
+ fast-glob: 3.3.3
+ is-glob: 4.0.3
+ minimatch: 9.0.5
+ semver: 7.7.1
+ ts-api-utils: 2.1.0(typescript@5.7.3)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@2.4.2))
+ '@typescript-eslint/scope-manager': 8.29.0
+ '@typescript-eslint/types': 8.29.0
+ '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.7.3)
+ eslint: 9.24.0(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.29.0':
+ dependencies:
+ '@typescript-eslint/types': 8.29.0
+ eslint-visitor-keys: 4.2.0
+
+ '@vitejs/plugin-react-swc@3.8.1(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))':
+ dependencies:
+ '@swc/core': 1.11.16
+ vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2)
+ transitivePeerDependencies:
+ - '@swc/helpers'
+
+ acorn-jsx@5.3.2(acorn@8.14.1):
+ dependencies:
+ acorn: 8.14.1
+
+ acorn@8.14.1: {}
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ asynckit@0.4.0: {}
+
+ axios@1.8.4:
+ dependencies:
+ follow-redirects: 1.15.9
+ form-data: 4.0.2
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ balanced-match@1.0.2: {}
+
+ brace-expansion@1.1.11:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.1:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ callsites@3.1.0: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.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: {}
+
+ cookie@1.0.2: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.1.3: {}
+
+ debug@4.4.0:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ delayed-stream@1.0.0: {}
+
+ detect-libc@2.0.3: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ enhanced-resolve@5.18.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.2.1
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ 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-register@3.6.0(esbuild@0.25.2):
+ dependencies:
+ debug: 4.4.0
+ esbuild: 0.25.2
+ transitivePeerDependencies:
+ - supports-color
+
+ esbuild@0.25.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.2
+ '@esbuild/android-arm': 0.25.2
+ '@esbuild/android-arm64': 0.25.2
+ '@esbuild/android-x64': 0.25.2
+ '@esbuild/darwin-arm64': 0.25.2
+ '@esbuild/darwin-x64': 0.25.2
+ '@esbuild/freebsd-arm64': 0.25.2
+ '@esbuild/freebsd-x64': 0.25.2
+ '@esbuild/linux-arm': 0.25.2
+ '@esbuild/linux-arm64': 0.25.2
+ '@esbuild/linux-ia32': 0.25.2
+ '@esbuild/linux-loong64': 0.25.2
+ '@esbuild/linux-mips64el': 0.25.2
+ '@esbuild/linux-ppc64': 0.25.2
+ '@esbuild/linux-riscv64': 0.25.2
+ '@esbuild/linux-s390x': 0.25.2
+ '@esbuild/linux-x64': 0.25.2
+ '@esbuild/netbsd-arm64': 0.25.2
+ '@esbuild/netbsd-x64': 0.25.2
+ '@esbuild/openbsd-arm64': 0.25.2
+ '@esbuild/openbsd-x64': 0.25.2
+ '@esbuild/sunos-x64': 0.25.2
+ '@esbuild/win32-arm64': 0.25.2
+ '@esbuild/win32-ia32': 0.25.2
+ '@esbuild/win32-x64': 0.25.2
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@5.2.0(eslint@9.24.0(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.24.0(jiti@2.4.2)
+
+ eslint-plugin-react-refresh@0.4.19(eslint@9.24.0(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.24.0(jiti@2.4.2)
+
+ eslint-scope@8.3.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.0: {}
+
+ eslint@9.24.0(jiti@2.4.2):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@2.4.2))
+ '@eslint-community/regexpp': 4.12.1
+ '@eslint/config-array': 0.20.0
+ '@eslint/config-helpers': 0.2.1
+ '@eslint/core': 0.12.0
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.24.0
+ '@eslint/plugin-kit': 0.2.8
+ '@humanfs/node': 0.16.6
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.2
+ '@types/estree': 1.0.7
+ '@types/json-schema': 7.0.15
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.3.0
+ eslint-visitor-keys: 4.2.0
+ espree: 10.3.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.4.2
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.3.0:
+ dependencies:
+ acorn: 8.14.1
+ acorn-jsx: 5.3.2(acorn@8.14.1)
+ eslint-visitor-keys: 4.2.0
+
+ esquery@1.6.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fastq@1.19.1:
+ dependencies:
+ reusify: 1.1.0
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
+ 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
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ globals@15.15.0: {}
+
+ 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
+
+ hook-form@0.0.1(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ ignore@5.3.2: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-number@7.0.0: {}
+
+ isexe@2.0.0: {}
+
+ jiti@2.4.2: {}
+
+ js-cookie@3.0.5: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-darwin-arm64@1.29.2:
+ optional: true
+
+ lightningcss-darwin-x64@1.29.2:
+ optional: true
+
+ lightningcss-freebsd-x64@1.29.2:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.29.2:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.29.2:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.29.2:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.29.2:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.29.2:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.29.2:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.29.2:
+ optional: true
+
+ lightningcss@1.29.2:
+ dependencies:
+ detect-libc: 2.0.3
+ optionalDependencies:
+ lightningcss-darwin-arm64: 1.29.2
+ lightningcss-darwin-x64: 1.29.2
+ lightningcss-freebsd-x64: 1.29.2
+ lightningcss-linux-arm-gnueabihf: 1.29.2
+ lightningcss-linux-arm64-gnu: 1.29.2
+ lightningcss-linux-arm64-musl: 1.29.2
+ lightningcss-linux-x64-gnu: 1.29.2
+ lightningcss-linux-x64-musl: 1.29.2
+ lightningcss-win32-arm64-msvc: 1.29.2
+ lightningcss-win32-x64-msvc: 1.29.2
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.merge@4.6.2: {}
+
+ math-intrinsics@1.1.0: {}
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.11
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.1
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.11: {}
+
+ natural-compare@1.4.0: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ postcss@8.5.3:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ prisma@6.6.0(typescript@5.7.3):
+ dependencies:
+ '@prisma/config': 6.6.0
+ '@prisma/engines': 6.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ proxy-from-env@1.1.0: {}
+
+ punycode@2.3.1: {}
+
+ queue-microtask@1.2.3: {}
+
+ react-dom@19.1.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ scheduler: 0.26.0
+
+ react-hook-form@7.55.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ react-icons@5.5.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ react-intersection-observer@9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ react-dom: 19.1.0(react@19.1.0)
+
+ react-router-dom@7.5.0(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.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+
+ react-router@7.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ '@types/cookie': 0.6.0
+ cookie: 1.0.2
+ react: 19.1.0
+ set-cookie-parser: 2.7.1
+ turbo-stream: 2.4.0
+ optionalDependencies:
+ react-dom: 19.1.0(react@19.1.0)
+
+ react@19.1.0: {}
+
+ resolve-from@4.0.0: {}
+
+ reusify@1.1.0: {}
+
+ rollup@4.39.0:
+ dependencies:
+ '@types/estree': 1.0.7
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.39.0
+ '@rollup/rollup-android-arm64': 4.39.0
+ '@rollup/rollup-darwin-arm64': 4.39.0
+ '@rollup/rollup-darwin-x64': 4.39.0
+ '@rollup/rollup-freebsd-arm64': 4.39.0
+ '@rollup/rollup-freebsd-x64': 4.39.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.39.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.39.0
+ '@rollup/rollup-linux-arm64-gnu': 4.39.0
+ '@rollup/rollup-linux-arm64-musl': 4.39.0
+ '@rollup/rollup-linux-loongarch64-gnu': 4.39.0
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.39.0
+ '@rollup/rollup-linux-riscv64-musl': 4.39.0
+ '@rollup/rollup-linux-s390x-gnu': 4.39.0
+ '@rollup/rollup-linux-x64-gnu': 4.39.0
+ '@rollup/rollup-linux-x64-musl': 4.39.0
+ '@rollup/rollup-win32-arm64-msvc': 4.39.0
+ '@rollup/rollup-win32-ia32-msvc': 4.39.0
+ '@rollup/rollup-win32-x64-msvc': 4.39.0
+ fsevents: 2.3.3
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ scheduler@0.26.0: {}
+
+ semver@7.7.1: {}
+
+ set-cookie-parser@2.7.1: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ strip-json-comments@3.1.1: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ tailwindcss@4.1.3: {}
+
+ tapable@2.2.1: {}
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ ts-api-utils@2.1.0(typescript@5.7.3):
+ dependencies:
+ typescript: 5.7.3
+
+ turbo-stream@2.4.0: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/parser': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.29.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3)
+ eslint: 9.24.0(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.7.3: {}
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2):
+ dependencies:
+ esbuild: 0.25.2
+ postcss: 8.5.3
+ rollup: 4.39.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ jiti: 2.4.2
+ lightningcss: 1.29.2
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod@3.24.2: {}
diff --git a/Week8/wantkdd/mission2/public/vite.svg b/Week8/wantkdd/mission2/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/Week8/wantkdd/mission2/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Week8/wantkdd/mission2/src/App.css b/Week8/wantkdd/mission2/src/App.css
new file mode 100644
index 00000000..e69de29b
diff --git a/Week8/wantkdd/mission2/src/App.tsx b/Week8/wantkdd/mission2/src/App.tsx
new file mode 100644
index 00000000..75662b2b
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/App.tsx
@@ -0,0 +1,73 @@
+import './App.css';
+import {
+ createBrowserRouter,
+ RouteObject,
+ RouterProvider,
+} from 'react-router-dom';
+import RootLayout from './layout/root-layout';
+import HomePage from './pages/home-page';
+import LoginPage from './pages/login-page';
+import SignupPage from './pages/signup-page';
+import { AuthProvider } from './context/AuthContext';
+import MyPage from './pages/my-page';
+import ProtectedLayout from './layout/ProtectedLayout';
+import GoogleLoginRedirectPage from './pages/googleLoginRedirect-page';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import LpDetailPage from './pages/detail-page';
+import SearchPage from './pages/search-page';
+
+const publicRoutes: RouteObject[] = [
+ {
+ path: '/',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: '/search-page',
+ element: ,
+ },
+ {
+ path: '/login-page',
+ element: ,
+ },
+ {
+ path: '/signup-page',
+ element: ,
+ },
+ { path: 'v1/auth/google/callback', element: },
+ ],
+ },
+];
+
+const protectedRoutes: RouteObject[] = [
+ {
+ element: ,
+ children: [
+ { path: '/my-page', element: },
+ { path: '/lp/:id', element: },
+ ],
+ },
+];
+
+const router = createBrowserRouter([...publicRoutes, ...protectedRoutes]);
+
+export const queryClient = new QueryClient();
+
+function App() {
+ return (
+ <>
+
+
+
+
+ {import.meta.env.DEV && }
+
+ >
+ );
+}
+
+export default App;
diff --git a/Week8/wantkdd/mission2/src/apis/addLp.ts b/Week8/wantkdd/mission2/src/apis/addLp.ts
new file mode 100644
index 00000000..cae52948
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/apis/addLp.ts
@@ -0,0 +1,30 @@
+import { CreateLpDto, Tag } from '../types/lp';
+import { axiosInstance } from './axios';
+
+export async function postLp(lpData: CreateLpDto) {
+ const preparedData = {
+ ...lpData,
+ tags: lpData.tags.map((tag: Tag) => tag.name),
+ };
+
+ // console.log('전송 데이터:', preparedData);
+ const response = await axiosInstance.post('/v1/lps', preparedData, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ return response.data;
+}
+
+export async function uploadImage(file: File): Promise {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await axiosInstance.post('/v1/uploads', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ return response.data.data.imageUrl;
+}
diff --git a/Week8/wantkdd/mission2/src/apis/auth.ts b/Week8/wantkdd/mission2/src/apis/auth.ts
new file mode 100644
index 00000000..338026df
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/apis/auth.ts
@@ -0,0 +1,48 @@
+import { LOCAL_STORAGE_KEY } from '../constants/key';
+import {
+ PatchUserInfoDto,
+ RequestSigninDto,
+ RequestSignupDto,
+ ResponseMyInfoDto,
+ ResponseSigninDto,
+} from '../types/auth';
+import { axiosInstance } from './axios';
+
+export const postSignup = async (
+ body: RequestSignupDto
+): Promise => {
+ const { data } = await axiosInstance.post('/v1/auth/signup', body);
+
+ return data;
+};
+
+export const postSignin = async (
+ body: RequestSigninDto
+): Promise => {
+ const { data } = await axiosInstance.post('/v1/auth/signin', body);
+
+ return data;
+};
+
+export const getMyInfo = async (): Promise => {
+ const { data } = await axiosInstance.get('/v1/users/me');
+ return data;
+};
+
+export const postLogout = async () => {
+ const refreshToken = localStorage.getItem(LOCAL_STORAGE_KEY.refreshToken);
+
+ const { data } = await axiosInstance.post('/v1/auth/signout', {
+ refreshToken,
+ });
+ return data;
+};
+
+export const patchUserInfo = async (body: PatchUserInfoDto) => {
+ const { data } = await axiosInstance.patch('/v1/users', body);
+ return data;
+};
+
+export const deleteUser = async () => {
+ return await axiosInstance.delete('/v1/users/');
+};
diff --git a/Week8/wantkdd/mission2/src/apis/axios.ts b/Week8/wantkdd/mission2/src/apis/axios.ts
new file mode 100644
index 00000000..b3fab508
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/apis/axios.ts
@@ -0,0 +1,105 @@
+import axios, { InternalAxiosRequestConfig } from 'axios';
+import { LOCAL_STORAGE_KEY } from '../constants/key';
+
+interface CustomInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
+ _retry?: boolean;
+}
+
+let refreshPromise: Promise | null = null;
+
+export const axiosInstance = axios.create({
+ baseURL: import.meta.env.VITE_SERVER_API_URL,
+ headers: {
+ accept: 'application/json',
+ },
+});
+
+axiosInstance.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('accessToken');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+axiosInstance.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ const originalRequest: CustomInternalAxiosRequestConfig = error.config;
+
+ if (originalRequest.url === '/v1/auth/signin') {
+ return Promise.reject(error);
+ }
+ if (
+ error.response &&
+ error.response.status === 401 &&
+ !originalRequest._retry
+ ) {
+ if (originalRequest.url === '/v1/auth/refresh') {
+ localStorage.removeItem(LOCAL_STORAGE_KEY.accessToken);
+ localStorage.removeItem(LOCAL_STORAGE_KEY.refreshToken);
+ window.location.href = '/login-page';
+ return Promise.reject(error);
+ }
+
+ originalRequest._retry = true;
+
+ if (!refreshPromise) {
+ refreshPromise = (async () => {
+ try {
+ const refreshToken = localStorage.getItem(
+ LOCAL_STORAGE_KEY.refreshToken
+ );
+
+ if (!refreshToken) {
+ throw new Error('리프레시 토큰이 없습니다');
+ }
+
+ const { data } = await axiosInstance.post('/v1/auth/refresh', {
+ refresh: refreshToken,
+ });
+
+ localStorage.setItem(
+ LOCAL_STORAGE_KEY.accessToken,
+ data.data.accessToken
+ );
+ localStorage.setItem(
+ LOCAL_STORAGE_KEY.refreshToken,
+ data.data.refreshToken
+ );
+
+ return data.data.accessToken;
+ } catch (refreshError) {
+ localStorage.removeItem(LOCAL_STORAGE_KEY.accessToken);
+ localStorage.removeItem(LOCAL_STORAGE_KEY.refreshToken);
+
+ window.location.href = '/login-page';
+ throw refreshError;
+ } finally {
+ refreshPromise = null;
+ }
+ })();
+ }
+
+ try {
+ const newAccessToken = await refreshPromise;
+ if (newAccessToken) {
+ axiosInstance.defaults.headers.common[
+ 'Authorization'
+ ] = `Bearer ${newAccessToken}`;
+ originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
+ return axiosInstance(originalRequest);
+ }
+ return Promise.reject(error);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+ return Promise.reject(error);
+ }
+);
diff --git a/Week8/wantkdd/mission2/src/apis/like.ts b/Week8/wantkdd/mission2/src/apis/like.ts
new file mode 100644
index 00000000..1c6eef10
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/apis/like.ts
@@ -0,0 +1,11 @@
+import { ResponseLikeLpDto } from '../types/lp-detail';
+import { axiosInstance } from './axios';
+
+export const postLike = async (lpId: number): Promise => {
+ const { data } = await axiosInstance.post(`/v1/lps/${lpId}/likes`);
+ return data;
+};
+export const deleteLike = async (lpId: number): Promise => {
+ const { data } = await axiosInstance.delete(`/v1/lps/${lpId}/likes`);
+ return data;
+};
diff --git a/Week8/wantkdd/mission2/src/apis/lp-detail.ts b/Week8/wantkdd/mission2/src/apis/lp-detail.ts
new file mode 100644
index 00000000..c9925f68
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/apis/lp-detail.ts
@@ -0,0 +1,72 @@
+import { axiosInstance } from './axios';
+import {
+ LpDetailResponse,
+ CommentsResponse,
+ LpDetailData,
+ UpdateLpRequest,
+} from '../types/lp-detail';
+import { PaginationDto } from '../types/common';
+
+export const getLpDetail = async (id: number): Promise => {
+ const { data } = await axiosInstance.get(`/v1/lps/${id}`);
+ return data.data;
+};
+
+export const getComments = async ({
+ lpId,
+ cursor,
+ limit,
+ order,
+}: {
+ lpId: number;
+} & PaginationDto): Promise => {
+ const { data } = await axiosInstance.get(
+ `/v1/lps/${lpId}/comments`,
+ {
+ params: { cursor, limit, order },
+ }
+ );
+ return data;
+};
+
+export const createComment = async (
+ lpId: string,
+ content: string
+): Promise => {
+ const { data } = await axiosInstance.post(`/v1/lps/${lpId}/comments`, {
+ content,
+ });
+ return data.data;
+};
+
+export const updateComment = async (
+ lpId: number,
+ commentId: number,
+ content: string
+) => {
+ const { data } = await axiosInstance.patch(
+ `/v1/lps/${lpId}/comments/${commentId}`,
+ {
+ content,
+ }
+ );
+ return data.data;
+};
+
+export const deleteComment = async (lpId: number, commentId: number) => {
+ const { data } = await axiosInstance.delete(
+ `/v1/lps/${lpId}/comments/${commentId}`
+ );
+ return data.data;
+};
+
+export const updateLp = async ({
+ lpId,
+ payload,
+}: {
+ lpId: number;
+ payload: UpdateLpRequest;
+}): Promise => {
+ const { data } = await axiosInstance.patch(`/v1/lps/${lpId}`, payload);
+ return data.data;
+};
diff --git a/Week8/wantkdd/mission2/src/apis/lp.ts b/Week8/wantkdd/mission2/src/apis/lp.ts
new file mode 100644
index 00000000..5d47c46a
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/apis/lp.ts
@@ -0,0 +1,17 @@
+import { PaginationDto } from '../types/common';
+import { ResponseLpListDto } from '../types/lp';
+import { axiosInstance } from './axios';
+
+export const getLpList = async (
+ paginationDto: PaginationDto
+): Promise => {
+ const { data } = await axiosInstance.get('/v1/lps', {
+ params: paginationDto,
+ });
+ return data;
+};
+
+export const deleteLp = async (lpId: number): Promise => {
+ const { data } = await axiosInstance.delete(`/v1/lps/${lpId}`);
+ return data.data;
+};
diff --git a/Week8/wantkdd/mission2/src/assets/default.png b/Week8/wantkdd/mission2/src/assets/default.png
new file mode 100644
index 00000000..aa051290
Binary files /dev/null and b/Week8/wantkdd/mission2/src/assets/default.png differ
diff --git a/Week8/wantkdd/mission2/src/assets/googleLogo.svg b/Week8/wantkdd/mission2/src/assets/googleLogo.svg
new file mode 100644
index 00000000..eb339320
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/assets/googleLogo.svg
@@ -0,0 +1,16 @@
+
diff --git a/Week8/wantkdd/mission2/src/assets/react.svg b/Week8/wantkdd/mission2/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Week8/wantkdd/mission2/src/components/add-lp/addButton.tsx b/Week8/wantkdd/mission2/src/components/add-lp/addButton.tsx
new file mode 100644
index 00000000..1a72ef1a
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/add-lp/addButton.tsx
@@ -0,0 +1,20 @@
+import { useState } from 'react';
+import AddLpModal from './addLp';
+
+const AddButton = () => {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+
+ {open && setOpen(false)} />}
+ >
+ );
+};
+
+export default AddButton;
diff --git a/Week8/wantkdd/mission2/src/components/add-lp/addLp.tsx b/Week8/wantkdd/mission2/src/components/add-lp/addLp.tsx
new file mode 100644
index 00000000..28cdea54
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/add-lp/addLp.tsx
@@ -0,0 +1,188 @@
+import React, { useState, useRef } from 'react';
+import { CreateLpDto, Tag } from '../../types/lp';
+import usePostLp from '../../hooks/mutations/lp/usePostLp';
+import { uploadImage } from '../../apis/addLp';
+
+const AddLp = ({ onClose }: { onClose: () => void }) => {
+ const modalRef = useRef(null);
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [thumbnail, setThumbnail] = useState('');
+ const [tags, setTags] = useState([]);
+ const [currentTag, setCurrentTag] = useState('');
+
+ const { mutate: createLp, isPending } = usePostLp();
+
+ const handleBackdropClick = (e: React.MouseEvent) => {
+ if (e.target === modalRef.current) {
+ onClose();
+ }
+ };
+
+ const handleAddTag = () => {
+ const trimmed = currentTag.trim();
+ if (trimmed) {
+ const exists = tags.some(
+ (tag) => tag.name.toLowerCase() === trimmed.toLowerCase()
+ );
+ if (!exists) {
+ const newTag: Tag = {
+ id: tags.length > 0 ? Math.max(...tags.map((t) => t.id)) + 1 : 1,
+ name: trimmed,
+ };
+ setTags((prev) => [...prev, newTag]);
+ setCurrentTag('');
+ }
+ }
+ };
+
+ const handleRemoveTag = (tagToRemove: Tag) => {
+ setTags((prev) => prev.filter((tag) => tag.id !== tagToRemove.id));
+ };
+
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ try {
+ const uploadedUrl = await uploadImage(file);
+ setThumbnail(uploadedUrl);
+ } catch (error) {
+ console.error('이미지 업로드 실패:', error);
+ alert('이미지 업로드 중 오류가 발생했습니다.');
+ }
+ };
+
+ const handleCreateLp = () => {
+ if (!title.trim() || !content.trim()) {
+ alert('제목과 내용을 입력해주세요.');
+ return;
+ }
+
+ const lpData: CreateLpDto = {
+ title,
+ content,
+ thumbnail,
+ published: true,
+ tags,
+ };
+
+ createLp(lpData, {
+ onSuccess: () => {
+ onClose();
+ },
+ onError: (error) => {
+ console.error('LP 생성 실패:', error);
+ alert('LP 생성 중 오류가 발생했습니다.');
+ },
+ });
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default AddLp;
diff --git a/Week8/wantkdd/mission2/src/components/footer.tsx b/Week8/wantkdd/mission2/src/components/footer.tsx
new file mode 100644
index 00000000..ed1090d9
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/footer.tsx
@@ -0,0 +1,20 @@
+import { Link } from 'react-router-dom';
+
+const footer = () => {
+ return (
+
+ );
+};
+
+export default footer;
diff --git a/Week8/wantkdd/mission2/src/components/googleLogin.tsx b/Week8/wantkdd/mission2/src/components/googleLogin.tsx
new file mode 100644
index 00000000..70c36b5e
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/googleLogin.tsx
@@ -0,0 +1,19 @@
+import googleLogo from '../assets/googleLogo.svg';
+
+const GoogleLoginButton = () => {
+ const handleGoogleLogin = () => {
+ window.location.href =
+ import.meta.env.VITE_SERVER_API_URL + '/v1/auth/google/login';
+ };
+
+ return (
+
+ );
+};
+
+export default GoogleLoginButton;
diff --git a/Week8/wantkdd/mission2/src/components/login-form.tsx b/Week8/wantkdd/mission2/src/components/login-form.tsx
new file mode 100644
index 00000000..4cc0a265
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/login-form.tsx
@@ -0,0 +1,111 @@
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { signinSchema, SigninFormFields } from '../utils/validate';
+import { useNavigate } from 'react-router-dom';
+// import { useAuth } from '../context/AuthContext';
+import { useLogin } from '../hooks/mutations/user/useLogin';
+const LoginForm = () => {
+ // const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid, isDirty },
+ } = useForm({
+ resolver: zodResolver(signinSchema),
+ mode: 'onBlur',
+ defaultValues: {
+ email: '',
+ password: '',
+ },
+ });
+ const { mutateAsync: login } = useLogin();
+ const onSubmit = async (data: SigninFormFields) => {
+ try {
+ await login(data);
+ navigate('/my-page');
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+ );
+};
+
+interface EmailInputProps {
+ register: ReturnType>['register'];
+ error?: {
+ message?: string;
+ };
+}
+
+const EmailInput = ({ register, error }: EmailInputProps) => {
+ return (
+
+
+ {error && (
+
{error.message}
+ )}
+
+ );
+};
+
+interface PasswordInputProps {
+ register: ReturnType>['register'];
+ error?: {
+ message?: string;
+ };
+}
+
+const PasswordInput = ({ register, error }: PasswordInputProps) => {
+ return (
+
+
+ {error && (
+
{error.message}
+ )}
+
+ );
+};
+
+interface SubmitButtonProps {
+ isDisabled: boolean;
+}
+
+const SubmitButton = ({ isDisabled }: SubmitButtonProps) => {
+ return (
+
+ );
+};
+
+export default LoginForm;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/DeleteModal.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/DeleteModal.tsx
new file mode 100644
index 00000000..eac51846
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/DeleteModal.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+interface DeleteModalProps {
+ onCancel: () => void;
+ onConfirm: () => void;
+}
+
+const DeleteModal: React.FC = ({ onCancel, onConfirm }) => {
+ return (
+
+
+
LP 삭제 확인
+
정말 이 LP를 삭제하시겠습니까?
+
+
+
+
+
+
+ );
+};
+
+export default DeleteModal;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/LpComments.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/LpComments.tsx
new file mode 100644
index 00000000..41f5da70
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/LpComments.tsx
@@ -0,0 +1,131 @@
+import React, { forwardRef } from 'react';
+import { Comment } from '../../types/lp-detail';
+import CommentSkeleton from './comment-skeleton';
+import CommentItem from './comment';
+import { useAuth } from '../../context/AuthContext';
+import useGetMyInfo from '../../hooks/queries/useGetMyInfo';
+
+interface LpCommentsProps {
+ lpId: number;
+ comments: Comment[];
+ commentContent: string;
+ setCommentContent: (content: string) => void;
+ commentOrder: 'desc' | 'asc';
+ setCommentOrder: (order: 'desc' | 'asc') => void;
+ onSubmitComment: (e: React.FormEvent) => void;
+ onUpdateComment: (commentId: number, content: string) => void;
+ onDeleteComment: (commentId: number) => void;
+ isLoading: boolean;
+ hasError: unknown;
+ hasNextPage: boolean | undefined;
+ isFetchingNextPage: boolean;
+ ref: any;
+}
+
+const LpComments: React.FC = forwardRef(
+ (
+ {
+ comments,
+ commentContent,
+ setCommentContent,
+ commentOrder,
+ setCommentOrder,
+ onSubmitComment,
+ onUpdateComment,
+ onDeleteComment,
+ isLoading,
+ hasError,
+ hasNextPage,
+ },
+ ref
+ ) => {
+ const { accessToken } = useAuth();
+ const { data: me } = useGetMyInfo(accessToken);
+
+ return (
+
+
+
+
댓글
+
+
+
+
+
+
+
+
+
+
+
+
+ {isLoading && (
+ <>
+
+
+
+ >
+ )}
+
+ {!isLoading && !hasError && (
+ <>
+ {comments.map((comment) => (
+
+ ))}
+ {hasNextPage &&
}
+ >
+ )}
+
+
+ );
+ }
+);
+
+export default LpComments;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/LpEdit.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/LpEdit.tsx
new file mode 100644
index 00000000..bdd7b4d5
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/LpEdit.tsx
@@ -0,0 +1,266 @@
+import React, { useState, useRef } from 'react';
+import { LpDetailData } from '../../types/lp-detail';
+import { Tag } from '../../types/lp';
+import { useUpdateLp } from '../../hooks/mutations/lp/useUpdateLp';
+import { queryClient } from '../../App';
+
+interface LpEditProps {
+ lpData: LpDetailData;
+ onCancel: () => void;
+ onDelete: () => void;
+}
+
+const LpEdit: React.FC = ({ lpData, onCancel, onDelete }) => {
+ const [editingField, setEditingField] = useState(null);
+ const [title, setTitle] = useState(lpData.title);
+ const [content, setContent] = useState(lpData.content);
+ const [thumbnail, setThumbnail] = useState(lpData.thumbnail);
+ const [previewThumbnail, setPreviewThumbnail] = useState(lpData.thumbnail);
+ const [tags, setTags] = useState(lpData.tags || []);
+ const [currentTag, setCurrentTag] = useState('');
+
+ const fileInputRef = useRef(null);
+ const { mutate: updateLp, isPending: isUpdating } = useUpdateLp();
+
+ const startEditing = (field: string) => {
+ setEditingField(field);
+ };
+
+ const handleThumbnailClick = () => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const result = reader.result as string;
+ setPreviewThumbnail(result);
+ setThumbnail(result);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleAddTag = () => {
+ const trimmed = currentTag.trim();
+ if (trimmed) {
+ const exists = tags.some(
+ (tag) => tag.name.toLowerCase() === trimmed.toLowerCase()
+ );
+ if (!exists) {
+ const newTag: Tag = {
+ id: tags.length > 0 ? Math.max(...tags.map((t) => t.id)) + 1 : 1,
+ name: trimmed,
+ };
+ setTags((prev) => [...prev, newTag]);
+ setCurrentTag('');
+ }
+ }
+ };
+
+ const handleRemoveTag = (tagToRemove: Tag) => {
+ setTags((prev) => prev.filter((tag) => tag.id !== tagToRemove.id));
+ };
+
+ const handleSaveChanges = () => {
+ if (!title.trim()) {
+ alert('제목은 빈칸일 수 없습니다.');
+ return;
+ }
+
+ updateLp(
+ {
+ lpId: lpData.id,
+ payload: {
+ title,
+ content,
+ thumbnail,
+ tags: tags.map((tag) => tag.name),
+ },
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpDetail', lpData.id] });
+ alert('LP가 성공적으로 업데이트되었습니다.');
+ onCancel();
+ },
+ }
+ );
+ };
+
+ return (
+
+
+
+
+ {lpData.author?.name?.charAt(0) || '?'}
+
+
+ {lpData.author?.name || '익명'}
+
+
+
+
+
+
+
+
+
+
+ {editingField === 'title' ? (
+
setTitle(e.target.value)}
+ onBlur={() => setEditingField(null)}
+ autoFocus
+ />
+ ) : (
+
startEditing('title')}
+ >
+ {title}
+
+ )}
+
+ {new Date(lpData.createdAt).toLocaleDateString()}
+
+
+
+
+
+

+
+
+
+
+ 클릭하여 LP 이미지 변경
+
+
+
+
+ {editingField === 'content' ? (
+
+
+ );
+};
+
+export default LpEdit;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/LpInfo.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/LpInfo.tsx
new file mode 100644
index 00000000..b96cf609
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/LpInfo.tsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import { LpDetailData } from '../../types/lp-detail';
+
+interface LpInfoProps {
+ lpData: LpDetailData;
+ isLiked: boolean | undefined;
+ onLike: () => void;
+ onDislike: () => void;
+ onEdit: () => void;
+ onDelete: () => void;
+}
+
+const LpInfo: React.FC = ({
+ lpData,
+ isLiked,
+ onLike,
+ onDislike,
+ onEdit,
+ onDelete,
+}) => {
+ return (
+
+
+
+
+ {lpData.author?.avatar ? (
+

+ ) : (
+ lpData.author?.name.charAt(0)
+ )}
+
+
+ {lpData.author?.name || '익명'}
+
+
+
+
+
+
+
+
+
+
{lpData.title}
+
+ {new Date(lpData.createdAt).toLocaleDateString()}
+
+
+
+
+
+

+
+
+
+
+
+
{lpData.content}
+
+
+ {lpData.tags.length > 0 ? (
+ lpData.tags.map((tag) => (
+
+ #{tag.name}
+
+ ))
+ ) : (
+ 태그 없음
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default LpInfo;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/comment-skeleton.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/comment-skeleton.tsx
new file mode 100644
index 00000000..c67eb435
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/comment-skeleton.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const CommentSkeleton: React.FC = () => (
+
+);
+
+export default CommentSkeleton;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/comment.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/comment.tsx
new file mode 100644
index 00000000..f3b81302
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/comment.tsx
@@ -0,0 +1,135 @@
+import React, { useState, useRef, useEffect } from 'react';
+import type { Comment } from '../../types/lp-detail';
+
+interface CommentProps {
+ comment: Comment;
+ isMyComment: boolean;
+ onUpdate: (id: number, content: string) => void;
+ onDelete: (id: number) => void;
+}
+
+const Comment: React.FC = ({
+ comment,
+ isMyComment,
+ onUpdate,
+ onDelete,
+}) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedContent, setEditedContent] = useState(comment.content);
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const menuRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
+ setIsMenuOpen(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const handleSubmitEdit = () => {
+ if (!editedContent.trim()) return;
+ onUpdate(comment.id, editedContent);
+ setIsEditing(false);
+ };
+
+ return (
+
+
+
+ {comment.author.avatar ? (
+

+ ) : (
+ comment.author.name.charAt(0)
+ )}
+
+
+
+
{comment.author.name}
+
+
+ {new Date(comment.createdAt).toLocaleDateString()}
+
+
+ {isMyComment && (
+
+
+
+ {isMenuOpen && (
+
+
+
+
+
+
+ )}
+
+ )}
+
+
+
+ {isEditing ? (
+ <>
+
+
+
+ );
+};
+
+export default Comment;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/lp-card-skeleton-list.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/lp-card-skeleton-list.tsx
new file mode 100644
index 00000000..82b79b53
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/lp-card-skeleton-list.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import LpCardSkeleton from './lp-card-skeleton';
+
+interface LpCardSkeletonListProps {
+ count: number;
+}
+
+const LpCardSkeletonList: React.FC = ({ count }) => {
+ return (
+ <>
+ {new Array(count).fill(0).map((_, idx: number) => (
+
+ ))}
+ >
+ );
+};
+
+export default LpCardSkeletonList;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/lp-card-skeleton.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/lp-card-skeleton.tsx
new file mode 100644
index 00000000..ee8af18c
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/lp-card-skeleton.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+const LpCardSkeleton: React.FC = () => {
+ return (
+
+ );
+};
+
+export default LpCardSkeleton;
diff --git a/Week8/wantkdd/mission2/src/components/lp-detail/lp-card.tsx b/Week8/wantkdd/mission2/src/components/lp-detail/lp-card.tsx
new file mode 100644
index 00000000..b4322eab
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/lp-detail/lp-card.tsx
@@ -0,0 +1,71 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+import { Lp } from '../../types/lp';
+
+interface LpCardProps {
+ lp: Lp;
+}
+
+const LpCard: React.FC = ({ lp }) => {
+ const navigate = useNavigate();
+ const { accessToken } = useAuth();
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ const handleImageLoad = () => {
+ setImageLoaded(true);
+ };
+ const handleCardClick = () => {
+ if (!accessToken) {
+ if (window.confirm('로그인이 필요합니다. 로그인 하시겠습니까?')) {
+ navigate('/login-page');
+ }
+ return;
+ }
+ navigate(`/lp/${lp.id}`);
+ };
+
+ return (
+
+
+
+ {!imageLoaded && (
+
+ )}
+

+
+
+
+
+
+
+
+
+ {lp.title}
+
+
+ {new Date(lp.createdAt).toLocaleDateString()}
+
+
+
+ ♥
+
+ {lp.likes?.length ?? 0}
+
+
+
+
+
+
+ );
+};
+
+export default LpCard;
diff --git a/Week8/wantkdd/mission2/src/components/navbar.tsx b/Week8/wantkdd/mission2/src/components/navbar.tsx
new file mode 100644
index 00000000..6bd1cda3
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/navbar.tsx
@@ -0,0 +1,81 @@
+import { useEffect, useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { useLogout } from '../hooks/mutations/user/useLogout';
+
+interface NavbarProps {
+ onMenuClick: () => void;
+}
+
+const Navbar = ({ onMenuClick }: NavbarProps) => {
+ const { accessToken, userInfo } = useAuth();
+ const { mutateAsync: logout } = useLogout();
+ const navigate = useNavigate();
+
+ const [userName, setUserName] = useState(userInfo?.name);
+
+ useEffect(() => {
+ if (userInfo?.name) {
+ setUserName(userInfo.name);
+ }
+ }, [userInfo]);
+
+ const handleLogout = async () => {
+ await logout();
+ navigate('/login-page');
+ };
+
+ return (
+
+ );
+};
+
+export default Navbar;
diff --git a/Week8/wantkdd/mission2/src/components/order-button.tsx b/Week8/wantkdd/mission2/src/components/order-button.tsx
new file mode 100644
index 00000000..7727b0ba
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/order-button.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { PAGINATION_ORDER } from '../enums/commons';
+
+interface OrderButtonProps {
+ order: PAGINATION_ORDER;
+ setOrder: (order: PAGINATION_ORDER) => void;
+}
+
+const OrderButton: React.FC = ({ order, setOrder }) => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default OrderButton;
diff --git a/Week8/wantkdd/mission2/src/components/sidebar.tsx b/Week8/wantkdd/mission2/src/components/sidebar.tsx
new file mode 100644
index 00000000..1535a26a
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/components/sidebar.tsx
@@ -0,0 +1,117 @@
+import React, { useState } from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import useDeleteUser from '../hooks/mutations/user/useDeleteUser';
+
+interface SidebarProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+const Sidebar: React.FC = ({ open, onClose }) => {
+ const location = useLocation();
+ const { accessToken } = useAuth();
+ const [showModal, setShowModal] = useState(false);
+
+ const { mutate: deleteUserAccount, isPending } = useDeleteUser();
+
+ const isActive = (path: string) => {
+ return location.pathname === path;
+ };
+
+ const handleDeleteClick = () => {
+ setShowModal(true);
+ };
+
+ const handleConfirmDelete = () => {
+ deleteUserAccount();
+ setShowModal(false);
+ };
+
+ const handleCancelDelete = () => {
+ setShowModal(false);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ 🔍 찾기
+
+
+
+ 👤 마이페이지
+
+
+
+ {accessToken && (
+
+ )}
+
+
+
+ {showModal && (
+
+
+
+ 회원 탈퇴 확인
+
+
정말 탈퇴하시겠습니까?
+
+
+
+
+
+
+ )}
+ >
+ );
+};
+
+export default Sidebar;
diff --git a/Week8/wantkdd/mission2/src/constants/key.ts b/Week8/wantkdd/mission2/src/constants/key.ts
new file mode 100644
index 00000000..41326a26
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/constants/key.ts
@@ -0,0 +1,9 @@
+export const LOCAL_STORAGE_KEY = {
+ accessToken: 'accessToken',
+ refreshToken: 'refreshToken',
+};
+
+export const QUERY_KEY = {
+ lps: 'lps',
+ myInfo: 'myInfo',
+};
diff --git a/Week8/wantkdd/mission2/src/context/AuthContext.tsx b/Week8/wantkdd/mission2/src/context/AuthContext.tsx
new file mode 100644
index 00000000..b1d4444f
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/context/AuthContext.tsx
@@ -0,0 +1,138 @@
+import {
+ createContext,
+ PropsWithChildren,
+ useState,
+ useContext,
+ useEffect,
+} from 'react';
+import { RequestSigninDto } from '../types/auth';
+import { LOCAL_STORAGE_KEY } from '../constants/key';
+import { postLogout, postSignin } from '../apis/auth';
+import { useLocalStorage } from '../hooks/custom/useLocalStorage';
+import axios from 'axios';
+import { axiosInstance } from '../apis/axios';
+
+interface UserInfo {
+ id: number;
+ name: string;
+ email: string;
+ bio?: string | null;
+ avatar?: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+interface AuthContextType {
+ accessToken: string | null;
+ refreshToken: string | null;
+ userInfo: UserInfo | null;
+ setUserInfo: (userInfo: UserInfo) => void;
+ login: (signinData: RequestSigninDto) => Promise;
+ logout: () => Promise;
+}
+
+export const AuthContext = createContext({
+ accessToken: null,
+ refreshToken: null,
+ userInfo: null,
+ setUserInfo: () => {},
+ login: async () => {},
+ logout: async () => {},
+});
+
+export const AuthProvider = ({ children }: PropsWithChildren) => {
+ const {
+ getItem: getAccessTokenFromStorage,
+ setItem: setAccessTokenInStorage,
+ removeItem: removeAccessTokenFromStorage,
+ } = useLocalStorage(LOCAL_STORAGE_KEY.accessToken);
+ const {
+ getItem: getRefreshTokenFromStorage,
+ setItem: setRefreshTokenInStorage,
+ removeItem: removeRefreshTokenFromStorage,
+ } = useLocalStorage(LOCAL_STORAGE_KEY.refreshToken);
+
+ const [accessToken, setAccessToken] = useState(
+ getAccessTokenFromStorage()
+ );
+ const [refreshToken, setRefreshToken] = useState(
+ getRefreshTokenFromStorage()
+ );
+ const [userInfo, setUserInfo] = useState(null);
+ useEffect(() => {
+ const fetchUserInfo = async () => {
+ if (accessToken) {
+ try {
+ const response = await axiosInstance.get('/v1/users/me');
+ setUserInfo(response.data.data);
+ } catch (error) {
+ console.error('사용자 정보를 가져오는데 실패했습니다.', error);
+ }
+ }
+ };
+
+ fetchUserInfo();
+ }, [accessToken]);
+
+ const login = async (signinData: RequestSigninDto) => {
+ try {
+ const { data } = await postSignin(signinData);
+ if (data) {
+ const newAccessToken = data.accessToken;
+ const newRefreshToken = data.refreshToken;
+
+ setAccessTokenInStorage(newAccessToken);
+ setRefreshTokenInStorage(newRefreshToken);
+
+ setAccessToken(newAccessToken);
+ setRefreshToken(newRefreshToken);
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response?.status === 401) {
+ alert('이메일 또는 비밀번호가 올바르지 않습니다.');
+ } else {
+ alert('로그인 중 오류가 발생했습니다.');
+ }
+ }
+ }
+ };
+
+ const logout = async () => {
+ try {
+ await postLogout();
+
+ removeAccessTokenFromStorage();
+ removeRefreshTokenFromStorage();
+
+ setAccessToken(null);
+ setRefreshToken(null);
+
+ alert('로그아웃 성공!');
+ } catch (error) {
+ console.error(error);
+ }
+ };
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('AuthContext 찾을 수 없음');
+ }
+ return context;
+};
diff --git a/Week8/wantkdd/mission2/src/enums/commons.ts b/Week8/wantkdd/mission2/src/enums/commons.ts
new file mode 100644
index 00000000..2190b698
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/enums/commons.ts
@@ -0,0 +1,4 @@
+export enum PAGINATION_ORDER {
+ 'asc' = 'asc',
+ 'desc' = 'desc',
+}
diff --git a/Week8/wantkdd/mission2/src/hooks/custom/useCustomFetch.ts b/Week8/wantkdd/mission2/src/hooks/custom/useCustomFetch.ts
new file mode 100644
index 00000000..42621d80
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/custom/useCustomFetch.ts
@@ -0,0 +1,59 @@
+import axios from 'axios';
+import { useState, useEffect } from 'react';
+
+interface ApiResponse {
+ data: T | null;
+ isLoading: boolean;
+ error: string | null;
+}
+
+interface UseFetchResult extends ApiResponse {
+ refetch: () => Promise;
+}
+
+export const useCustomFetch = (
+ url: string,
+ dependencies: React.DependencyList = []
+): UseFetchResult => {
+ const [state, setState] = useState>({
+ data: null,
+ isLoading: true,
+ error: null,
+ });
+
+ const fetchData = async () => {
+ setState((prev) => ({ ...prev, isLoading: true }));
+ try {
+ const response = await axios(url, {
+ headers: {
+ Authorization: `Bearer ${import.meta.env.VITE_TMDB_KEY}`,
+ },
+ });
+ setState({
+ data: response.data,
+ isLoading: false,
+ error: null,
+ });
+ } catch (err) {
+ setState({
+ data: null,
+ isLoading: false,
+ error: '🚨요청에 실패했습니다. 무언가가 잘못 된 것이 분명하다..🚨',
+ });
+ console.error(err);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, [...dependencies, url]);
+
+ const refetch = async () => {
+ await fetchData();
+ };
+
+ return {
+ ...state,
+ refetch,
+ };
+};
diff --git a/Week8/wantkdd/mission2/src/hooks/custom/useDebounce.ts b/Week8/wantkdd/mission2/src/hooks/custom/useDebounce.ts
new file mode 100644
index 00000000..322d5ea3
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/custom/useDebounce.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from 'react';
+
+function useDebounce(value: T, delay: number) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+ //value, delay 변경될 때마다 실행
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ //delay시간 이후 value를 debouncedValue로 업데이트하는 타이머 시작
+ setDebouncedValue(value);
+ }, delay);
+ //value 변경되면 기존 타이머를 지워서 업데이트 취소
+ //값이 계속 바뀔 때마다 마지막에 멈춘 값만 업데이트
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, delay]);
+ //최종적으로 잠시 기다린 후의 값을 반환
+ return debouncedValue;
+}
+
+export default useDebounce;
diff --git a/Week8/wantkdd/mission2/src/hooks/custom/useForm.ts b/Week8/wantkdd/mission2/src/hooks/custom/useForm.ts
new file mode 100644
index 00000000..e8202d71
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/custom/useForm.ts
@@ -0,0 +1,53 @@
+import { ChangeEvent, useEffect, useState } from 'react';
+
+interface UseFormProps {
+ initialValue: T;
+ validate: (values: T) => Record;
+}
+
+function useForm({ initialValue, validate }: UseFormProps) {
+ const [values, setValues] = useState(initialValue);
+ const [touched, setTouched] = useState>();
+ const [errors, setErrors] = useState>();
+
+ const handleChange = (name: keyof T, text: string) => {
+ setValues({
+ ...values,
+ [name]: text,
+ });
+ };
+
+ const handleBlur = (name: keyof T) => {
+ setTouched({
+ ...touched,
+ [name]: true,
+ });
+ };
+
+ const getInputProps = (name: keyof T) => {
+ const value = values[name];
+ const onChange = (e: ChangeEvent) =>
+ handleChange(name, e.target.value);
+
+ const onBlur = () => handleBlur(name);
+
+ return { value, onChange, onBlur };
+ };
+
+ useEffect(() => {
+ const someTouched = touched ? Object.values(touched).some((t) => t) : false;
+ if (someTouched) {
+ const newErrors = validate(values);
+ setErrors(newErrors);
+ }
+ }, [values, validate, touched]);
+
+ return {
+ values,
+ errors,
+ touched,
+ getInputProps,
+ };
+}
+
+export default useForm;
diff --git a/Week8/wantkdd/mission2/src/hooks/custom/useLocalStorage.ts b/Week8/wantkdd/mission2/src/hooks/custom/useLocalStorage.ts
new file mode 100644
index 00000000..a0058c67
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/custom/useLocalStorage.ts
@@ -0,0 +1,39 @@
+export const useLocalStorage = (key: string) => {
+ const setItem = (value: unknown) => {
+ try {
+ // 문자열이면 그대로 저장, 아니면 JSON.stringify 사용
+ const valueToStore =
+ typeof value === 'string' ? value : JSON.stringify(value);
+ window.localStorage.setItem(key, valueToStore);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ const getItem = () => {
+ try {
+ const item = window.localStorage.getItem(key);
+ if (!item) return null;
+
+ // 문자열인지 JSON인지 판단하여 적절히 처리
+ try {
+ return JSON.parse(item);
+ } catch {
+ // JSON 파싱에 실패하면 일반 문자열로 반환
+ return item;
+ }
+ } catch (error) {
+ console.log(error);
+ return null;
+ }
+ };
+
+ const removeItem = () => {
+ try {
+ window.localStorage.removeItem(key);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+ return { setItem, getItem, removeItem };
+};
diff --git a/Week8/wantkdd/mission2/src/hooks/custom/useLpComments.ts b/Week8/wantkdd/mission2/src/hooks/custom/useLpComments.ts
new file mode 100644
index 00000000..4995f78d
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/custom/useLpComments.ts
@@ -0,0 +1,82 @@
+import { useState } from 'react';
+import { useInView } from 'react-intersection-observer';
+import { useGetInfiniteComments } from '../queries/useGetInfiniteLpDetail';
+import usePostComment from '../mutations/comments/usePostComment';
+import useUpdateComment from '../mutations/comments/useUpdateComment';
+import useDeleteComment from '../mutations/comments/useDeleteComment';
+
+const useLpComments = (lpId: number) => {
+ const [commentOrder, setCommentOrder] = useState<'desc' | 'asc'>('desc');
+ const [commentContent, setCommentContent] = useState('');
+ const COMMENTS_PER_PAGE = 5;
+
+ const { ref, inView } = useInView({
+ threshold: 0.5,
+ triggerOnce: false,
+ });
+
+ const {
+ data: commentsData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading: isCommentsLoading,
+ error: commentsError,
+ refetch,
+ } = useGetInfiniteComments(lpId, COMMENTS_PER_PAGE, '', commentOrder);
+
+ const { mutate: postComment } = usePostComment(lpId);
+ const { mutate: updateComment } = useUpdateComment(lpId);
+ const { mutate: deleteCommentMutate } = useDeleteComment(lpId);
+
+ const handleSubmitComment = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!commentContent.trim()) return;
+
+ postComment(commentContent, {
+ onSuccess: () => {
+ setCommentContent('');
+ refetch();
+ },
+ });
+ };
+
+ const handleUpdateComment = (commentId: number, content: string) => {
+ updateComment(
+ { commentId, content },
+ {
+ onSuccess: () => {
+ refetch();
+ },
+ }
+ );
+ };
+
+ const handleDeleteComment = (commentId: number) => {
+ deleteCommentMutate(commentId, {
+ onSuccess: () => {
+ refetch();
+ },
+ });
+ };
+
+ return {
+ commentsData,
+ isCommentsLoading,
+ commentsError,
+ commentOrder,
+ setCommentOrder,
+ commentContent,
+ setCommentContent,
+ handleSubmitComment,
+ handleUpdateComment,
+ handleDeleteComment,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ ref,
+ inView,
+ };
+};
+
+export default useLpComments;
diff --git a/Week8/wantkdd/mission2/src/hooks/custom/useLpDetail.ts b/Week8/wantkdd/mission2/src/hooks/custom/useLpDetail.ts
new file mode 100644
index 00000000..9a0ae8ec
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/custom/useLpDetail.ts
@@ -0,0 +1,48 @@
+import { useState } from 'react';
+import { useGetLpDetail } from '../queries/useGetLpDetail';
+import useGetMyInfo from '../queries/useGetMyInfo';
+import usePostLike from '../mutations/like/usePostLike';
+import useDeleteLike from '../mutations/like/useDeleteLikte';
+import { useDeleteLp } from '../mutations/lp/useDeleteLp';
+
+const useLpDetail = (lpId: number, accessToken: string | null) => {
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [isPlaying, setIsPlaying] = useState(true);
+
+ const {
+ data: lpData,
+ isLoading: isLpLoading,
+ error: lpError,
+ } = useGetLpDetail(lpId);
+
+ const { data: me } = useGetMyInfo(accessToken);
+ const { mutate: likeMutate } = usePostLike();
+ const { mutate: disLikeMutate } = useDeleteLike();
+ const { mutate: deleteLp } = useDeleteLp();
+
+ const isLiked = lpData?.likes.some((like) => like.userId === me?.data.id);
+
+ const handleLikeLp = () => {
+ likeMutate(lpId);
+ };
+
+ const handleDislikeLp = () => {
+ disLikeMutate(lpId);
+ };
+
+ return {
+ lpData,
+ isLpLoading,
+ lpError,
+ isEditMode,
+ setIsEditMode,
+ isPlaying,
+ setIsPlaying,
+ handleLikeLp,
+ handleDislikeLp,
+ isLiked,
+ deleteLp,
+ };
+};
+
+export default useLpDetail;
diff --git a/Week8/wantkdd/mission2/src/hooks/custom/useThrottle.ts b/Week8/wantkdd/mission2/src/hooks/custom/useThrottle.ts
new file mode 100644
index 00000000..4cf2ebb3
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/custom/useThrottle.ts
@@ -0,0 +1,25 @@
+import { useEffect, useRef, useState } from 'react';
+
+function useThrottle(value: T, delay: number = 500): T {
+ const [throttledValue, setThrottledValue] = useState(value);
+
+ const lastExcuted = useRef(Date.now());
+
+ useEffect(() => {
+ if (Date.now() >= lastExcuted.current + delay) {
+ lastExcuted.current = Date.now();
+ setThrottledValue(value);
+ } else {
+ const timerId = setTimeout(() => {
+ lastExcuted.current = Date.now();
+ setThrottledValue(value);
+ }, delay);
+ return () => {
+ clearTimeout(timerId);
+ };
+ }
+ }, [value, delay]);
+ return throttledValue;
+}
+
+export default useThrottle;
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/comments/useDeleteComment.ts b/Week8/wantkdd/mission2/src/hooks/mutations/comments/useDeleteComment.ts
new file mode 100644
index 00000000..fcf73daf
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/comments/useDeleteComment.ts
@@ -0,0 +1,14 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deleteComment } from '../../../apis/lp-detail';
+
+const useDeleteComment = (lpId: number) => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (commentId: number) => deleteComment(lpId, commentId),
+ onSuccess: () => {
+ queryClient.invalidateQueries(['comments', lpId]);
+ },
+ });
+};
+
+export default useDeleteComment;
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/comments/usePostComment.ts b/Week8/wantkdd/mission2/src/hooks/mutations/comments/usePostComment.ts
new file mode 100644
index 00000000..658c993b
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/comments/usePostComment.ts
@@ -0,0 +1,15 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { createComment } from '../../../apis/lp-detail';
+
+const usePostComment = (lpId: number) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (content: string) => createComment(String(lpId), content),
+ onSuccess: () => {
+ queryClient.invalidateQueries(['comments', lpId]);
+ },
+ });
+};
+
+export default usePostComment;
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/comments/useUpdateComment.ts b/Week8/wantkdd/mission2/src/hooks/mutations/comments/useUpdateComment.ts
new file mode 100644
index 00000000..2ae1ea36
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/comments/useUpdateComment.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateComment } from '../../../apis/lp-detail';
+
+const useUpdateComment = (lpId: number) => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({
+ commentId,
+ content,
+ }: {
+ commentId: number;
+ content: string;
+ }) => updateComment(lpId, commentId, content),
+ onSuccess: () => {
+ queryClient.invalidateQueries(['comments', lpId]);
+ },
+ });
+};
+
+export default useUpdateComment;
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/like/useDeleteLikte.ts b/Week8/wantkdd/mission2/src/hooks/mutations/like/useDeleteLikte.ts
new file mode 100644
index 00000000..ff2793f8
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/like/useDeleteLikte.ts
@@ -0,0 +1,52 @@
+import { useMutation } from '@tanstack/react-query';
+import { deleteLike } from '../../../apis/like';
+import { queryClient } from '../../../App';
+import { QUERY_KEY } from '../../../constants/key';
+import { LpDetailData } from '../../../types/lp-detail';
+import useGetMyInfo from '../../queries/useGetMyInfo';
+import { useAuth } from '../../../context/AuthContext';
+
+function useDeleteLike() {
+ const { accessToken } = useAuth();
+ const { data: me } = useGetMyInfo(accessToken);
+
+ return useMutation({
+ mutationFn: deleteLike,
+
+ onMutate: async (lpId: number) => {
+ //Optimistic update
+ await queryClient.cancelQueries({ queryKey: [QUERY_KEY.lps, lpId] });
+ const previousData = queryClient.getQueryData([
+ QUERY_KEY.lps,
+ lpId,
+ ]);
+
+ if (previousData && me?.data.id) {
+ queryClient.setQueryData([QUERY_KEY.lps, lpId], {
+ ...previousData,
+ likes: previousData.likes.filter(
+ (like) => like.userId !== me.data.id
+ ),
+ });
+ }
+
+ return { previousData };
+ },
+
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.lps, data.data.lpId],
+ exact: true,
+ });
+ },
+ onError: (error, lpId, context) => {
+ console.error('좋아요 취소 실패:', error);
+
+ if (context?.previousData) {
+ queryClient.setQueryData([QUERY_KEY.lps, lpId], context.previousData);
+ }
+ },
+ });
+}
+
+export default useDeleteLike;
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/like/usePostLike.ts b/Week8/wantkdd/mission2/src/hooks/mutations/like/usePostLike.ts
new file mode 100644
index 00000000..b3b2590d
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/like/usePostLike.ts
@@ -0,0 +1,50 @@
+import { useMutation } from '@tanstack/react-query';
+import { postLike } from '../../../apis/like';
+import { QUERY_KEY } from '../../../constants/key';
+import { queryClient } from '../../../App';
+import { LpDetailData } from '../../../types/lp-detail';
+import useGetMyInfo from '../../queries/useGetMyInfo';
+import { useAuth } from '../../../context/AuthContext';
+
+function usePostLike() {
+ const { accessToken } = useAuth();
+ const { data: me } = useGetMyInfo(accessToken);
+
+ return useMutation({
+ mutationFn: postLike,
+
+ onMutate: async (lpId: number) => {
+ await queryClient.cancelQueries({ queryKey: [QUERY_KEY.lps, lpId] });
+
+ const previousData = queryClient.getQueryData([
+ QUERY_KEY.lps,
+ lpId,
+ ]);
+
+ if (previousData && me?.data.id) {
+ queryClient.setQueryData([QUERY_KEY.lps, lpId], {
+ ...previousData,
+ likes: [
+ ...previousData.likes,
+ {
+ id: Date.now(),
+ userId: me.data.id,
+ lpId: lpId,
+ },
+ ],
+ });
+ }
+
+ return { previousData };
+ },
+
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.lps, data.data.lpId],
+ exact: true,
+ });
+ },
+ });
+}
+
+export default usePostLike;
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/lp/useDeleteLp.ts b/Week8/wantkdd/mission2/src/hooks/mutations/lp/useDeleteLp.ts
new file mode 100644
index 00000000..76ff1350
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/lp/useDeleteLp.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deleteLp } from '../../../apis/lp';
+import { QUERY_KEY } from '../../../constants/key';
+import { useNavigate } from 'react-router-dom';
+
+export const useDeleteLp = () => {
+ const queryClient = useQueryClient();
+ const navigate = useNavigate();
+
+ return useMutation({
+ mutationFn: (lpId: number) => deleteLp(lpId),
+ onSuccess: (_, lpId) => {
+ queryClient.removeQueries({
+ queryKey: [QUERY_KEY.lps, lpId],
+ });
+
+ navigate('/');
+ },
+ });
+};
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/lp/usePostLp.ts b/Week8/wantkdd/mission2/src/hooks/mutations/lp/usePostLp.ts
new file mode 100644
index 00000000..4b87463c
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/lp/usePostLp.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { postLp } from '../../../apis/addLp';
+import { QUERY_KEY } from '../../../constants/key';
+
+export default function usePostLp() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: postLp,
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY.lps] });
+
+ if (data?.data?.id) {
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.lps, data.data.id],
+ });
+ }
+ },
+ });
+}
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/lp/useUpdateLp.ts b/Week8/wantkdd/mission2/src/hooks/mutations/lp/useUpdateLp.ts
new file mode 100644
index 00000000..899e95d0
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/lp/useUpdateLp.ts
@@ -0,0 +1,23 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateLp } from '../../../apis/lp-detail';
+import { UpdateLpRequest } from '../../../types/lp-detail';
+
+export const useUpdateLp = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ lpId,
+ payload,
+ }: {
+ lpId: number;
+ payload: UpdateLpRequest;
+ }) => updateLp({ lpId, payload }),
+
+ onSuccess: (_data, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ['lpDetail', variables.lpId],
+ });
+ },
+ });
+};
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/user/useDeleteUser.ts b/Week8/wantkdd/mission2/src/hooks/mutations/user/useDeleteUser.ts
new file mode 100644
index 00000000..a0a7b26c
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/user/useDeleteUser.ts
@@ -0,0 +1,20 @@
+import { useMutation } from '@tanstack/react-query';
+import { useNavigate } from 'react-router-dom';
+import { deleteUser } from '../../../apis/auth';
+import { useAuth } from '../../../context/AuthContext';
+
+const useDeleteUser = () => {
+ const navigate = useNavigate();
+ const { logout } = useAuth();
+
+ return useMutation({
+ mutationFn: deleteUser,
+ onSuccess: async () => {
+ alert('회원 탈퇴가 완료되었습니다.');
+ await logout();
+ navigate('/');
+ },
+ });
+};
+
+export default useDeleteUser;
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/user/useLogin.ts b/Week8/wantkdd/mission2/src/hooks/mutations/user/useLogin.ts
new file mode 100644
index 00000000..f7eab0c7
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/user/useLogin.ts
@@ -0,0 +1,21 @@
+import { useMutation } from '@tanstack/react-query';
+import { SigninFormFields } from '../../../utils/validate';
+import { useAuth } from '../../../context/AuthContext';
+import axios from 'axios';
+
+export const useLogin = () => {
+ const { login } = useAuth();
+
+ return useMutation({
+ mutationFn: (data: SigninFormFields) => {
+ return login(data);
+ },
+ onError: (error) => {
+ if (axios.isAxiosError(error) && error.response?.status === 401) {
+ alert('이메일 또는 비밀번호가 올바르지 않습니다.');
+ } else {
+ alert('로그인 중 오류가 발생했습니다.');
+ }
+ },
+ });
+};
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/user/useLogout.ts b/Week8/wantkdd/mission2/src/hooks/mutations/user/useLogout.ts
new file mode 100644
index 00000000..cccfd892
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/user/useLogout.ts
@@ -0,0 +1,13 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { useAuth } from '../../../context/AuthContext';
+
+export const useLogout = () => {
+ const { logout } = useAuth();
+
+ return useMutation({
+ mutationFn: () => {
+ return logout();
+ },
+ });
+};
diff --git a/Week8/wantkdd/mission2/src/hooks/mutations/user/useUpdateMyInfo.ts b/Week8/wantkdd/mission2/src/hooks/mutations/user/useUpdateMyInfo.ts
new file mode 100644
index 00000000..6a5d7446
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/mutations/user/useUpdateMyInfo.ts
@@ -0,0 +1,57 @@
+import { useMutation } from '@tanstack/react-query';
+import { patchUserInfo } from '../../../apis/auth';
+import { queryClient } from '../../../App';
+import { QUERY_KEY } from '../../../constants/key';
+import { PatchUserInfoDto, ResponseMyInfoDto } from '../../../types/auth';
+import { useAuth } from '../../../context/AuthContext';
+
+function useUpdateUserInfo() {
+ const { setUserInfo } = useAuth();
+
+ return useMutation({
+ mutationFn: (body: PatchUserInfoDto) => patchUserInfo(body),
+
+ onMutate: async (newUserData: PatchUserInfoDto) => {
+ await queryClient.cancelQueries({ queryKey: [QUERY_KEY.myInfo] });
+
+ const previousUserInfo = queryClient.getQueryData([
+ QUERY_KEY.myInfo,
+ ]);
+
+ if (previousUserInfo) {
+ queryClient.setQueryData([QUERY_KEY.myInfo], {
+ ...previousUserInfo,
+ data: {
+ ...previousUserInfo.data,
+ name: newUserData.name,
+ bio: newUserData.bio || previousUserInfo.data.bio,
+ avatar: newUserData.avatar || previousUserInfo.data.avatar,
+ },
+ });
+
+ setUserInfo({
+ ...previousUserInfo.data,
+ name: newUserData.name,
+ bio: newUserData.bio || previousUserInfo.data.bio,
+ avatar: newUserData.avatar || previousUserInfo.data.avatar,
+ });
+ }
+
+ return { previousUserInfo };
+ },
+
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY.myInfo] });
+ },
+ onError: (error, _, context) => {
+ console.error('사용자 정보 업데이트 실패:', error);
+
+ if (context?.previousUserInfo) {
+ queryClient.setQueryData([QUERY_KEY.myInfo], context.previousUserInfo);
+ setUserInfo(context.previousUserInfo.data);
+ }
+ },
+ });
+}
+
+export default useUpdateUserInfo;
diff --git a/Week8/wantkdd/mission2/src/hooks/queries/useGetInfiniteLpDetail.ts b/Week8/wantkdd/mission2/src/hooks/queries/useGetInfiniteLpDetail.ts
new file mode 100644
index 00000000..76e0ad55
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/queries/useGetInfiniteLpDetail.ts
@@ -0,0 +1,29 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { getComments } from '../../apis/lp-detail';
+import { QUERY_KEY } from '../../constants/key';
+import { CommentsResponse } from '../../types/lp-detail';
+
+export function useGetInfiniteComments(
+ lpId: number,
+ limit: number,
+ search: string,
+ order: 'desc' | 'asc'
+) {
+ return useInfiniteQuery({
+ queryKey: [QUERY_KEY.lps, lpId, 'comments', order, limit],
+ queryFn: ({ pageParam }) =>
+ getComments({
+ lpId,
+ cursor: pageParam,
+ limit,
+ order,
+ search,
+ }),
+ initialPageParam: 0,
+ getNextPageParam: (lastPage) =>
+ lastPage.data.hasNext ? lastPage.data.nextCursor : undefined,
+ staleTime: 1000 * 60 * 5,
+ gcTime: 1000 * 60 * 10,
+ enabled: !!lpId,
+ });
+}
diff --git a/Week8/wantkdd/mission2/src/hooks/queries/useGetInfiniteLpList.ts b/Week8/wantkdd/mission2/src/hooks/queries/useGetInfiniteLpList.ts
new file mode 100644
index 00000000..f7640b94
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/queries/useGetInfiniteLpList.ts
@@ -0,0 +1,23 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { getLpList } from '../../apis/lp';
+import { QUERY_KEY } from '../../constants/key';
+
+function useGetInfiniteLpList(
+ limit: number,
+ order: 'desc' | 'asc',
+ search?: string
+) {
+ return useInfiniteQuery({
+ queryFn: ({ pageParam }) =>
+ getLpList({ cursor: pageParam, limit, order, search }),
+ queryKey: [QUERY_KEY.lps, order, limit, search],
+ initialPageParam: 0,
+ getNextPageParam: (lastPage) => {
+ return lastPage.data.hasNext ? lastPage.data.nextCursor : undefined;
+ },
+ staleTime: 1000 * 60 * 5,
+ gcTime: 1000 * 60 * 10,
+ });
+}
+
+export default useGetInfiniteLpList;
diff --git a/Week8/wantkdd/mission2/src/hooks/queries/useGetLpDetail.ts b/Week8/wantkdd/mission2/src/hooks/queries/useGetLpDetail.ts
new file mode 100644
index 00000000..a1e90017
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/queries/useGetLpDetail.ts
@@ -0,0 +1,12 @@
+import { useQuery } from '@tanstack/react-query';
+import { getLpDetail } from '../../apis/lp-detail';
+import { QUERY_KEY } from '../../constants/key';
+import { LpDetailData } from '../../types/lp-detail';
+
+export function useGetLpDetail(id: number) {
+ return useQuery({
+ queryKey: [QUERY_KEY.lps, id],
+ queryFn: () => getLpDetail(id),
+ staleTime: 1000 * 60 * 5,
+ });
+}
diff --git a/Week8/wantkdd/mission2/src/hooks/queries/useGetLpList.ts b/Week8/wantkdd/mission2/src/hooks/queries/useGetLpList.ts
new file mode 100644
index 00000000..6c5a59d1
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/queries/useGetLpList.ts
@@ -0,0 +1,23 @@
+import { useQuery } from '@tanstack/react-query';
+import { PaginationDto } from '../../types/common';
+import { getLpList } from '../../apis/lp';
+import { QUERY_KEY } from '../../constants/key';
+
+import { ResponseLpListDto } from '../../types/lp';
+
+function useGetLpList({
+ order = 'desc',
+ limit = 40,
+ search = '',
+}: PaginationDto) {
+ return useQuery({
+ queryKey: [QUERY_KEY.lps, order, limit, search],
+ queryFn: () => getLpList({ order, limit, search }),
+ staleTime: 1000 * 60 * 5,
+ gcTime: 1000 * 60 * 10,
+ retry: 3,
+ select: (data: ResponseLpListDto) => data.data.data,
+ });
+}
+
+export default useGetLpList;
diff --git a/Week8/wantkdd/mission2/src/hooks/queries/useGetMyInfo.ts b/Week8/wantkdd/mission2/src/hooks/queries/useGetMyInfo.ts
new file mode 100644
index 00000000..4ed74bc2
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/queries/useGetMyInfo.ts
@@ -0,0 +1,14 @@
+import { useQuery } from '@tanstack/react-query';
+import { QUERY_KEY } from '../../constants/key';
+
+import { getMyInfo } from '../../apis/auth';
+
+function useGetMyInfo(accessToken: string | null) {
+ return useQuery({
+ queryKey: [QUERY_KEY.myInfo],
+ queryFn: getMyInfo,
+ enabled: !!accessToken,
+ });
+}
+
+export default useGetMyInfo;
diff --git a/Week8/wantkdd/mission2/src/hooks/queries/useSearchLp.ts b/Week8/wantkdd/mission2/src/hooks/queries/useSearchLp.ts
new file mode 100644
index 00000000..1542d091
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/hooks/queries/useSearchLp.ts
@@ -0,0 +1,5 @@
+import useGetInfiniteLpList from './useGetInfiniteLpList';
+
+export function useSearchLps(search: string) {
+ return useGetInfiniteLpList(10, 'desc', search);
+}
diff --git a/Week8/wantkdd/mission2/src/index.css b/Week8/wantkdd/mission2/src/index.css
new file mode 100644
index 00000000..ca24e35c
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/index.css
@@ -0,0 +1,11 @@
+@import 'tailwindcss';
+@layer utilities {
+ .animate-spin-slow {
+ animation: spin 3s linear infinite;
+ }
+}
+@layer utilities {
+ .animate-spin-slow {
+ animation: spin 3s linear infinite;
+ }
+}
diff --git a/Week8/wantkdd/mission2/src/layout/ProtectedLayout.tsx b/Week8/wantkdd/mission2/src/layout/ProtectedLayout.tsx
new file mode 100644
index 00000000..3db94f82
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/layout/ProtectedLayout.tsx
@@ -0,0 +1,18 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import RootLayout from './root-layout';
+
+const ProtectedLayout = () => {
+ const { accessToken } = useAuth();
+ if (!accessToken) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default ProtectedLayout;
diff --git a/Week8/wantkdd/mission2/src/layout/root-layout.tsx b/Week8/wantkdd/mission2/src/layout/root-layout.tsx
new file mode 100644
index 00000000..fca461f1
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/layout/root-layout.tsx
@@ -0,0 +1,32 @@
+import { Outlet } from 'react-router-dom';
+import Navbar from '../components/navbar.tsx';
+import { PropsWithChildren, useState } from 'react';
+import Footer from '../components/footer.tsx';
+import Sidebar from '../components/sidebar.tsx';
+import AddButton from '../components/add-lp/addButton.tsx';
+
+const RootLayout: React.FC = ({ children }) => {
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+
+ const toggleSidebar = () => setSidebarOpen((prev) => !prev);
+
+ return (
+
+
+
+
+
setSidebarOpen(false)} />
+
+ {children || }
+
+
+
+
+
+
+ );
+};
+
+export default RootLayout;
diff --git a/Week8/wantkdd/mission2/src/main.tsx b/Week8/wantkdd/mission2/src/main.tsx
new file mode 100644
index 00000000..df655eae
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import './index.css';
+import App from './App.tsx';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/Week8/wantkdd/mission2/src/pages/detail-page.tsx b/Week8/wantkdd/mission2/src/pages/detail-page.tsx
new file mode 100644
index 00000000..ca385420
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/detail-page.tsx
@@ -0,0 +1,115 @@
+import React, { useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import useLpDetail from '../hooks/custom/useLpDetail';
+import useLpComments from '../hooks/custom/useLpComments';
+import LpInfo from '../components/lp-detail/LpInfo';
+import LpEdit from '../components/lp-detail/LpEdit';
+import LpComments from '../components/lp-detail/LpComments';
+import DeleteModal from '../components/lp-detail/DeleteModal';
+
+const LpDetailPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ const { accessToken } = useAuth();
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const lpId = Number(id);
+
+ const {
+ lpData,
+ isLpLoading,
+ lpError,
+ isEditMode,
+ setIsEditMode,
+ handleLikeLp,
+ handleDislikeLp,
+ isLiked,
+ deleteLp,
+ } = useLpDetail(lpId, accessToken);
+
+ const {
+ commentsData,
+ isCommentsLoading,
+ commentsError,
+ commentOrder,
+ setCommentOrder,
+ commentContent,
+ setCommentContent,
+ handleSubmitComment,
+ handleUpdateComment,
+ handleDeleteComment,
+ hasNextPage,
+ isFetchingNextPage,
+ ref,
+ } = useLpComments(lpId);
+
+ if (isLpLoading) {
+ return (
+
+
로딩 중...
+
+ );
+ }
+
+ if (lpError || !lpData) {
+ return (
+
+
LP를 불러오지 못했습니다.
+
+ );
+ }
+
+ const handleDeleteClick = () => setShowDeleteModal(true);
+ const handleCancelDelete = () => setShowDeleteModal(false);
+ const handleConfirmDelete = () => deleteLp(lpId);
+
+ const comments = commentsData?.pages.flatMap((page) => page.data.data) || [];
+
+ return (
+
+
+ {isEditMode ? (
+ setIsEditMode(false)}
+ onDelete={handleDeleteClick}
+ />
+ ) : (
+ setIsEditMode(true)}
+ onDelete={handleDeleteClick}
+ />
+ )}
+
+
+
+ {showDeleteModal && (
+
+ )}
+
+
+ );
+};
+
+export default LpDetailPage;
diff --git a/Week8/wantkdd/mission2/src/pages/googleLoginRedirect-page.tsx b/Week8/wantkdd/mission2/src/pages/googleLoginRedirect-page.tsx
new file mode 100644
index 00000000..5eec08de
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/googleLoginRedirect-page.tsx
@@ -0,0 +1,27 @@
+import { useEffect } from 'react';
+import { useLocalStorage } from '../hooks/custom/useLocalStorage';
+import { LOCAL_STORAGE_KEY } from '../constants/key';
+
+const GoogleLoginRedirectPage = () => {
+ const { setItem: setAccessToken } = useLocalStorage(
+ LOCAL_STORAGE_KEY.accessToken
+ );
+ const { setItem: setRefreshToken } = useLocalStorage(
+ LOCAL_STORAGE_KEY.refreshToken
+ );
+
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const accessToken = urlParams.get(LOCAL_STORAGE_KEY.accessToken);
+ const refreshToken = urlParams.get(LOCAL_STORAGE_KEY.refreshToken);
+
+ if (accessToken) {
+ setAccessToken(accessToken);
+ setRefreshToken(refreshToken);
+ window.location.href = '/my-page';
+ }
+ }, [setAccessToken, setRefreshToken]);
+ return 구글 리다이렉 화면
;
+};
+
+export default GoogleLoginRedirectPage;
diff --git a/Week8/wantkdd/mission2/src/pages/home-page.tsx b/Week8/wantkdd/mission2/src/pages/home-page.tsx
new file mode 100644
index 00000000..366e2c43
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/home-page.tsx
@@ -0,0 +1,84 @@
+import React, { useEffect, useState } from 'react';
+import { useInView } from 'react-intersection-observer';
+import useGetInfiniteLpList from '../hooks/queries/useGetInfiniteLpList';
+import LpCard from '../components/lp-detail/lp-card';
+import LpCardSkeletonList from '../components/lp-detail/lp-card-skeleton-list';
+import useThrottle from '../hooks/custom/useThrottle';
+import { PAGINATION_ORDER } from '../enums/commons';
+import OrderButton from '../components/order-button';
+
+const HomePage: React.FC = () => {
+ const [order, setOrder] = useState(PAGINATION_ORDER.desc);
+ const [prevInView, setPrevInView] = useState(false);
+
+ const { ref, inView } = useInView({
+ threshold: 0.1,
+ rootMargin: '0px 0px -100px 0px',
+ });
+ const throttledInView = useThrottle(inView, 1000);
+
+ const {
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ status,
+ error,
+ refetch,
+ } = useGetInfiniteLpList(20, order, '');
+
+ useEffect(() => {
+ refetch();
+ }, [order, refetch]);
+
+ useEffect(() => {
+ //inView가 false에서 true로 변할 때만 다음 페이지 호출
+ if (throttledInView && !prevInView && hasNextPage && !isFetchingNextPage) {
+ setPrevInView(true);
+ fetchNextPage();
+ console.log('다음페이지호출');
+ } else if (!throttledInView) {
+ setPrevInView(false);
+ }
+ }, [
+ throttledInView,
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage,
+ prevInView,
+ ]);
+
+ const allLps = data?.pages.flatMap((page) => page.data.data) || [];
+
+ if (status === 'error') {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {allLps.length === 0 ? (
+
+ LP가 없습니다.
+
+ ) : (
+ <>
+ {allLps.map((lp) => (
+
+ ))}
+
+ {isFetchingNextPage &&
}
+ >
+ )}
+
+
+
+ );
+};
+
+export default HomePage;
diff --git a/Week8/wantkdd/mission2/src/pages/login-page.tsx b/Week8/wantkdd/mission2/src/pages/login-page.tsx
new file mode 100644
index 00000000..26650f85
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/login-page.tsx
@@ -0,0 +1,43 @@
+import { Link, useNavigate } from 'react-router-dom';
+import { useEffect } from 'react';
+import { useAuth } from '../context/AuthContext';
+import GoogleLoginButton from '../components/googleLogin';
+import LoginForm from '../components/login-form';
+
+const LoginPage = () => {
+ const { accessToken } = useAuth();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (accessToken) {
+ navigate('/my-page');
+ }
+ }, [accessToken, navigate]);
+
+ return (
+
+
+
+
+ <
+
+
+ 로그인
+
+
+
+
+
+
+
+ OR
+
+
+
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/Week8/wantkdd/mission2/src/pages/my-page.tsx b/Week8/wantkdd/mission2/src/pages/my-page.tsx
new file mode 100644
index 00000000..4486c266
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/my-page.tsx
@@ -0,0 +1,188 @@
+import { useEffect, useState, useRef } from 'react';
+import { getMyInfo } from '../apis/auth';
+import { ResponseMyInfoDto } from '../types/auth';
+import useUpdateUserInfo from '../hooks/mutations/user/useUpdateMyInfo';
+import { useAuth } from '../context/AuthContext';
+
+const MyPage = () => {
+ const [data, setData] = useState({} as ResponseMyInfoDto);
+ const [editMode, setEditMode] = useState(false);
+ const [name, setName] = useState('');
+ const [bio, setBio] = useState('');
+ const [avatar, setAvatar] = useState('');
+ const [previewAvatar, setPreviewAvatar] = useState('');
+ const fileInputRef = useRef(null);
+
+ const { mutate: updateUserInfo, isPending } = useUpdateUserInfo();
+ const { setUserInfo } = useAuth();
+
+ useEffect(() => {
+ const getData = async () => {
+ try {
+ const response = await getMyInfo();
+ setData(response);
+
+ setName(response.data.name);
+ setBio(response.data.bio || '');
+ setAvatar(response.data.avatar || '');
+ setPreviewAvatar(response.data.avatar || '');
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ getData();
+ }, []);
+
+ const handleAvatarClick = () => {
+ if (editMode && fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const result = reader.result as string;
+ setPreviewAvatar(result);
+ setAvatar(result);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleSubmit = () => {
+ if (!name.trim()) {
+ alert('이름은 빈칸일 수 없습니다.');
+ return;
+ }
+
+ updateUserInfo(
+ { name, bio: bio || undefined, avatar: avatar || undefined },
+ {
+ onSuccess: () => {
+ alert('정보가 수정되었습니다.');
+ setData((prev) => ({
+ ...prev,
+ data: {
+ ...prev.data,
+ name,
+ bio: bio || '',
+ avatar: avatar || '',
+ },
+ }));
+ setUserInfo({
+ ...data.data,
+ name,
+ bio: bio || null,
+ avatar: avatar || null,
+ });
+ setEditMode(false);
+ },
+ }
+ );
+ };
+
+ return (
+
+
+
+
+
+ {previewAvatar ? (
+

+ ) : (
+
없음
+ )}
+ {editMode && (
+
+ )}
+
+
+
+
+ {!editMode ? (
+ <>
+
+ {data.data?.name}님 환영합니다
+
+
+
이메일
+
{data.data?.email}
+
+
+
소개
+
{data.data?.bio || '소개가 없습니다.'}
+
+
+ >
+ ) : (
+ <>
+
+
+ setName(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default MyPage;
diff --git a/Week8/wantkdd/mission2/src/pages/search-page.tsx b/Week8/wantkdd/mission2/src/pages/search-page.tsx
new file mode 100644
index 00000000..b5807e4b
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/search-page.tsx
@@ -0,0 +1,86 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useInView } from 'react-intersection-observer';
+import useDebounce from '../hooks/custom/useDebounce';
+import useGetInfiniteLpList from '../hooks/queries/useGetInfiniteLpList';
+import LpCard from '../components/lp-detail/lp-card';
+import LpCardSkeletonList from '../components/lp-detail/lp-card-skeleton-list';
+import useThrottle from '../hooks/custom/useThrottle';
+import { PAGINATION_ORDER } from '../enums/commons';
+import OrderButton from '../components/order-button';
+
+const SearchPage: React.FC = () => {
+ const [search, setSearch] = useState('');
+ const [order, setOrder] = useState(PAGINATION_ORDER.desc);
+ const debouncedSearch = useDebounce(search, 500);
+
+ const { ref, inView } = useInView({
+ threshold: 0.1,
+ rootMargin: '0px 0px -100px 0px',
+ });
+ const throttledInView = useThrottle(inView, 1000);
+
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
+ useGetInfiniteLpList(20, order, debouncedSearch);
+
+ const prevInViewRef = useRef(false);
+
+ useEffect(() => {
+ if (
+ throttledInView &&
+ !prevInViewRef.current &&
+ hasNextPage &&
+ !isFetchingNextPage
+ ) {
+ fetchNextPage();
+ console.log('다음 페이지 호출');
+ prevInViewRef.current = true; //다음 호출 방지
+ } else if (!throttledInView) {
+ prevInViewRef.current = false;
+ }
+ }, [throttledInView, hasNextPage, isFetchingNextPage, fetchNextPage]);
+
+ const lps = data?.pages.flatMap((page) => page.data.data) || [];
+
+ return (
+
+
LP 검색
+
+
+ setSearch(e.target.value)}
+ placeholder="검색어를 입력하세요"
+ className="w-full px-4 py-2 rounded-md bg-gray-700 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-500"
+ />
+
+
+
+
+
+ {debouncedSearch === '' ? (
+
+ 검색어를 입력해주세요.
+
+ ) : isLoading ? (
+
+ ) : lps.length === 0 ? (
+
+ 검색 결과가 없습니다.
+
+ ) : (
+ <>
+ {lps.map((lp) => (
+
+ ))}
+ {isFetchingNextPage &&
}
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default SearchPage;
diff --git a/Week8/wantkdd/mission2/src/pages/signup-page.tsx b/Week8/wantkdd/mission2/src/pages/signup-page.tsx
new file mode 100644
index 00000000..06d26325
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/signup-page.tsx
@@ -0,0 +1,125 @@
+import { z } from 'zod';
+import {
+ SubmitHandler,
+ useForm,
+ UseFormRegister,
+ FieldErrors,
+} from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { postSignup } from '../apis/auth';
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { signupSchema } from '../utils/validate';
+import EmailStep from './signup/email-step';
+import PasswordStep from './signup/password-step';
+import NameStep from './signup/name-step';
+
+export type FormFields = z.infer;
+
+export type StepProps = {
+ register: UseFormRegister;
+ errors: FieldErrors;
+ watchedValues: FormFields;
+};
+
+const SignupPage = () => {
+ const [step, setStep] = useState(1);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showPasswordCheck, setShowPasswordCheck] = useState(false);
+
+ const togglePassword = () => setShowPassword((prev) => !prev);
+ const togglePasswordCheck = () => setShowPasswordCheck((prev) => !prev);
+
+ const navigate = useNavigate();
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ formState: { errors, isSubmitting },
+ trigger,
+ } = useForm({
+ defaultValues: {
+ name: '',
+ email: '',
+ password: '',
+ passwordCheck: '',
+ },
+ resolver: zodResolver(signupSchema),
+ mode: 'onBlur',
+ });
+
+ const watchedValues = watch();
+
+ const stepValidationFields = {
+ 1: ['email'] as const,
+ 2: ['password', 'passwordCheck'] as const,
+ 3: ['name'] as const,
+ };
+
+ const onSubmit: SubmitHandler = async (data) => {
+ try {
+ const response = await postSignup(data);
+ if (response) {
+ navigate('/login-page');
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const nextStep = async () => {
+ const fieldsToValidate =
+ stepValidationFields[step as keyof typeof stepValidationFields];
+ const isStepValid = await trigger(fieldsToValidate);
+
+ if (isStepValid) {
+ setStep((prev) => prev + 1);
+ }
+ };
+ const prevStep = () => {
+ setStep((prev) => (prev - 1 < 1 ? 1 : prev - 1));
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default SignupPage;
diff --git a/Week8/wantkdd/mission2/src/pages/signup/email-step.tsx b/Week8/wantkdd/mission2/src/pages/signup/email-step.tsx
new file mode 100644
index 00000000..27b487ec
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/signup/email-step.tsx
@@ -0,0 +1,40 @@
+import { StepProps } from '../signup-page';
+
+type EmailStepProps = StepProps & {
+ nextStep: () => void;
+};
+const ErrorMessage = ({ message }: { message: string | undefined }) => (
+ {message}
+);
+
+const EmailStep = ({
+ register,
+ errors,
+ watchedValues,
+ nextStep,
+}: EmailStepProps) => (
+ <>
+
+
+ {errors?.email && }
+
+
+ >
+);
+
+export default EmailStep;
diff --git a/Week8/wantkdd/mission2/src/pages/signup/name-step.tsx b/Week8/wantkdd/mission2/src/pages/signup/name-step.tsx
new file mode 100644
index 00000000..8e793d99
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/signup/name-step.tsx
@@ -0,0 +1,58 @@
+import { StepProps } from '../signup-page';
+
+type NameStepProps = StepProps & {
+ isSubmitting: boolean;
+ prevStep: () => void;
+ onSubmit: () => void;
+};
+
+const ErrorMessage = ({ message }: { message: string | undefined }) => (
+ {message}
+);
+
+const NameStep = ({
+ register,
+ errors,
+ watchedValues,
+ isSubmitting,
+ prevStep,
+ onSubmit,
+}: NameStepProps) => (
+ <>
+
+
이메일: {watchedValues.email}
+
비밀번호: {watchedValues.password}
+
+
+
+ {errors?.name && }
+
+
+
+
+
+
+ >
+);
+
+export default NameStep;
diff --git a/Week8/wantkdd/mission2/src/pages/signup/password-step.tsx b/Week8/wantkdd/mission2/src/pages/signup/password-step.tsx
new file mode 100644
index 00000000..3cfb5143
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/pages/signup/password-step.tsx
@@ -0,0 +1,99 @@
+import { FiEye, FiEyeOff } from 'react-icons/fi';
+import { StepProps } from '../signup-page';
+
+type PasswordStepProps = StepProps & {
+ showPassword: boolean;
+ showPasswordCheck: boolean;
+ togglePassword: () => void;
+ togglePasswordCheck: () => void;
+ nextStep: () => void;
+ prevStep: () => void;
+};
+
+const ErrorMessage = ({ message }: { message: string | undefined }) => (
+ {message}
+);
+
+const PasswordStep = ({
+ register,
+ errors,
+ watchedValues,
+ showPassword,
+ showPasswordCheck,
+ togglePassword,
+ togglePasswordCheck,
+ nextStep,
+ prevStep,
+}: PasswordStepProps) => (
+ <>
+
+
이메일: {watchedValues.email}
+
+
+
+
+ {errors?.password && }
+
+
+
+
+
+ {errors?.passwordCheck && (
+
+ )}
+
+
+
+
+
+
+ >
+);
+
+export default PasswordStep;
diff --git a/Week8/wantkdd/mission2/src/types/auth.ts b/Week8/wantkdd/mission2/src/types/auth.ts
new file mode 100644
index 00000000..fdf5ec18
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/types/auth.ts
@@ -0,0 +1,46 @@
+import { CommonRespense } from './common';
+
+export type RequestSignupDto = {
+ name: string;
+ email: string;
+ bio?: string;
+ avatar?: string;
+ password: string;
+};
+
+export type ResponseSignupDto = CommonRespense<{
+ id: number;
+ name: string;
+ email: string;
+ bio: null | string;
+ avatar: null | string;
+ createdAt: Date;
+ updatedAt: Date;
+}>;
+
+export type RequestSigninDto = {
+ email: string;
+ password: string;
+};
+export type ResponseSigninDto = CommonRespense<{
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+}>;
+
+export type ResponseMyInfoDto = CommonRespense<{
+ id: number;
+ name: string;
+ email: string;
+ bio: null | string;
+ avatar: null | string;
+ createdAt: Date;
+ updatedAt: Date;
+}>;
+
+export type PatchUserInfoDto = {
+ name: string;
+ bio?: string;
+ avatar?: string;
+};
diff --git a/Week8/wantkdd/mission2/src/types/common.ts b/Week8/wantkdd/mission2/src/types/common.ts
new file mode 100644
index 00000000..029855d1
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/types/common.ts
@@ -0,0 +1,20 @@
+import { PAGINATION_ORDER } from '../enums/commons';
+
+export type CommonRespense = {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: T;
+};
+
+export type PaginationDto = {
+ cursor?: number;
+ limit?: number;
+ search?: string;
+ order?: PAGINATION_ORDER;
+};
+export type CursorBasedResponse = CommonRespense<{
+ data: T;
+ nextCursor: number | null;
+ hasNext: boolean;
+}>;
diff --git a/Week8/wantkdd/mission2/src/types/lp-detail.ts b/Week8/wantkdd/mission2/src/types/lp-detail.ts
new file mode 100644
index 00000000..77fab240
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/types/lp-detail.ts
@@ -0,0 +1,54 @@
+import { Lp } from './lp';
+import { CommonRespense, CursorBasedResponse } from './common';
+
+export interface Author {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface LpDetailData extends Lp {
+ author?: Author;
+}
+
+export type LpDetailResponse = CommonRespense;
+
+export interface CommentAuthor {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Comment {
+ id: number;
+ content: string;
+ lpId: number;
+ authorId: number;
+ createdAt: string;
+ updatedAt: string;
+ author: CommentAuthor;
+}
+
+export type CommentsResponse = CursorBasedResponse;
+
+export type ResponseLikeLpDto = CommonRespense<{
+ id: number;
+ userId: number;
+ lpId: number;
+}>;
+
+export interface UpdateLpRequest {
+ title?: string;
+ content?: string;
+ thumbnail?: string;
+ tags?: string[];
+ published?: boolean;
+}
diff --git a/Week8/wantkdd/mission2/src/types/lp.ts b/Week8/wantkdd/mission2/src/types/lp.ts
new file mode 100644
index 00000000..a16ba04e
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/types/lp.ts
@@ -0,0 +1,34 @@
+import { CursorBasedResponse } from './common';
+
+export type Tag = {
+ id: number;
+ name: string;
+};
+
+export type Likes = {
+ id: number;
+ userId: number;
+ lpId: number;
+};
+
+export type Lp = {
+ id: number;
+ title: string;
+ content: string;
+ thumbnail: string;
+ published: boolean;
+ authorId: number;
+ createdAt: Date;
+ updatedAt: Date;
+ tags: Tag[];
+ likes: Likes[];
+};
+export type ResponseLpListDto = CursorBasedResponse;
+
+export type CreateLpDto = {
+ title: string;
+ content: string;
+ thumbnail: string;
+ tags: Tag[];
+ published: boolean;
+};
diff --git a/Week8/wantkdd/mission2/src/utils/validate.ts b/Week8/wantkdd/mission2/src/utils/validate.ts
new file mode 100644
index 00000000..fb67d6ab
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/utils/validate.ts
@@ -0,0 +1,33 @@
+import { z } from 'zod';
+
+export const signupSchema = z
+ .object({
+ email: z.string().email({ message: '올바른 이메일 형식이 아닙니다.' }),
+ password: z
+ .string()
+ .min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
+ .max(20, { message: '비밀번호는 20자 이하여야 합니다.' }),
+ passwordCheck: z
+ .string()
+ .min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
+ .max(20, {
+ message: '비밀번호는 20자 이하여야 합니다.',
+ }),
+ name: z.string().min(1, { message: '이름을 입력해주세요.' }),
+ })
+ .refine((data) => data.password === data.passwordCheck, {
+ message: '비밀번호가 일치하지 않습니다.',
+ path: ['passwordCheck'],
+ });
+
+export type SignupFormFields = z.infer;
+
+export const signinSchema = z.object({
+ email: z.string().email({ message: '올바른 이메일 형식이 아닙니다.' }),
+ password: z
+ .string()
+ .min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
+ .max(20, { message: '비밀번호는 20자 이하여야 합니다.' }),
+});
+
+export type SigninFormFields = z.infer;
diff --git a/Week8/wantkdd/mission2/src/vite-env.d.ts b/Week8/wantkdd/mission2/src/vite-env.d.ts
new file mode 100644
index 00000000..b48ad87b
--- /dev/null
+++ b/Week8/wantkdd/mission2/src/vite-env.d.ts
@@ -0,0 +1,8 @@
+///
+interface ImportMetaEnv {
+ readonly VITE_TMDB_KEY: string;
+ readonly VITE_SERVER_API_URL: string;
+}
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/Week8/wantkdd/mission2/tsconfig.app.json b/Week8/wantkdd/mission2/tsconfig.app.json
new file mode 100644
index 00000000..358ca9ba
--- /dev/null
+++ b/Week8/wantkdd/mission2/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/Week8/wantkdd/mission2/tsconfig.json b/Week8/wantkdd/mission2/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/Week8/wantkdd/mission2/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/Week8/wantkdd/mission2/tsconfig.node.json b/Week8/wantkdd/mission2/tsconfig.node.json
new file mode 100644
index 00000000..db0becc8
--- /dev/null
+++ b/Week8/wantkdd/mission2/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/Week8/wantkdd/mission2/vite.config.ts b/Week8/wantkdd/mission2/vite.config.ts
new file mode 100644
index 00000000..e598a4a6
--- /dev/null
+++ b/Week8/wantkdd/mission2/vite.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite';
+import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react-swc';
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+});