diff --git a/Week8/pocheonLim/mission1/.gitignore b/Week8/pocheonLim/mission1/.gitignore
new file mode 100644
index 00000000..f8155a11
--- /dev/null
+++ b/Week8/pocheonLim/mission1/.gitignore
@@ -0,0 +1,25 @@
+# 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/pocheonLim/mission1/README.md b/Week8/pocheonLim/mission1/README.md
new file mode 100644
index 00000000..da984443
--- /dev/null
+++ b/Week8/pocheonLim/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) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/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/pocheonLim/mission1/eslint.config.js b/Week8/pocheonLim/mission1/eslint.config.js
new file mode 100644
index 00000000..092408a9
--- /dev/null
+++ b/Week8/pocheonLim/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/pocheonLim/mission1/index.html b/Week8/pocheonLim/mission1/index.html
new file mode 100644
index 00000000..1c0b0b22
--- /dev/null
+++ b/Week8/pocheonLim/mission1/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
diff --git a/Week8/pocheonLim/mission1/package.json b/Week8/pocheonLim/mission1/package.json
new file mode 100644
index 00000000..a4612a82
--- /dev/null
+++ b/Week8/pocheonLim/mission1/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "mission2",
+ "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",
+ "@tailwindcss/vite": "^4.1.3",
+ "@tanstack/react-query": "^5.75.0",
+ "@tanstack/react-query-devtools": "^5.75.0",
+ "axios": "^1.8.4",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hook-form": "^7.55.0",
+ "react-router-dom": "^7.5.0",
+ "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",
+ "autoprefixer": "^10.4.21",
+ "eslint": "^9.21.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^15.15.0",
+ "postcss": "^8.5.3",
+ "tailwindcss": "^4.1.3",
+ "typescript": "~5.7.2",
+ "typescript-eslint": "^8.24.1",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/Week8/pocheonLim/mission1/pnpm-lock.yaml b/Week8/pocheonLim/mission1/pnpm-lock.yaml
new file mode 100644
index 00000000..015ea46a
--- /dev/null
+++ b/Week8/pocheonLim/mission1/pnpm-lock.yaml
@@ -0,0 +1,2499 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@hookform/resolvers':
+ specifier: ^5.0.1
+ version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
+ '@tailwindcss/vite':
+ specifier: ^4.1.3
+ version: 4.1.4(vite@6.3.3(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.0
+ version: 5.75.0(@tanstack/react-query@5.75.0(react@19.1.0))(react@19.1.0)
+ axios:
+ specifier: ^1.8.4
+ version: 1.8.4
+ 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.56.1(react@19.1.0)
+ react-router-dom:
+ specifier: ^7.5.0
+ version: 7.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ zod:
+ specifier: ^3.24.2
+ version: 3.24.3
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.21.0
+ version: 9.25.1
+ '@types/react':
+ specifier: ^19.0.10
+ version: 19.1.2
+ '@types/react-dom':
+ specifier: ^19.0.4
+ version: 19.1.2(@types/react@19.1.2)
+ '@vitejs/plugin-react-swc':
+ specifier: ^3.8.0
+ version: 3.9.0(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2))
+ autoprefixer:
+ specifier: ^10.4.21
+ version: 10.4.21(postcss@8.5.3)
+ eslint:
+ specifier: ^9.21.0
+ version: 9.25.1(jiti@2.4.2)
+ eslint-plugin-react-hooks:
+ specifier: ^5.1.0
+ version: 5.2.0(eslint@9.25.1(jiti@2.4.2))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.19
+ version: 0.4.20(eslint@9.25.1(jiti@2.4.2))
+ globals:
+ specifier: ^15.15.0
+ version: 15.15.0
+ postcss:
+ specifier: ^8.5.3
+ version: 8.5.3
+ tailwindcss:
+ specifier: ^4.1.3
+ version: 4.1.4
+ typescript:
+ specifier: ~5.7.2
+ version: 5.7.3
+ typescript-eslint:
+ specifier: ^8.24.1
+ version: 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ vite:
+ specifier: ^6.2.0
+ version: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)
+
+packages:
+
+ '@esbuild/aix-ppc64@0.25.3':
+ resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.3':
+ resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.3':
+ resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.3':
+ resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.3':
+ resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.3':
+ resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.3':
+ resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.3':
+ resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.3':
+ resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.3':
+ resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.3':
+ resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.3':
+ resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.3':
+ resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.3':
+ resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.3':
+ resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.3':
+ resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.3':
+ resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.3':
+ resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.3':
+ resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.3':
+ resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.3':
+ resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.25.3':
+ resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.3':
+ resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.3':
+ resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.3':
+ resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.6.1':
+ resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==}
+ 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.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.25.1':
+ resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==}
+ 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'}
+
+ '@rollup/rollup-android-arm-eabi@4.40.0':
+ resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.40.0':
+ resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.40.0':
+ resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.40.0':
+ resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.40.0':
+ resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.40.0':
+ resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.40.0':
+ resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.40.0':
+ resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.40.0':
+ resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.40.0':
+ resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.40.0':
+ resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
+ resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.40.0':
+ resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.40.0':
+ resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.40.0':
+ resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.40.0':
+ resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.40.0':
+ resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.40.0':
+ resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.40.0':
+ resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.40.0':
+ resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
+ '@swc/core-darwin-arm64@1.11.22':
+ resolution: {integrity: sha512-upSiFQfo1TE2QM3+KpBcp5SrOdKKjoc+oUoD1mmBDU2Wv4Bjjv16Z2I5ADvIqMV+b87AhYW+4Qu6iVrQD7j96Q==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@swc/core-darwin-x64@1.11.22':
+ resolution: {integrity: sha512-8PEuF/gxIMJVK21DjuCOtzdqstn2DqnxVhpAYfXEtm3WmMqLIOIZBypF/xafAozyaHws4aB/5xmz8/7rPsjavw==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@swc/core-linux-arm-gnueabihf@1.11.22':
+ resolution: {integrity: sha512-NIPTXvqtn9e7oQHgdaxM9Z/anHoXC3Fg4ZAgw5rSGa1OlnKKupt5sdfJamNggSi+eAtyoFcyfkgqHnfe2u63HA==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@swc/core-linux-arm64-gnu@1.11.22':
+ resolution: {integrity: sha512-xZ+bgS60c5r8kAeYsLNjJJhhQNkXdidQ277pUabSlu5GjR0CkQUPQ+L9hFeHf8DITEqpPBPRiAiiJsWq5eqMBg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-arm64-musl@1.11.22':
+ resolution: {integrity: sha512-JhrP/q5VqQl2eJR0xKYIkKTPjgf8CRsAmRnjJA2PtZhfQ543YbYvUqxyXSRyBOxdyX8JwzuAxIPEAlKlT7PPuQ==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-x64-gnu@1.11.22':
+ resolution: {integrity: sha512-htmAVL+U01gk9GyziVUP0UWYaUQBgrsiP7Ytf6uDffrySyn/FclUS3MDPocNydqYsOpj3OpNKPxkaHK+F+X5fg==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-linux-x64-musl@1.11.22':
+ resolution: {integrity: sha512-PL0VHbduWPX+ANoyOzr58jBiL2VnD0xGSFwPy7NRZ1Pr6SNWm4jw3x2u6RjLArGhS5EcWp64BSk9ZxqmTV3FEg==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-win32-arm64-msvc@1.11.22':
+ resolution: {integrity: sha512-moJvFhhTVGoMeEThtdF7hQog80Q00CS06v5uB+32VRuv+I31+4WPRyGlTWHO+oY4rReNcXut/mlDHPH7p0LdFg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@swc/core-win32-ia32-msvc@1.11.22':
+ resolution: {integrity: sha512-/jnsPJJz89F1aKHIb5ScHkwyzBciz2AjEq2m9tDvQdIdVufdJ4SpEDEN9FqsRNRLcBHjtbLs6bnboA+B+pRFXw==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@swc/core-win32-x64-msvc@1.11.22':
+ resolution: {integrity: sha512-lc93Y8Mku7LCFGqIxJ91coXZp2HeoDcFZSHCL90Wttg5xhk5xVM9uUCP+OdQsSsEixLF34h5DbT9ObzP8rAdRw==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/core@1.11.22':
+ resolution: {integrity: sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@swc/helpers': '>=0.5.17'
+ 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.4':
+ resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==}
+
+ '@tailwindcss/oxide-android-arm64@4.1.4':
+ resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.4':
+ resolution: {integrity: sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.1.4':
+ resolution: {integrity: sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.4':
+ resolution: {integrity: sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4':
+ resolution: {integrity: sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.4':
+ resolution: {integrity: sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.4':
+ resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.4':
+ resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.4':
+ resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.4':
+ resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.4':
+ resolution: {integrity: sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.4':
+ resolution: {integrity: sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.1.4':
+ resolution: {integrity: sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==}
+ engines: {node: '>= 10'}
+
+ '@tailwindcss/vite@4.1.4':
+ resolution: {integrity: sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==}
+ 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.0':
+ resolution: {integrity: sha512-ogTsCCONHPq8opyeU/NJZlus8+g36KnK+cybC1cs0Knr8iO/wAYgnciz+3DVNq38kRflBb0iJhHkUexp/7WfcQ==}
+ peerDependencies:
+ '@tanstack/react-query': ^5.75.0
+ react: ^18 || ^19
+
+ '@tanstack/react-query@5.75.0':
+ resolution: {integrity: sha512-H+TNgxmTbzH8qQ5MT5xsZEhQ8BG1tUYduDSfeAOzroVZgd/AEjg1rRYSP/9Tl9/hPobZ7iZzV401n77kStrbKw==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@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.2':
+ resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+
+ '@types/react@19.1.2':
+ resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==}
+
+ '@typescript-eslint/eslint-plugin@8.31.0':
+ resolution: {integrity: sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==}
+ 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.31.0':
+ resolution: {integrity: sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==}
+ 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.31.0':
+ resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/type-utils@8.31.0':
+ resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==}
+ 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.31.0':
+ resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.31.0':
+ resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/utils@8.31.0':
+ resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==}
+ 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.31.0':
+ resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react-swc@3.9.0':
+ resolution: {integrity: sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==}
+ 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==}
+
+ autoprefixer@10.4.21:
+ resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+
+ 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'}
+
+ browserslist@4.24.4:
+ resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ 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'}
+
+ caniuse-lite@1.0.30001715:
+ resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==}
+
+ 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.4:
+ resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.143:
+ resolution: {integrity: sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==}
+
+ 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@0.25.3:
+ resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ 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.20:
+ resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==}
+ 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.25.1:
+ resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==}
+ 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==}
+
+ fdir@6.4.4:
+ resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ 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'}
+
+ fraction.js@4.3.7:
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+
+ 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'}
+
+ 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-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==}
+
+ node-releases@2.0.19:
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+
+ normalize-range@0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+
+ 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'}
+
+ picomatch@4.0.2:
+ resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
+ engines: {node: '>=12'}
+
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
+ 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'}
+
+ 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.56.1:
+ resolution: {integrity: sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-router-dom@7.5.1:
+ resolution: {integrity: sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.5.1:
+ resolution: {integrity: sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==}
+ 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.40.0:
+ resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==}
+ 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.4:
+ resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==}
+
+ tapable@2.2.1:
+ resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+ engines: {node: '>=6'}
+
+ tinyglobby@0.2.13:
+ resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
+ engines: {node: '>=12.0.0'}
+
+ 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.31.0:
+ resolution: {integrity: sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ==}
+ 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
+
+ update-browserslist-db@1.1.3:
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite@6.3.3:
+ resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
+ 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.3:
+ resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
+
+snapshots:
+
+ '@esbuild/aix-ppc64@0.25.3':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/android-arm@0.25.3':
+ optional: true
+
+ '@esbuild/android-x64@0.25.3':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.3':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.3':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.3':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.3':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.3':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.3':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.3':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.3':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.3':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.3':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.3':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.6.1(eslint@9.25.1(jiti@2.4.2))':
+ dependencies:
+ eslint: 9.25.1(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.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.25.1': {}
+
+ '@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.56.1(react@19.1.0))':
+ dependencies:
+ '@standard-schema/utils': 0.3.0
+ react-hook-form: 7.56.1(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
+
+ '@rollup/rollup-android-arm-eabi@4.40.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.40.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.40.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.40.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.40.0':
+ optional: true
+
+ '@standard-schema/utils@0.3.0': {}
+
+ '@swc/core-darwin-arm64@1.11.22':
+ optional: true
+
+ '@swc/core-darwin-x64@1.11.22':
+ optional: true
+
+ '@swc/core-linux-arm-gnueabihf@1.11.22':
+ optional: true
+
+ '@swc/core-linux-arm64-gnu@1.11.22':
+ optional: true
+
+ '@swc/core-linux-arm64-musl@1.11.22':
+ optional: true
+
+ '@swc/core-linux-x64-gnu@1.11.22':
+ optional: true
+
+ '@swc/core-linux-x64-musl@1.11.22':
+ optional: true
+
+ '@swc/core-win32-arm64-msvc@1.11.22':
+ optional: true
+
+ '@swc/core-win32-ia32-msvc@1.11.22':
+ optional: true
+
+ '@swc/core-win32-x64-msvc@1.11.22':
+ optional: true
+
+ '@swc/core@1.11.22':
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.21
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.11.22
+ '@swc/core-darwin-x64': 1.11.22
+ '@swc/core-linux-arm-gnueabihf': 1.11.22
+ '@swc/core-linux-arm64-gnu': 1.11.22
+ '@swc/core-linux-arm64-musl': 1.11.22
+ '@swc/core-linux-x64-gnu': 1.11.22
+ '@swc/core-linux-x64-musl': 1.11.22
+ '@swc/core-win32-arm64-msvc': 1.11.22
+ '@swc/core-win32-ia32-msvc': 1.11.22
+ '@swc/core-win32-x64-msvc': 1.11.22
+
+ '@swc/counter@0.1.3': {}
+
+ '@swc/types@0.1.21':
+ dependencies:
+ '@swc/counter': 0.1.3
+
+ '@tailwindcss/node@4.1.4':
+ dependencies:
+ enhanced-resolve: 5.18.1
+ jiti: 2.4.2
+ lightningcss: 1.29.2
+ tailwindcss: 4.1.4
+
+ '@tailwindcss/oxide-android-arm64@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide@4.1.4':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.1.4
+ '@tailwindcss/oxide-darwin-arm64': 4.1.4
+ '@tailwindcss/oxide-darwin-x64': 4.1.4
+ '@tailwindcss/oxide-freebsd-x64': 4.1.4
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.4
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.1.4
+ '@tailwindcss/oxide-linux-arm64-musl': 4.1.4
+ '@tailwindcss/oxide-linux-x64-gnu': 4.1.4
+ '@tailwindcss/oxide-linux-x64-musl': 4.1.4
+ '@tailwindcss/oxide-wasm32-wasi': 4.1.4
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
+ '@tailwindcss/oxide-win32-x64-msvc': 4.1.4
+
+ '@tailwindcss/vite@4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2))':
+ dependencies:
+ '@tailwindcss/node': 4.1.4
+ '@tailwindcss/oxide': 4.1.4
+ tailwindcss: 4.1.4
+ vite: 6.3.3(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.0(@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/estree@1.0.7': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/react-dom@19.1.2(@types/react@19.1.2)':
+ dependencies:
+ '@types/react': 19.1.2
+
+ '@types/react@19.1.2':
+ dependencies:
+ csstype: 3.1.3
+
+ '@typescript-eslint/eslint-plugin@8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/scope-manager': 8.31.0
+ '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.31.0
+ eslint: 9.25.1(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.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.31.0
+ '@typescript-eslint/types': 8.31.0
+ '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.31.0
+ debug: 4.4.0
+ eslint: 9.25.1(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.31.0':
+ dependencies:
+ '@typescript-eslint/types': 8.31.0
+ '@typescript-eslint/visitor-keys': 8.31.0
+
+ '@typescript-eslint/type-utils@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ debug: 4.4.0
+ eslint: 9.25.1(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.31.0': {}
+
+ '@typescript-eslint/typescript-estree@8.31.0(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.31.0
+ '@typescript-eslint/visitor-keys': 8.31.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.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2))
+ '@typescript-eslint/scope-manager': 8.31.0
+ '@typescript-eslint/types': 8.31.0
+ '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.7.3)
+ eslint: 9.25.1(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.31.0':
+ dependencies:
+ '@typescript-eslint/types': 8.31.0
+ eslint-visitor-keys: 4.2.0
+
+ '@vitejs/plugin-react-swc@3.9.0(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2))':
+ dependencies:
+ '@swc/core': 1.11.22
+ vite: 6.3.3(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: {}
+
+ autoprefixer@10.4.21(postcss@8.5.3):
+ dependencies:
+ browserslist: 4.24.4
+ caniuse-lite: 1.0.30001715
+ fraction.js: 4.3.7
+ normalize-range: 0.1.2
+ picocolors: 1.1.1
+ postcss: 8.5.3
+ postcss-value-parser: 4.2.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
+
+ browserslist@4.24.4:
+ dependencies:
+ caniuse-lite: 1.0.30001715
+ electron-to-chromium: 1.5.143
+ node-releases: 2.0.19
+ update-browserslist-db: 1.1.3(browserslist@4.24.4)
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001715: {}
+
+ 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.4: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ electron-to-chromium@1.5.143: {}
+
+ 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@0.25.3:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.3
+ '@esbuild/android-arm': 0.25.3
+ '@esbuild/android-arm64': 0.25.3
+ '@esbuild/android-x64': 0.25.3
+ '@esbuild/darwin-arm64': 0.25.3
+ '@esbuild/darwin-x64': 0.25.3
+ '@esbuild/freebsd-arm64': 0.25.3
+ '@esbuild/freebsd-x64': 0.25.3
+ '@esbuild/linux-arm': 0.25.3
+ '@esbuild/linux-arm64': 0.25.3
+ '@esbuild/linux-ia32': 0.25.3
+ '@esbuild/linux-loong64': 0.25.3
+ '@esbuild/linux-mips64el': 0.25.3
+ '@esbuild/linux-ppc64': 0.25.3
+ '@esbuild/linux-riscv64': 0.25.3
+ '@esbuild/linux-s390x': 0.25.3
+ '@esbuild/linux-x64': 0.25.3
+ '@esbuild/netbsd-arm64': 0.25.3
+ '@esbuild/netbsd-x64': 0.25.3
+ '@esbuild/openbsd-arm64': 0.25.3
+ '@esbuild/openbsd-x64': 0.25.3
+ '@esbuild/sunos-x64': 0.25.3
+ '@esbuild/win32-arm64': 0.25.3
+ '@esbuild/win32-ia32': 0.25.3
+ '@esbuild/win32-x64': 0.25.3
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@5.2.0(eslint@9.25.1(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.25.1(jiti@2.4.2)
+
+ eslint-plugin-react-refresh@0.4.20(eslint@9.25.1(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.25.1(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.25.1(jiti@2.4.2):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(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.13.0
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.25.1
+ '@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
+
+ fdir@6.4.4(picomatch@4.0.2):
+ optionalDependencies:
+ picomatch: 4.0.2
+
+ 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
+
+ fraction.js@4.3.7: {}
+
+ 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
+
+ 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-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.4
+ 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: {}
+
+ node-releases@2.0.19: {}
+
+ normalize-range@0.1.2: {}
+
+ 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: {}
+
+ picomatch@4.0.2: {}
+
+ postcss-value-parser@4.2.0: {}
+
+ postcss@8.5.3:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ 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.56.1(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ react-router-dom@7.5.1(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.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+
+ react-router@7.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ cookie: 1.0.2
+ react: 19.1.0
+ set-cookie-parser: 2.7.1
+ 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.40.0:
+ dependencies:
+ '@types/estree': 1.0.7
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.40.0
+ '@rollup/rollup-android-arm64': 4.40.0
+ '@rollup/rollup-darwin-arm64': 4.40.0
+ '@rollup/rollup-darwin-x64': 4.40.0
+ '@rollup/rollup-freebsd-arm64': 4.40.0
+ '@rollup/rollup-freebsd-x64': 4.40.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.40.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.40.0
+ '@rollup/rollup-linux-arm64-gnu': 4.40.0
+ '@rollup/rollup-linux-arm64-musl': 4.40.0
+ '@rollup/rollup-linux-loongarch64-gnu': 4.40.0
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.40.0
+ '@rollup/rollup-linux-riscv64-musl': 4.40.0
+ '@rollup/rollup-linux-s390x-gnu': 4.40.0
+ '@rollup/rollup-linux-x64-gnu': 4.40.0
+ '@rollup/rollup-linux-x64-musl': 4.40.0
+ '@rollup/rollup-win32-arm64-msvc': 4.40.0
+ '@rollup/rollup-win32-ia32-msvc': 4.40.0
+ '@rollup/rollup-win32-x64-msvc': 4.40.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.4: {}
+
+ tapable@2.2.1: {}
+
+ tinyglobby@0.2.13:
+ dependencies:
+ fdir: 6.4.4(picomatch@4.0.2)
+ picomatch: 4.0.2
+
+ 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.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/parser': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ eslint: 9.25.1(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.7.3: {}
+
+ update-browserslist-db@1.1.3(browserslist@4.24.4):
+ dependencies:
+ browserslist: 4.24.4
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2):
+ dependencies:
+ esbuild: 0.25.3
+ fdir: 6.4.4(picomatch@4.0.2)
+ picomatch: 4.0.2
+ postcss: 8.5.3
+ rollup: 4.40.0
+ tinyglobby: 0.2.13
+ 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.3: {}
diff --git a/Week8/pocheonLim/mission1/public/images/google.png b/Week8/pocheonLim/mission1/public/images/google.png
new file mode 100644
index 00000000..db9fa09e
Binary files /dev/null and b/Week8/pocheonLim/mission1/public/images/google.png differ
diff --git a/Week8/pocheonLim/mission1/public/images/lp.png b/Week8/pocheonLim/mission1/public/images/lp.png
new file mode 100644
index 00000000..5d936536
Binary files /dev/null and b/Week8/pocheonLim/mission1/public/images/lp.png differ
diff --git a/Week8/pocheonLim/mission1/public/images/user.png b/Week8/pocheonLim/mission1/public/images/user.png
new file mode 100644
index 00000000..6d15e9e6
Binary files /dev/null and b/Week8/pocheonLim/mission1/public/images/user.png differ
diff --git a/Week8/pocheonLim/.gitkeep b/Week8/pocheonLim/mission1/src/App.css
similarity index 100%
rename from Week8/pocheonLim/.gitkeep
rename to Week8/pocheonLim/mission1/src/App.css
diff --git a/Week8/pocheonLim/mission1/src/App.tsx b/Week8/pocheonLim/mission1/src/App.tsx
new file mode 100644
index 00000000..0df26c82
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/App.tsx
@@ -0,0 +1,70 @@
+import { createBrowserRouter, RouterProvider } from 'react-router-dom'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import './App.css'
+import HomeLayout from './layouts/HomeLayout'
+import LoginPage from './pages/LoginPage'
+import SignupPage from './pages/SignupPage'
+import MyPage from './pages/MyPage'
+import { AuthProvider } from './contexts/AuthContext'
+import ProtectedRoute from './components/auth/ProtectedRoute'
+import GoogleLoginRidirectPage from './pages/GoogleLoginRidirectPage'
+import LpListPage from './pages/LpListPage'
+import LpDetailPage from './pages/LpDetailPage'
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ errorElement: Error
,
+ children: [
+ {
+ path: "",
+ element: ,
+ },
+ {
+ path: "login",
+ element: ,
+ },
+ {
+ path: "signup",
+ element: ,
+ },
+ {
+ path: "mypage",
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: "v1/auth/google/callback",
+ element: ,
+ },
+ {
+ path: "lp/:lpId",
+ element: (
+
+
+
+ ),
+ },
+ ],
+ },
+]);
+
+const queryClient = new QueryClient();
+
+function App() {
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/Week8/pocheonLim/mission1/src/apis/auth.ts b/Week8/pocheonLim/mission1/src/apis/auth.ts
new file mode 100644
index 00000000..191d71eb
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/apis/auth.ts
@@ -0,0 +1,196 @@
+import axios from 'axios';
+
+const API_BASE_URL = import.meta.env.VITE_SERVER_API_URL;
+
+// Axios 인스턴스 생성
+export const axiosInstance = axios.create({
+ baseURL: API_BASE_URL,
+});
+
+// 요청 인터셉터
+axiosInstance.interceptors.request.use(
+ (config) => {
+ const accessToken = localStorage.getItem('accessToken');
+ if (accessToken) {
+ console.log('요청에 토큰 추가됨:', accessToken.substring(0, 10) + '...');
+ config.headers.Authorization = `Bearer ${accessToken}`;
+ }
+ return config;
+ },
+ (error) => {
+ console.error('요청 인터셉터 에러:', error);
+ return Promise.reject(error);
+ }
+);
+
+// 응답 인터셉터
+axiosInstance.interceptors.response.use(
+ (response) => {
+ console.log('응답 성공:', response.config.url);
+ return response;
+ },
+ async (error) => {
+ const originalRequest = error.config;
+
+ // 토큰 재발급 요청 자체가 실패한 경우는 인터셉터에서 제외
+ if (originalRequest.url === '/v1/auth/refresh') {
+ console.error('토큰 재발급 실패');
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ window.location.href = '/login';
+ return Promise.reject(error);
+ }
+
+ // 401 에러이고, 이미 재시도한 요청이 아닌 경우
+ if (error.response?.status === 401 && !originalRequest._retry) {
+ console.log('토큰 만료됨, 재발급 시도...');
+ originalRequest._retry = true;
+
+ try {
+ // 토큰 재발급 시도
+ const response = await refreshToken();
+ if (response.status) {
+ console.log('토큰 재발급 성공');
+ // 새로운 토큰 저장
+ localStorage.setItem('accessToken', response.data.accessToken);
+ localStorage.setItem('refreshToken', response.data.refreshToken);
+
+ // 실패한 요청의 Authorization 헤더 업데이트
+ originalRequest.headers.Authorization = `Bearer ${response.data.accessToken}`;
+
+ console.log('실패한 요청 재시도:', originalRequest.url);
+ // 실패한 요청 재시도
+ return axiosInstance(originalRequest);
+ }
+ } catch (refreshError) {
+ console.error('토큰 재발급 실패:', refreshError);
+ // 토큰 재발급 실패 시 로그아웃 처리
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ console.log('로그아웃 처리됨');
+ window.location.href = '/login';
+ return Promise.reject(refreshError);
+ }
+ }
+
+ console.error('❌ API 요청 실패:', error.response?.status, error.config.url);
+ return Promise.reject(error);
+ }
+);
+
+//회원가입 요청
+export interface SignupRequest {
+ name: string;
+ email: string;
+ password: string;
+ bio?: string;
+ avatar?: string;
+}
+
+//회원가입 응답
+export interface SignupResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+}
+
+//회원가입 요청 함수
+export const signup = async (data: SignupRequest): Promise => {
+ const response = await axios.post(`${API_BASE_URL}/v1/auth/signup`, data);
+ return response.data;
+};
+
+//로그인 요청
+export interface LoginRequest {
+ email: string;
+ password: string;
+}
+
+//로그인 응답
+export interface LoginResponse {
+ status: boolean;
+ message: string;
+ statusCode: number;
+ data: {
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+ }
+}
+
+//로그인 요청 함수
+export const login = async (data: LoginRequest): Promise => {
+ const response = await axiosInstance.post('/v1/auth/signin', data);
+ return response.data;
+};
+
+//사용자 정보 응답
+export interface UserInfoResponse {
+ status: boolean;
+ message: string;
+ statusCode: number;
+ data: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ }
+}
+
+//사용자 정보 요청 함수
+export const getUserInfo = async (): Promise => {
+ const response = await axiosInstance.get('/v1/users/me');
+ return response.data;
+};
+
+// 토큰 재발급 응답
+export interface RefreshTokenResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+ }
+}
+
+// 토큰 재발급 요청 함수
+export const refreshToken = async (): Promise => {
+ const refreshToken = localStorage.getItem('refreshToken');
+ if (!refreshToken) {
+ throw new Error('리프레시 토큰이 없습니다.');
+ }
+
+ // axiosInstance 대신 일반 axios를 사용하여 인터셉터 우회
+ const response = await axios.post(`${API_BASE_URL}/v1/auth/refresh`, {
+ refresh: refreshToken
+ }, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+ return response.data;
+};
+
+export const signout = async (): Promise => {
+ await axiosInstance.post('/v1/auth/signout');
+ console.log("로그아웃 연동 성공~");
+};
+
+export const updateProfile = (data: { name: string; bio?: string | null; avatar?: string }) =>
+ axiosInstance.patch('/v1/users', data);
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/apis/lp.ts b/Week8/pocheonLim/mission1/src/apis/lp.ts
new file mode 100644
index 00000000..e6ca0741
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/apis/lp.ts
@@ -0,0 +1,200 @@
+import { Lp } from '../types/lp';
+import { axiosInstance } from './auth';
+
+interface FetchLpListParams {
+ order?: 'asc' | 'desc';
+ cursor?: number;
+ limit?: number;
+ search?: string;
+}
+
+interface LpListResponse {
+ data: {
+ data: Lp[];
+ nextCursor: number;
+ hasNext: boolean;
+ };
+}
+
+interface FetchCommentsParams {
+ lpId: string | number;
+ order?: 'asc' | 'desc';
+ cursor?: number;
+ limit?: number;
+}
+
+export const fetchLpList = async ({ order = 'desc', cursor = 0, limit = 10, search }: FetchLpListParams = {}) => {
+ const searchParams = new URLSearchParams({
+ order,
+ cursor: cursor.toString(),
+ limit: limit.toString(),
+ ...(search && { search }),
+ });
+
+ const url = `${import.meta.env.VITE_SERVER_API_URL}/v1/lps?${searchParams.toString()}`;
+
+ try {
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('API Error:', response.status, response.statusText);
+ throw new Error(`요청 실패 봐라 (${response.status}): ${errorText}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ if (!contentType || !contentType.includes('application/json')) {
+ throw new Error(`틀렸다네;;: ${contentType}`);
+ }
+
+ const data = await response.json();
+ return data.data as LpListResponse['data'];
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`과연: ${error.message}`);
+ }
+ throw new Error('오류입니다');
+ }
+};
+
+export const fetchComments = async ({
+ lpId,
+ order = 'asc',
+ cursor = 0,
+ limit = 10,
+}: FetchCommentsParams) => {
+ const searchParams = new URLSearchParams({
+ order,
+ cursor: cursor.toString(),
+ limit: limit.toString(),
+ });
+ const url = `/v1/lps/${lpId}/comments?${searchParams.toString()}`;
+ const res = await axiosInstance.get(url);
+ const data = res.data;
+ return {
+ data: data.data.data,
+ nextCursor: data.data.nextCursor,
+ hasNext: data.data.hasNext,
+ };
+};
+
+export interface CreateLpRequest {
+ title: string;
+ content: string;
+ thumbnail: string;
+ tags: string[];
+ published: boolean;
+}
+
+export interface CreateLpResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ title: string;
+ content: string;
+ thumbnail: string;
+ published: boolean;
+ authorId: number;
+ createdAt: string;
+ updatedAt: string;
+ };
+}
+
+export const createLp = async (
+ lpData: CreateLpRequest,
+): Promise => {
+ try {
+ console.log("createLp lpData:", lpData);
+ const response = await axiosInstance.post("/v1/lps", lpData);
+ return response.data;
+ } catch (error) {
+ console.log("❌ ~ createLp ~ error:", error);
+ throw error;
+ }
+};
+
+export interface FetchUserLpListParams {
+ order?: 'asc' | 'desc';
+ cursor?: number;
+ limit?: number;
+}
+
+export interface UserLpListResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ data: Lp[];
+ nextCursor: number;
+ hasNext: boolean;
+ };
+}
+
+export const fetchUserLpList = async ({ order = 'desc', cursor = 0, limit = 10 }: FetchUserLpListParams = {}) => {
+ const searchParams = new URLSearchParams({
+ order,
+ cursor: cursor.toString(),
+ limit: limit.toString(),
+ });
+ const res = await axiosInstance.get(`/v1/lps/user?${searchParams.toString()}`);
+ return res.data as UserLpListResponse;
+};
+
+// 댓글 수정
+export const updateComment = (lpId: number, commentId: number, content: string) =>
+ axiosInstance.patch(`/v1/lps/${lpId}/comments/${commentId}`, { content });
+
+// 댓글 삭제
+export const deleteComment = (lpId: number, commentId: number) =>
+ axiosInstance.delete(`/v1/lps/${lpId}/comments/${commentId}`);
+
+// LP 수정
+export interface UpdateLpRequest {
+ title: string;
+ content: string;
+ thumbnail: string;
+ tags: string[];
+ published: boolean;
+}
+
+export interface UpdateLpResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: Lp;
+}
+
+export const updateLp = async (
+ lpId: number,
+ data: UpdateLpRequest
+): Promise => {
+ const res = await axiosInstance.patch(`/v1/lps/${lpId}`, data);
+ return res.data;
+};
+
+export const deleteLp = async (lpId: number) => {
+ const response = await axiosInstance.delete(`/v1/lps/${lpId}`);
+ return response.data;
+};
+
+// 좋아요 추가
+export const addLike = async (lpId: number) => {
+ const response = await axiosInstance.post(`/v1/lps/${lpId}/likes`);
+ return response.data;
+};
+
+// 좋아요 취소
+export const removeLike = async (lpId: number) => {
+ const response = await axiosInstance.delete(`/v1/lps/${lpId}/likes`);
+ return response.data;
+};
+
+// 댓글 생성
+export const createComment = (lpId: number, content: string) =>
+ axiosInstance.post(`/v1/lps/${lpId}/comments`, { content });
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/apis/upload.ts b/Week8/pocheonLim/mission1/src/apis/upload.ts
new file mode 100644
index 00000000..a3afad00
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/apis/upload.ts
@@ -0,0 +1,10 @@
+import { axiosInstance } from './auth';
+
+export const uploadImage = async (file: File): Promise => {
+ const formData = new FormData();
+ formData.append('file', file);
+ const res = await axiosInstance.post('/v1/uploads', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ });
+ return res.data.data.imageUrl;
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/FabButton.tsx b/Week8/pocheonLim/mission1/src/components/FabButton.tsx
new file mode 100644
index 00000000..211a33f5
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/FabButton.tsx
@@ -0,0 +1,27 @@
+import React, { useState } from 'react';
+import { useAuth } from '../contexts/AuthContext';
+import { MdAdd } from 'react-icons/md';
+import LpCreateModal from './LpCreateModal';
+
+const FabButton = () => {
+ const { isLoggedIn } = useAuth();
+ const [open, setOpen] = useState(false);
+
+ if (!isLoggedIn) return null;
+
+ return (
+ <>
+
+ setOpen(false)} />
+ >
+ );
+};
+
+export default FabButton;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/LpCreateModal.tsx b/Week8/pocheonLim/mission1/src/components/LpCreateModal.tsx
new file mode 100644
index 00000000..fab6dedd
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/LpCreateModal.tsx
@@ -0,0 +1,147 @@
+import React, { useRef } from 'react';
+import { IoMdClose } from 'react-icons/io';
+import { useLpCreate } from '../hooks/lp/mutations/useLpCreate';
+import { useTagManagement } from '../hooks/utils/useTagManagement';
+import { uploadImage } from '../apis/upload';
+
+interface LpCreateModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+const LpCreateModal = ({ isOpen, onClose }: LpCreateModalProps) => {
+ const [image, setImage] = React.useState(null);
+ const [name, setName] = React.useState('');
+ const [content, setContent] = React.useState('');
+ const fileInputRef = useRef(null);
+ const { tagInput, setTagInput, tags, handleAddTag, handleRemoveTag, handleTagKeyDown } = useTagManagement();
+ const { mutation, handleAddLp } = useLpCreate();
+
+ if (!isOpen) return null;
+
+ // 모달 바깥 클릭 시 닫기
+ const handleOverlayClick = (e: React.MouseEvent) => {
+ if (e.target === e.currentTarget) onClose();
+ };
+
+ // LP 이미지 클릭 시 파일 input 열기
+ const handleImageClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ // 파일 이미지 변경
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ try {
+ const imageUrl = await uploadImage(file);
+ setImage(imageUrl);
+ } catch {
+ alert('이미지 업로드에 실패했습니다.');
+ }
+ }
+ };
+
+ // 모든 값이 입력되어야 Add LP 활성화
+ const isAddLpEnabled = !!(image && name.trim() && content.trim() && tags.length);
+
+ // LP 생성 요청
+ const handleSubmit = () => {
+ handleAddLp({
+ title: name,
+ content,
+ thumbnail: image || '',
+ tags,
+ published: true,
+ });
+ onClose();
+ };
+
+ return (
+
+
+ {/* X 버튼 */}
+
+ {/* LP 이미지 */}
+
+

+
+
+ {/* 입력 폼 */}
+
setName(e.target.value)}
+ />
+
setContent(e.target.value)}
+ />
+
+ setTagInput(e.target.value)}
+ onKeyDown={handleTagKeyDown}
+ />
+
+
+ {/* 태그 리스트 */}
+
+ {tags.map(tag => (
+
+ {tag}
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default LpCreateModal;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/auth/ProtectedRoute.tsx b/Week8/pocheonLim/mission1/src/components/auth/ProtectedRoute.tsx
new file mode 100644
index 00000000..2584a6be
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/auth/ProtectedRoute.tsx
@@ -0,0 +1,18 @@
+import { Navigate } from 'react-router-dom';
+import { useAuth } from '../../contexts/AuthContext';
+
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+}
+
+const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
+ const { user } = useAuth();
+
+ if (!user) {
+ return ;
+ }
+
+ return <>{children}>;
+};
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/comment/CommentSection.tsx b/Week8/pocheonLim/mission1/src/components/comment/CommentSection.tsx
new file mode 100644
index 00000000..7fd3e1bd
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/comment/CommentSection.tsx
@@ -0,0 +1,174 @@
+import { MdMoreVert, MdEdit, MdDelete } from 'react-icons/md';
+import { Comment } from '../../types/comment';
+import SkeletonComment from '../skeleton/SkeletonComment';
+
+interface CommentSectionProps {
+ commentOrder: 'asc' | 'desc';
+ handleSortChange: (order: 'asc' | 'desc') => void;
+ commentInput: string;
+ setCommentInput: (value: string) => void;
+ commentError: string;
+ isCommentLoading: boolean;
+ isFetchingNextPage: boolean;
+ comments: Comment[];
+ user: { id: number } | null;
+ showMenuId: number | null;
+ setShowMenuId: (id: number | null) => void;
+ editingCommentId: number | null;
+ editedContent: string;
+ setEditedContent: (value: string) => void;
+ handleCommentSubmit: () => void;
+ handleCommentEdit: (id: number, content: string) => void;
+ handleCommentDelete: (id: number) => void;
+ handleCommentUpdate: (id: number, content: string) => void;
+ handleCommentEditCancel: () => void;
+ createCommentMutation: { isPending: boolean };
+ updateCommentMutation: { isPending: boolean };
+ COMMENT_LIMIT: number;
+ scrollRef: React.RefObject;
+}
+
+export const CommentSection = ({
+ commentOrder,
+ handleSortChange,
+ commentInput,
+ setCommentInput,
+ commentError,
+ isCommentLoading,
+ isFetchingNextPage,
+ comments,
+ user,
+ showMenuId,
+ setShowMenuId,
+ editingCommentId,
+ editedContent,
+ setEditedContent,
+ handleCommentSubmit,
+ handleCommentEdit,
+ handleCommentDelete,
+ handleCommentUpdate,
+ handleCommentEditCancel,
+ createCommentMutation,
+ updateCommentMutation,
+ COMMENT_LIMIT,
+ scrollRef,
+}: CommentSectionProps) => {
+ return (
+
+
+
댓글
+
+
+
+
+
+
+
+
+
+
+
+ {commentError &&
{commentError}
}
+
+
+ {(isCommentLoading || isFetchingNextPage) &&
+ Array.from({ length: COMMENT_LIMIT }).map((_, idx) => (
+
+ ))}
+ {!isCommentLoading && !isFetchingNextPage && comments.length === 0 && (
+
아직 댓글이 없습니다.
+ )}
+ {!isCommentLoading && !isFetchingNextPage && comments.map((comment) => {
+ const isMine = user?.id === comment.authorId;
+ return (
+
+

+
+
+
{comment.author.name}
+ {isMine && (
+
+
+ {showMenuId === comment.id && (
+
+
+
+
+ )}
+
+ )}
+
+ {editingCommentId === comment.id ? (
+
+ ) : (
+
{comment.content}
+ )}
+
{new Date(comment.createdAt).toLocaleString()}
+
+
+ );
+ })}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/layout/Header.tsx b/Week8/pocheonLim/mission1/src/components/layout/Header.tsx
new file mode 100644
index 00000000..038c6467
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/layout/Header.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { useAuth } from '../../contexts/AuthContext';
+import { Link } from 'react-router-dom';
+import { useMutation } from '@tanstack/react-query';
+import { signout } from '../../apis/auth';
+
+interface HeaderProps {
+ onHamburgerClick?: () => void;
+}
+
+const Header: React.FC = ({ onHamburgerClick }) => {
+ const { isLoggedIn, user, logout } = useAuth();
+
+ const signoutMutation = useMutation({
+ mutationFn: signout,
+ onSuccess: () => {
+ logout();
+ },
+ onError: () => {
+ alert('로그아웃에 실패했습니다.');
+ }
+ });
+
+ return (
+
+ );
+};
+
+export default Header;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/layout/Sidebar.tsx b/Week8/pocheonLim/mission1/src/components/layout/Sidebar.tsx
new file mode 100644
index 00000000..2ad52436
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/layout/Sidebar.tsx
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { useMutation } from '@tanstack/react-query';
+import { axiosInstance } from '../../apis/auth';
+import { useAuth } from '../../contexts/AuthContext';
+
+interface SidebarProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+const Sidebar: React.FC = ({ open, onClose }) => {
+ const [showWithdrawModal, setShowWithdrawModal] = useState(false);
+ const { logout, isLoggedIn } = useAuth();
+ const navigate = useNavigate();
+
+ const withdrawMutation = useMutation({
+ mutationFn: async () => {
+ const res = await axiosInstance.delete('/v1/users');
+ return res.data;
+ },
+ onSuccess: () => {
+ logout();
+ navigate('/login');
+ },
+ });
+
+ // md 이상에서는 항상 보이고, 그 미만에서는 open일 때만 보임
+ return (
+ <>
+ {/* 오버레이: 모바일/태블릿에서만, 사이드바가 열렸을 때만 보임 */}
+
+ {/* 사이드바 본체 */}
+
+
+
+
+
+
+
+
+ {/* 탈퇴 모달 */}
+ {showWithdrawModal && (
+
+
+
+
정말 탈퇴하시겠습니까?
+
+
+
+
+ {withdrawMutation.isPending &&
탈퇴 처리 중...
}
+ {withdrawMutation.isError &&
탈퇴 중 오류가 발생했습니다.
}
+
+
+ )}
+ >
+ );
+};
+
+export default Sidebar;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/lp/LpContent.tsx b/Week8/pocheonLim/mission1/src/components/lp/LpContent.tsx
new file mode 100644
index 00000000..2ea92db4
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/lp/LpContent.tsx
@@ -0,0 +1,112 @@
+import { useRef } from 'react';
+import { LpDetail } from '../../types/lp';
+
+interface LpContentProps {
+ data: LpDetail;
+ isEditing: boolean;
+ editContent: string;
+ setEditContent: (value: string) => void;
+ editThumbnail: string;
+ editTagInput: string;
+ setEditTagInput: (value: string) => void;
+ editTags: string[];
+ isUploading: boolean;
+ uploadError: string | null;
+ handleAddTag: () => void;
+ handleRemoveTag: (tag: string) => void;
+ handleTagKeyDown: (e: React.KeyboardEvent) => void;
+ handleImageUploadChange: (file: File) => void;
+}
+
+export const LpContent = ({
+ data,
+ isEditing,
+ editContent,
+ setEditContent,
+ editThumbnail,
+ editTagInput,
+ setEditTagInput,
+ editTags,
+ isUploading,
+ uploadError,
+ handleAddTag,
+ handleRemoveTag,
+ handleTagKeyDown,
+ handleImageUploadChange,
+}: LpContentProps) => {
+ const fileInputRef = useRef(null);
+
+ return (
+ <>
+
+ {isEditing ? (
+
fileInputRef.current?.click()}>
+

+
+ {isUploading ? '업로드 중...' : '이미지 변경'}
+
+ {uploadError &&
{uploadError}
}
+
e.target.files?.[0] && handleImageUploadChange(e.target.files[0])}
+ />
+
+ ) : (
+

+ )}
+
+
+ {data.audioUrl && (
+
+ )}
+
+
+ {isEditing ? (
+
+
+
+ {isEditing ? (
+ <>
+ setEditTagInput(e.target.value)}
+ onKeyDown={handleTagKeyDown}
+ className="px-2 py-1 rounded bg-gray-800 border border-gray-700 text-white"
+ placeholder="태그 입력 후 Enter"
+ />
+
+ {editTags.map((tag, idx) => (
+
+ {tag}
+
+
+ ))}
+ >
+ ) : (
+ <>
+ {data.tags?.map((tag: { id: number; name: string }) => (
+ #{tag.name}
+ ))}
+ >
+ )}
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/lp/LpHeader.tsx b/Week8/pocheonLim/mission1/src/components/lp/LpHeader.tsx
new file mode 100644
index 00000000..78acb25c
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/lp/LpHeader.tsx
@@ -0,0 +1,60 @@
+import { MdEdit, MdDelete } from 'react-icons/md';
+import { LpDetail } from '../../types/lp';
+
+interface LpHeaderProps {
+ data: LpDetail;
+ isMyLp: boolean;
+ isEditing: boolean;
+ editTitle: string;
+ setEditTitle: (value: string) => void;
+ setIsEditing: (value: boolean) => void;
+ onDelete: () => void;
+}
+
+export const LpHeader = ({
+ data,
+ isMyLp,
+ isEditing,
+ editTitle,
+ setEditTitle,
+ setIsEditing,
+ onDelete,
+}: LpHeaderProps) => {
+ return (
+ <>
+
+

+
{data.author?.name}
+
{new Date(data.createdAt).toLocaleDateString()}
+
+
+ {isEditing ? (
+ setEditTitle(e.target.value)}
+ className="px-2 py-1 rounded bg-gray-800 border border-gray-700 text-white w-full"
+ autoFocus
+ />
+ ) : (
+ {data.title}
+ )}
+
+ {isMyLp && !isEditing && (
+
+
+
+
+ )}
+ >
+ );
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/signup/CompletionStep.tsx b/Week8/pocheonLim/mission1/src/components/signup/CompletionStep.tsx
new file mode 100644
index 00000000..c3053c59
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/signup/CompletionStep.tsx
@@ -0,0 +1,40 @@
+import { useSignup } from '../../contexts/SignupContext';
+import { useNavigate } from 'react-router-dom';
+
+const CompletionStep = () => {
+ const { email, nickname } = useSignup();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
{nickname}님,
+
환영합니다!
+
+
+
+
가입하신 이메일
+
+ mail
+ {email}
+
+
+
+
+
+ );
+};
+
+export default CompletionStep;
diff --git a/Week8/pocheonLim/mission1/src/components/signup/EmailStep.tsx b/Week8/pocheonLim/mission1/src/components/signup/EmailStep.tsx
new file mode 100644
index 00000000..dbe7dbb5
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/signup/EmailStep.tsx
@@ -0,0 +1,51 @@
+import { useSignup } from '../../contexts/SignupContext';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { emailSchema, type EmailFormData } from '../../schemas/signup.schema';
+
+const EmailStep = () => {
+ const { setEmail, setStep } = useSignup();
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(emailSchema),
+ mode: 'onChange',
+ });
+
+ const onSubmit = (data: EmailFormData) => {
+ setEmail(data.email);
+ setStep(2);
+ };
+
+ return (
+
+ );
+};
+
+export default EmailStep;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/signup/NicknameStep.tsx b/Week8/pocheonLim/mission1/src/components/signup/NicknameStep.tsx
new file mode 100644
index 00000000..05809f00
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/signup/NicknameStep.tsx
@@ -0,0 +1,95 @@
+import { useSignup } from '../../contexts/SignupContext';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { nicknameSchema, type NicknameFormData } from '../../schemas/signup.schema';
+import { signup } from '../../apis/auth';
+import { useNavigate } from 'react-router-dom';
+import { useState } from 'react';
+import { AxiosError } from 'axios';
+
+const NicknameStep = () => {
+ const { email, password, setNickname, clearSignupData } = useSignup();
+ const navigate = useNavigate();
+ const [errorMessage, setErrorMessage] = useState('');
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(nicknameSchema),
+ mode: 'onChange',
+ });
+
+ const onSubmit = async (data: NicknameFormData) => {
+ try {
+ setNickname(data.nickname);
+ setErrorMessage(''); // 에러 메시지 초기화
+
+ //API 요청하고 RESPONSE 받아오기
+ const response = await signup({
+ name: data.nickname,
+ email,
+ password,
+ });
+
+ if (response.status) {
+ // 회원가입 완료 후 데이터 초기화
+ clearSignupData();
+ // 로그인 페이지로 이동
+ navigate('/login');
+ } else {
+ setErrorMessage(response.message || '회원가입에 실패했습니다.');
+ }
+ } catch (error) {
+ if (error instanceof AxiosError && error.response?.status === 409) {
+ setErrorMessage('이미 사용 중인 이메일 또는 닉네임입니다.');
+ } else {
+ setErrorMessage('회원가입 중 오류가 발생했습니다. 다시 시도해주세요.');
+ }
+ console.error('회원가입 중 오류 발생:', error);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default NicknameStep;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/signup/PasswordStep.tsx b/Week8/pocheonLim/mission1/src/components/signup/PasswordStep.tsx
new file mode 100644
index 00000000..501d998a
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/signup/PasswordStep.tsx
@@ -0,0 +1,93 @@
+import { useSignup } from '../../contexts/SignupContext';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { passwordSchema, type PasswordFormData } from '../../schemas/signup.schema';
+import { useState } from 'react';
+
+const PasswordStep = () => {
+ const { setPassword, setStep } = useSignup();
+ const [showPassword, setShowPassword] = useState(false);
+ const [showPasswordConfirm, setShowPasswordConfirm] = useState(false);
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(passwordSchema),
+ mode: 'onChange',
+ });
+
+ const onSubmit = (data: PasswordFormData) => {
+ setPassword(data.password);
+ setStep(3);
+ };
+
+ return (
+
+ );
+};
+
+export default PasswordStep;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/skeleton/SkeletonComment.tsx b/Week8/pocheonLim/mission1/src/components/skeleton/SkeletonComment.tsx
new file mode 100644
index 00000000..16dd8d2e
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/skeleton/SkeletonComment.tsx
@@ -0,0 +1,11 @@
+const SkeletonComment = () => (
+
+);
+
+export default SkeletonComment;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/components/skeleton/SkeletonLpCard.tsx b/Week8/pocheonLim/mission1/src/components/skeleton/SkeletonLpCard.tsx
new file mode 100644
index 00000000..4eb33e48
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/components/skeleton/SkeletonLpCard.tsx
@@ -0,0 +1,19 @@
+const SkeletonLpCard = () => (
+
+);
+
+export default SkeletonLpCard;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/contexts/AuthContext.tsx b/Week8/pocheonLim/mission1/src/contexts/AuthContext.tsx
new file mode 100644
index 00000000..cc182e7a
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/contexts/AuthContext.tsx
@@ -0,0 +1,115 @@
+import { createContext, useContext, ReactNode, useState, useEffect } from 'react';
+import { getUserInfo } from '../apis/auth';
+
+interface User {
+ id: number;
+ email: string;
+ name: string;
+ bio?: string | null;
+ avatar?: string | null;
+ createdAt?: string;
+ updatedAt?: string;
+}
+
+interface AuthContextType {
+ isLoggedIn: boolean;
+ user: User | null;
+ isLoading: boolean;
+ login: (accessToken: string, refreshToken: string) => Promise;
+ logout: () => void;
+ updateUser: (userData: Partial) => void;
+}
+
+const AuthContext = createContext(undefined);
+
+const useAuthStorage = () => {
+ const getTokens = () => ({
+ accessToken: localStorage.getItem('accessToken'),
+ refreshToken: localStorage.getItem('refreshToken'),
+ });
+
+ const setTokens = (accessToken: string, refreshToken: string) => {
+ localStorage.setItem('accessToken', accessToken);
+ localStorage.setItem('refreshToken', refreshToken);
+ };
+
+ const clearTokens = () => {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ };
+
+ return { getTokens, setTokens, clearTokens };
+};
+
+export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const { getTokens, setTokens, clearTokens } = useAuthStorage();
+
+ const fetchUserInfo = async () => {
+ try {
+ const response = await getUserInfo();
+ if (response.status) {
+ setUser(response.data);
+ setIsLoggedIn(true);
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('사용자 정보 조회 중 오류 발생:', error);
+ return false;
+ }
+ };
+
+ useEffect(() => {
+ const initializeAuth = async () => {
+ const { accessToken } = getTokens();
+ if (!accessToken) {
+ setIsLoading(false);
+ return;
+ }
+
+ const success = await fetchUserInfo();
+ if (!success) {
+ logout();
+ }
+ setIsLoading(false);
+ };
+
+ initializeAuth();
+ }, []);
+
+ const login = async (accessToken: string, refreshToken: string) => {
+ setTokens(accessToken, refreshToken);
+ await fetchUserInfo();
+ };
+
+ const logout = () => {
+ clearTokens();
+ setIsLoggedIn(false);
+ setUser(null);
+ };
+
+ const updateUser = (userData: Partial) => {
+ setUser(prev => prev ? { ...prev, ...userData } : null);
+ };
+
+ if (isLoading) {
+ return 로딩 중...
;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth는 AuthProvider 내에서만 사용할 수 있습니다.');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/contexts/SignupContext.tsx b/Week8/pocheonLim/mission1/src/contexts/SignupContext.tsx
new file mode 100644
index 00000000..3913e303
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/contexts/SignupContext.tsx
@@ -0,0 +1,61 @@
+import { createContext, useContext, ReactNode } from 'react';
+import useLocalStorage from '../hooks/auth/useLocalStorage';
+
+interface SignupContextType {
+ email: string;
+ setEmail: (email: string) => void;
+ password: string;
+ setPassword: (password: string) => void;
+ nickname: string;
+ setNickname: (nickname: string) => void;
+ step: number;
+ setStep: (step: number) => void;
+ clearSignupData: () => void; // 회원가입 완료 후 데이터 초기화
+}
+
+const SignupContext = createContext(undefined);
+
+export const SignupProvider = ({ children }: { children: ReactNode }) => {
+ const [email, setEmail] = useLocalStorage('signup_email', '');
+ const [password, setPassword] = useLocalStorage('signup_password', '');
+ const [nickname, setNickname] = useLocalStorage('signup_nickname', '');
+ const [step, setStep] = useLocalStorage('signup_step', 1);
+
+ // 회원가입 완료 후 모든 데이터 초기화
+ const clearSignupData = () => {
+ localStorage.removeItem('signup_email');
+ localStorage.removeItem('signup_password');
+ localStorage.removeItem('signup_nickname');
+ localStorage.removeItem('signup_step');
+ setEmail('');
+ setPassword('');
+ setNickname('');
+ setStep(1);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useSignup = () => {
+ const context = useContext(SignupContext);
+ if (context === undefined) {
+ throw new Error('useSignup must be used within a SignupProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/auth/useLocalStorage.ts b/Week8/pocheonLim/mission1/src/hooks/auth/useLocalStorage.ts
new file mode 100644
index 00000000..f76977da
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/auth/useLocalStorage.ts
@@ -0,0 +1,35 @@
+import { useState } from 'react';
+
+function useLocalStorage(key: string, initialValue: T) {
+ // 초기값을 가져오는 함수
+ const getStoredValue = () => {
+ try {
+ const item = window.localStorage.getItem(key);
+ return item ? JSON.parse(item) : initialValue;
+ } catch (error) {
+ console.error('Error reading from localStorage:', error);
+ return initialValue;
+ }
+ };
+
+ // 상태 초기화
+ const [storedValue, setStoredValue] = useState(getStoredValue);
+
+ // localStorage에 값을 저장하는 함수
+ const setValue = (value: T | ((val: T) => T)) => {
+ try {
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
+ setStoredValue(valueToStore);
+ window.localStorage.setItem(
+ key,
+ typeof valueToStore === 'string' ? valueToStore : JSON.stringify(valueToStore)
+ );
+ } catch (error) {
+ console.error('Error saving to localStorage:', error);
+ }
+ };
+
+ return [storedValue, setValue] as const;
+}
+
+export default useLocalStorage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/auth/useLoginForm.ts b/Week8/pocheonLim/mission1/src/hooks/auth/useLoginForm.ts
new file mode 100644
index 00000000..9c81948a
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/auth/useLoginForm.ts
@@ -0,0 +1,65 @@
+import { useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router-dom';
+import { useMutation } from '@tanstack/react-query';
+import { axiosInstance } from '../../apis/auth';
+import type { LoginFormData } from '../../schemas/login.schema';
+import { useAuth } from '../../contexts/AuthContext';
+
+export const useLoginForm = () => {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid },
+ } = useForm({
+ mode: 'onChange',
+ });
+
+ const navigate = useNavigate();
+ const { login: authLogin } = useAuth();
+
+ const loginMutation = useMutation({
+ mutationFn: async (data: LoginFormData) => {
+ const response = await axiosInstance.post('/v1/auth/signin', data);
+ return response.data;
+ },
+ onSuccess: async (response) => {
+ if (response.status) {
+ await authLogin(response.data.accessToken, response.data.refreshToken);
+ navigate('/');
+ }
+ },
+ onError: (error) => {
+ console.error('로그인 중 오류 발생:', error);
+ },
+ });
+
+ const emailRegister = register('email', {
+ required: true,
+ pattern: {
+ value: /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i,
+ message: '올바른 이메일 형식을 입력해주세요.',
+ },
+ });
+
+ const passwordRegister = register('password', {
+ required: true,
+ minLength: {
+ value: 8,
+ message: '비밀번호는 8자 이상이어야 합니다.',
+ },
+ });
+
+ const onSubmit = async (data: LoginFormData) => {
+ loginMutation.mutate(data);
+ };
+
+ return {
+ emailRegister,
+ passwordRegister,
+ handleSubmit,
+ onSubmit,
+ errors,
+ isValid,
+ isPending: loginMutation.isPending,
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/comment/useCommentManagement.ts b/Week8/pocheonLim/mission1/src/hooks/comment/useCommentManagement.ts
new file mode 100644
index 00000000..448c8e98
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/comment/useCommentManagement.ts
@@ -0,0 +1,78 @@
+import { useState } from 'react';
+import { useComments, COMMENT_LIMIT } from './useComments';
+import { useCreateComment } from '../lp/comments/useCreateComment';
+import { useUpdateComment } from '../lp/comments/useUpdateComment';
+import { useDeleteComment } from '../lp/comments/useDeleteComment';
+import { useAuth } from '../../contexts/AuthContext';
+
+export const useCommentManagement = (lpId: string, commentOrder: 'asc' | 'desc') => {
+ const [commentInput, setCommentInput] = useState('');
+ const [commentError, setCommentError] = useState('');
+ const [editingCommentId, setEditingCommentId] = useState(null);
+ const [editedContent, setEditedContent] = useState('');
+ const [showMenuId, setShowMenuId] = useState(null);
+ const { user } = useAuth();
+
+ const {
+ data: commentData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading: isCommentLoading,
+ } = useComments(lpId, commentOrder);
+
+ const createCommentMutation = useCreateComment(lpId, commentOrder, setCommentInput, setCommentError);
+ const updateCommentMutation = useUpdateComment(lpId, commentOrder, setEditingCommentId, setEditedContent, setCommentError);
+ const deleteCommentMutation = useDeleteComment(lpId, commentOrder, setCommentError);
+
+ const handleCommentSubmit = () => {
+ if (!lpId) return;
+ createCommentMutation.mutate({ lpId: Number(lpId), content: commentInput });
+ };
+
+ const handleCommentEdit = (commentId: number, content: string) => {
+ setEditingCommentId(commentId);
+ setEditedContent(content);
+ setShowMenuId(null);
+ };
+
+ const handleCommentDelete = (commentId: number) => {
+ setShowMenuId(null);
+ deleteCommentMutation.mutate(commentId);
+ };
+
+ const handleCommentUpdate = (commentId: number, content: string) => {
+ updateCommentMutation.mutate({ commentId, content });
+ };
+
+ const handleCommentEditCancel = () => {
+ setEditingCommentId(null);
+ setEditedContent('');
+ };
+
+ return {
+ commentInput,
+ setCommentInput,
+ commentError,
+ editingCommentId,
+ editedContent,
+ setEditedContent,
+ showMenuId,
+ setShowMenuId,
+ user,
+ commentData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isCommentLoading,
+ createCommentMutation,
+ updateCommentMutation,
+ deleteCommentMutation,
+ handleCommentSubmit,
+ handleCommentEdit,
+ handleCommentDelete,
+ handleCommentUpdate,
+ handleCommentEditCancel,
+ COMMENT_LIMIT
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/comment/useCommentSort.ts b/Week8/pocheonLim/mission1/src/hooks/comment/useCommentSort.ts
new file mode 100644
index 00000000..c825f0b6
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/comment/useCommentSort.ts
@@ -0,0 +1,16 @@
+import { useState } from 'react';
+
+export type CommentOrder = 'asc' | 'desc';
+
+export const useCommentSort = () => {
+ const [commentOrder, setCommentOrder] = useState('asc');
+
+ const handleSortChange = (order: CommentOrder) => {
+ setCommentOrder(order);
+ };
+
+ return {
+ commentOrder,
+ handleSortChange
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/comment/useComments.ts b/Week8/pocheonLim/mission1/src/hooks/comment/useComments.ts
new file mode 100644
index 00000000..3796a43c
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/comment/useComments.ts
@@ -0,0 +1,18 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { fetchComments } from '../../apis/lp';
+
+export const COMMENT_LIMIT = 5;
+
+export const useComments = (lpId: string | undefined, order: 'asc' | 'desc') => {
+ return useInfiniteQuery({
+ queryKey: ['comments', lpId, order],
+ queryFn: ({ pageParam = 0 }) => fetchComments({ lpId: lpId!, order, cursor: pageParam, limit: COMMENT_LIMIT }),
+ getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.nextCursor : undefined,
+ initialPageParam: 0,
+ enabled: !!lpId,
+ staleTime: 0,
+ gcTime: 1000 * 60 * 5,
+ refetchInterval: false,
+ refetchIntervalInBackground: false,
+ });
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/like/useLikeManagement.ts b/Week8/pocheonLim/mission1/src/hooks/like/useLikeManagement.ts
new file mode 100644
index 00000000..3c9ec04a
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/like/useLikeManagement.ts
@@ -0,0 +1,30 @@
+import { useAddLike } from '../lp/likes/useAddLike';
+import { useRemoveLike } from '../lp/likes/useRemoveLike';
+import { useAuth } from '../../contexts/AuthContext';
+
+export const useLikeManagement = (lpId: string, commentOrder: 'asc' | 'desc', likes: { userId: number }[] | undefined) => {
+ const { user } = useAuth();
+ const addLikeMutation = useAddLike(lpId);
+ const removeLikeMutation = useRemoveLike(lpId);
+
+ const isLiked = likes?.some(like => like.userId === user?.id);
+
+ const handleLikeClick = () => {
+ if (!user) {
+ alert('로그인이 필요합니다.');
+ return;
+ }
+ if (isLiked) {
+ removeLikeMutation.mutate();
+ } else {
+ addLikeMutation.mutate();
+ }
+ };
+
+ return {
+ isLiked,
+ handleLikeClick,
+ addLikeMutation,
+ removeLikeMutation
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/comments/useCreateComment.ts b/Week8/pocheonLim/mission1/src/hooks/lp/comments/useCreateComment.ts
new file mode 100644
index 00000000..85dd5c33
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/comments/useCreateComment.ts
@@ -0,0 +1,35 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { createComment } from '../../../apis/lp';
+
+export function useCreateComment(
+ lpId: number | string,
+ commentOrder: 'asc' | 'desc',
+ setCommentInput: (v: string) => void,
+ setCommentError: (v: string) => void
+) {
+ const queryClient = useQueryClient();
+
+ const createCommentMutation = useMutation({
+ mutationFn: ({ lpId, content }: { lpId: number; content: string }) =>
+ createComment(Number(lpId), content),
+ onSuccess: () => {
+ setCommentInput('');
+ setCommentError('');
+ queryClient.invalidateQueries({ queryKey: ['comments', lpId, commentOrder] });
+ },
+ onError: (err: unknown) => {
+ if (
+ typeof err === 'object' &&
+ err !== null &&
+ 'response' in err &&
+ typeof (err as { response?: { data?: { message?: string } } }).response?.data?.message === 'string'
+ ) {
+ setCommentError((err as { response: { data: { message: string } } }).response.data.message);
+ } else {
+ setCommentError('댓글 등록 중 오류가 발생했습니다.');
+ }
+ },
+ });
+
+ return createCommentMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/comments/useDeleteComment.ts b/Week8/pocheonLim/mission1/src/hooks/lp/comments/useDeleteComment.ts
new file mode 100644
index 00000000..b6c7c67f
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/comments/useDeleteComment.ts
@@ -0,0 +1,29 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deleteComment } from '../../../apis/lp';
+
+export function useDeleteComment(
+ lpId: string,
+ commentOrder: 'asc' | 'desc',
+ setCommentError: (v: string) => void
+) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (commentId: number) => deleteComment(Number(lpId), commentId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['comments', lpId, commentOrder] });
+ },
+ onError: (err: unknown) => {
+ if (
+ typeof err === 'object' &&
+ err !== null &&
+ 'response' in err &&
+ typeof (err as { response?: { data?: { message?: string } } }).response?.data?.message === 'string'
+ ) {
+ setCommentError((err as { response: { data: { message: string } } }).response.data.message);
+ } else {
+ setCommentError('댓글 삭제 중 오류가 발생했습니다.');
+ }
+ },
+ });
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/comments/useUpdateComment.ts b/Week8/pocheonLim/mission1/src/hooks/lp/comments/useUpdateComment.ts
new file mode 100644
index 00000000..3caeeaf6
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/comments/useUpdateComment.ts
@@ -0,0 +1,34 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateComment } from '../../../apis/lp';
+
+export function useUpdateComment(
+ lpId: string,
+ commentOrder: 'asc' | 'desc',
+ setEditingCommentId: (v: number | null) => void,
+ setEditedContent: (v: string) => void,
+ setCommentError: (v: string) => void
+) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ commentId, content }: { commentId: number; content: string }) =>
+ updateComment(Number(lpId), commentId, content),
+ onSuccess: () => {
+ setEditingCommentId(null);
+ setEditedContent('');
+ queryClient.invalidateQueries({ queryKey: ['comments', lpId, commentOrder] });
+ },
+ onError: (err: unknown) => {
+ if (
+ typeof err === 'object' &&
+ err !== null &&
+ 'response' in err &&
+ typeof (err as { response?: { data?: { message?: string } } }).response?.data?.message === 'string'
+ ) {
+ setCommentError((err as { response: { data: { message: string } } }).response.data.message);
+ } else {
+ setCommentError('댓글 수정 중 오류가 발생했습니다.');
+ }
+ },
+ });
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/likes/useAddLike.ts b/Week8/pocheonLim/mission1/src/hooks/lp/likes/useAddLike.ts
new file mode 100644
index 00000000..94c66ecc
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/likes/useAddLike.ts
@@ -0,0 +1,33 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { addLike } from '../../../apis/lp';
+import { Lp } from '../../../types/lp';
+
+export function useAddLike(lpId: number | string) {
+ const queryClient = useQueryClient();
+
+ const addLikeMutation = useMutation({
+ mutationFn: () => addLike(Number(lpId)),
+ onMutate: async () => {
+ const previousData = queryClient.getQueryData(['lpDetail', lpId]);
+ queryClient.setQueryData(['lpDetail', lpId], (old: Lp | undefined) => {
+ if (!old) return old;
+ return {
+ ...old,
+ likes: [...(old.likes || []), { userId: null, lpId: Number(lpId) }]
+ };
+ });
+ return { previousData };
+ },
+ onError: (_, __, context) => {
+ if (context?.previousData) {
+ queryClient.setQueryData(['lpDetail', lpId], context.previousData);
+ }
+ alert('좋아요 추가에 실패했습니다.');
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpDetail', lpId] });
+ }
+ });
+
+ return addLikeMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/likes/useRemoveLike.ts b/Week8/pocheonLim/mission1/src/hooks/lp/likes/useRemoveLike.ts
new file mode 100644
index 00000000..3e8254d9
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/likes/useRemoveLike.ts
@@ -0,0 +1,33 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { removeLike } from '../../../apis/lp';
+import { Lp } from '../../../types/lp';
+
+export function useRemoveLike(lpId: number | string) {
+ const queryClient = useQueryClient();
+
+ const removeLikeMutation = useMutation({
+ mutationFn: () => removeLike(Number(lpId)),
+ onMutate: async () => {
+ const previousData = queryClient.getQueryData(['lpDetail', lpId]);
+ queryClient.setQueryData(['lpDetail', lpId], (old: Lp | undefined) => {
+ if (!old) return old;
+ return {
+ ...old,
+ likes: (old.likes || []).filter((like: Lp['likes'][number]) => like.userId !== null)
+ };
+ });
+ return { previousData };
+ },
+ onError: (_, __, context) => {
+ if (context?.previousData) {
+ queryClient.setQueryData(['lpDetail', lpId], context.previousData);
+ }
+ alert('좋아요 취소에 실패했습니다.');
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpDetail', lpId] });
+ }
+ });
+
+ return removeLikeMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useDeleteLp.ts b/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useDeleteLp.ts
new file mode 100644
index 00000000..86897090
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useDeleteLp.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deleteLp } from '../../../apis/lp';
+
+export function useDeleteLp(lpId: number | string, navigate: (path: string) => void) {
+ const queryClient = useQueryClient();
+
+ const deleteLpMutation = useMutation({
+ mutationFn: () => deleteLp(Number(lpId)),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['lps'] });
+ queryClient.invalidateQueries({ queryKey: ['myLps'] });
+ navigate('/mypage');
+ },
+ onError: () => {
+ alert('LP 삭제에 실패했습니다.');
+ }
+ });
+
+ return deleteLpMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useLpCreate.ts b/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useLpCreate.ts
new file mode 100644
index 00000000..f26cef8a
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useLpCreate.ts
@@ -0,0 +1,31 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { createLp } from '../../../apis/lp';
+
+export const useLpCreate = () => {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: createLp,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['userLpList'] });
+ },
+ onError: () => {
+ alert('LP 생성에 실패했습니다.');
+ }
+ });
+
+ const handleAddLp = (data: {
+ title: string;
+ content: string;
+ thumbnail: string;
+ tags: string[];
+ published: boolean;
+ }) => {
+ mutation.mutate(data);
+ };
+
+ return {
+ mutation,
+ handleAddLp
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useLpEdit.ts b/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useLpEdit.ts
new file mode 100644
index 00000000..f8169301
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useLpEdit.ts
@@ -0,0 +1,78 @@
+import { useState, useEffect } from 'react';
+import { useImageUpload } from '../../utils/useImageUpload';
+import { LpDetail } from '../../../types/lp';
+
+export const useLpEdit = (initialData: LpDetail | undefined) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editTitle, setEditTitle] = useState('');
+ const [editContent, setEditContent] = useState('');
+ const [editThumbnail, setEditThumbnail] = useState('');
+ const [editTagInput, setEditTagInput] = useState('');
+ const [editTags, setEditTags] = useState([]);
+ const { handleImageUpload, isUploading, error: uploadError } = useImageUpload();
+
+ useEffect(() => {
+ if (initialData) {
+ setEditTitle(initialData.title);
+ setEditContent(initialData.content);
+ setEditThumbnail(initialData.thumbnail);
+ setEditTags(initialData.tags ? initialData.tags.map(tag => tag.name) : []);
+ }
+ }, [initialData]);
+
+ const handleAddTag = () => {
+ const tag = editTagInput.trim();
+ if (tag && !editTags.includes(tag)) {
+ setEditTags([...editTags, tag]);
+ setEditTagInput('');
+ }
+ };
+
+ const handleRemoveTag = (tag: string) => {
+ setEditTags(editTags.filter(t => t !== tag));
+ };
+
+ const handleTagKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && editTagInput.trim()) {
+ e.preventDefault();
+ handleAddTag();
+ }
+ };
+
+ const handleImageUploadChange = async (file: File) => {
+ const imageUrl = await handleImageUpload(file);
+ if (imageUrl) {
+ setEditThumbnail(imageUrl);
+ }
+ };
+
+ const resetEditForm = () => {
+ if (initialData) {
+ setEditTitle(initialData.title);
+ setEditContent(initialData.content);
+ setEditThumbnail(initialData.thumbnail);
+ setEditTags(initialData.tags ? initialData.tags.map(tag => tag.name) : []);
+ }
+ setIsEditing(false);
+ };
+
+ return {
+ isEditing,
+ setIsEditing,
+ editTitle,
+ setEditTitle,
+ editContent,
+ setEditContent,
+ editThumbnail,
+ editTagInput,
+ setEditTagInput,
+ editTags,
+ isUploading,
+ uploadError,
+ handleAddTag,
+ handleRemoveTag,
+ handleTagKeyDown,
+ handleImageUploadChange,
+ resetEditForm
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useUpdateLp.ts b/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useUpdateLp.ts
new file mode 100644
index 00000000..abc27286
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/mutations/useUpdateLp.ts
@@ -0,0 +1,21 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateLp } from '../../../apis/lp';
+
+export function useUpdateLp(lpId: number | string) {
+ const queryClient = useQueryClient();
+
+ const updateLpMutation = useMutation({
+ mutationFn: (updateData: { title: string; content: string; thumbnail: string; tags: string[]; published: boolean }) =>
+ updateLp(Number(lpId), updateData),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpDetail', lpId] });
+ queryClient.invalidateQueries({ queryKey: ['lps'] });
+ queryClient.invalidateQueries({ queryKey: ['myLps'] });
+ },
+ onError: () => {
+ alert('LP 수정에 실패했습니다.');
+ }
+ });
+
+ return updateLpMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpDetail.ts b/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpDetail.ts
new file mode 100644
index 00000000..de60f170
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpDetail.ts
@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query';
+import { axiosInstance } from '../../../apis/auth';
+import { LpDetail } from '../../../types/lp';
+
+const fetchLpDetail = async (lpId: string): Promise => {
+ try {
+ const res = await axiosInstance.get(`/v1/lps/${lpId}`);
+ return res.data.data;
+ } catch (error) {
+ console.error('LP 상세 정보 조회 실패:', error);
+ throw error;
+ }
+};
+
+export const useLpDetail = (lpId: string | undefined) => {
+ return useQuery({
+ queryKey: ['lpDetail', lpId],
+ queryFn: () => fetchLpDetail(lpId!),
+ enabled: !!lpId,
+ retry: 1,
+ });
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpList.ts b/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpList.ts
new file mode 100644
index 00000000..dcf37072
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpList.ts
@@ -0,0 +1,17 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { fetchLpList } from '../../../apis/lp';
+
+export const LIMIT = 30;
+
+export const useLpList = (order: 'asc' | 'desc', search?: string) => {
+ return useInfiniteQuery({
+ queryKey: ['lpList', order, search],
+ queryFn: ({ pageParam = 0 }) => fetchLpList({ order, cursor: pageParam, limit: LIMIT, search }),
+ getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.nextCursor : undefined,
+ initialPageParam: 0,
+ staleTime: 1000 * 60 * 2,
+ gcTime: 1000 * 60 * 10,
+ refetchOnMount: true,
+ refetchOnWindowFocus: false,
+ });
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpSort.ts b/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpSort.ts
new file mode 100644
index 00000000..3d9e3f42
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/lp/queries/useLpSort.ts
@@ -0,0 +1,21 @@
+import { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { fetchUserLpList } from '../../../apis/lp';
+
+export const useLpSort = () => {
+ const [order, setOrder] = useState<'asc' | 'desc'>('asc');
+
+ const { data, isLoading, isError, error } = useQuery({
+ queryKey: ['userLpList', order],
+ queryFn: () => fetchUserLpList({ order }),
+ });
+
+ return {
+ order,
+ setOrder,
+ data,
+ isLoading,
+ isError,
+ error
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/profile/useProfileEdit.ts b/Week8/pocheonLim/mission1/src/hooks/profile/useProfileEdit.ts
new file mode 100644
index 00000000..96a36fd8
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/profile/useProfileEdit.ts
@@ -0,0 +1,43 @@
+import { useState } from 'react';
+import { User } from '../../types/user';
+
+type UserWithOptionalFields = Omit & {
+ bio?: string | null;
+ avatar?: string | null;
+ createdAt?: string;
+ updatedAt?: string;
+};
+
+export const useProfileEdit = (user: UserWithOptionalFields | null) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedName, setEditedName] = useState(user?.name || '');
+ const [editedBio, setEditedBio] = useState(user?.bio || '');
+ const [previewUrl, setPreviewUrl] = useState(null);
+
+ const handleImageChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setPreviewUrl(URL.createObjectURL(file));
+ }
+ };
+
+ const resetEditForm = () => {
+ setIsEditing(false);
+ setEditedName(user?.name || '');
+ setEditedBio(user?.bio || '');
+ setPreviewUrl(null);
+ };
+
+ return {
+ isEditing,
+ setIsEditing,
+ editedName,
+ setEditedName,
+ editedBio,
+ setEditedBio,
+ previewUrl,
+ setPreviewUrl,
+ handleImageChange,
+ resetEditForm
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/profile/useProfileMutation.ts b/Week8/pocheonLim/mission1/src/hooks/profile/useProfileMutation.ts
new file mode 100644
index 00000000..cf19fa53
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/profile/useProfileMutation.ts
@@ -0,0 +1,44 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateProfile } from '../../apis/auth';
+import { User } from '../../types/user';
+
+type UserWithOptionalFields = Omit & {
+ bio?: string | null;
+ avatar?: string | null;
+ createdAt?: string;
+ updatedAt?: string;
+};
+
+export function useProfileMutation(user: UserWithOptionalFields | null, updateUser: (u: UserWithOptionalFields) => void) {
+ const queryClient = useQueryClient();
+
+ const updateProfileMutation = useMutation({
+ mutationFn: async (data: { name: string; bio?: string | null; avatar?: string }) => {
+ const response = await updateProfile(data);
+ return response.data;
+ },
+ onMutate: async (newData) => {
+ const previousUser = user;
+ if (previousUser) {
+ updateUser({
+ ...previousUser,
+ name: newData.name,
+ bio: newData.bio || null,
+ avatar: newData.avatar || previousUser.avatar,
+ });
+ }
+ return { previousUser };
+ },
+ onError: (_, __, context) => {
+ if (context?.previousUser) {
+ updateUser(context.previousUser);
+ }
+ alert('프로필 업데이트에 실패했습니다.');
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: ['user'] });
+ },
+ });
+
+ return updateProfileMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/utils/useDebounce.ts b/Week8/pocheonLim/mission1/src/hooks/utils/useDebounce.ts
new file mode 100644
index 00000000..8a3adf9a
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/utils/useDebounce.ts
@@ -0,0 +1,14 @@
+import { useEffect, useState } from "react";
+
+function useDebounce(value: T, delay: number) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
+ return () => clearTimeout(handler);
+ }, [value, delay]);
+
+ return debouncedValue;
+}
+
+export default useDebounce;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/utils/useImageUpload.ts b/Week8/pocheonLim/mission1/src/hooks/utils/useImageUpload.ts
new file mode 100644
index 00000000..38a93f1d
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/utils/useImageUpload.ts
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+import { uploadImage } from '../../apis/upload';
+
+export const useImageUpload = () => {
+ const [isUploading, setIsUploading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleImageUpload = async (file: File) => {
+ try {
+ setIsUploading(true);
+ setError(null);
+ const imageUrl = await uploadImage(file);
+ return imageUrl;
+ } catch (err) {
+ setError('이미지 업로드에 실패했습니다.');
+ throw err;
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ return {
+ handleImageUpload,
+ isUploading,
+ error
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/utils/useInfiniteScroll.ts b/Week8/pocheonLim/mission1/src/hooks/utils/useInfiniteScroll.ts
new file mode 100644
index 00000000..7e3dd697
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/utils/useInfiniteScroll.ts
@@ -0,0 +1,27 @@
+import { useEffect, useRef } from 'react';
+
+interface UseInfiniteScrollProps {
+ hasNextPage: boolean;
+ isFetchingNextPage: boolean;
+ fetchNextPage: () => void;
+}
+
+export const useInfiniteScroll = ({ hasNextPage, isFetchingNextPage, fetchNextPage }: UseInfiniteScrollProps) => {
+ const scrollRef = useRef(null);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ if (!scrollRef.current) return;
+ const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
+ if (scrollTop + clientHeight >= scrollHeight - 100 && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ };
+
+ const ref = scrollRef.current;
+ if (ref) ref.addEventListener('scroll', handleScroll);
+ return () => { if (ref) ref.removeEventListener('scroll', handleScroll); };
+ }, [fetchNextPage, hasNextPage, isFetchingNextPage]);
+
+ return { scrollRef };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/hooks/utils/useTagManagement.ts b/Week8/pocheonLim/mission1/src/hooks/utils/useTagManagement.ts
new file mode 100644
index 00000000..4d721902
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/hooks/utils/useTagManagement.ts
@@ -0,0 +1,33 @@
+import { useState } from 'react';
+
+export const useTagManagement = () => {
+ const [tagInput, setTagInput] = useState('');
+ const [tags, setTags] = useState([]);
+
+ const handleAddTag = () => {
+ if (tagInput.trim() && !tags.includes(tagInput.trim())) {
+ setTags([...tags, tagInput.trim()]);
+ setTagInput('');
+ }
+ };
+
+ const handleRemoveTag = (tag: string) => {
+ setTags(tags.filter(t => t !== tag));
+ };
+
+ const handleTagKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && tagInput.trim()) {
+ e.preventDefault();
+ handleAddTag();
+ }
+ };
+
+ return {
+ tagInput,
+ setTagInput,
+ tags,
+ handleAddTag,
+ handleRemoveTag,
+ handleTagKeyDown
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/index.css b/Week8/pocheonLim/mission1/src/index.css
new file mode 100644
index 00000000..a461c505
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/index.css
@@ -0,0 +1 @@
+@import "tailwindcss";
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/layouts/HomeLayout.tsx b/Week8/pocheonLim/mission1/src/layouts/HomeLayout.tsx
new file mode 100644
index 00000000..fc8f1af3
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/layouts/HomeLayout.tsx
@@ -0,0 +1,54 @@
+import { Outlet } from 'react-router-dom';
+import React, { useState, useEffect, useRef } from 'react';
+import Header from '../components/layout/Header';
+import Sidebar from '../components/layout/Sidebar';
+import FabButton from '../components/FabButton';
+
+const SIDEBAR_WIDTH = 240; // w-60 = 240px
+const BREAKPOINT = 768;
+
+const HomeLayout = () => {
+ const [sidebarOpen, setSidebarOpen] = useState(window.innerWidth >= BREAKPOINT);
+ const prevIsDesktop = useRef(window.innerWidth >= BREAKPOINT);
+
+ useEffect(() => {
+ const handleResize = () => {
+ const isDesktop = window.innerWidth >= BREAKPOINT;
+
+ if (!prevIsDesktop.current && isDesktop) {
+ setSidebarOpen(true);
+ }
+
+ if (prevIsDesktop.current && !isDesktop) {
+ setSidebarOpen(false);
+ }
+ prevIsDesktop.current = isDesktop;
+ };
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ // 햄버거 버튼: 언제든 토글
+ const handleHamburgerClick = () => setSidebarOpen((prev) => !prev);
+
+ const marginLeft = sidebarOpen ? SIDEBAR_WIDTH : 0;
+
+ return (
+
+
setSidebarOpen(false)} />
+
+
+ {/* Main Content */}
+
+
+
+
+
+
+ );
+};
+
+export default HomeLayout;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/main.tsx b/Week8/pocheonLim/mission1/src/main.tsx
new file mode 100644
index 00000000..bef5202a
--- /dev/null
+++ b/Week8/pocheonLim/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/pocheonLim/mission1/src/pages/GoogleLoginRidirectPage.tsx b/Week8/pocheonLim/mission1/src/pages/GoogleLoginRidirectPage.tsx
new file mode 100644
index 00000000..ac2fb4af
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/pages/GoogleLoginRidirectPage.tsx
@@ -0,0 +1,24 @@
+import { useEffect } from 'react';
+import useLocalStorage from '../hooks/auth/useLocalStorage';
+
+const GoogleLoginRidirectPage = () => {
+ const [, setAccessToken] = useLocalStorage('accessToken', '');
+ const [, setRefreshToken] = useLocalStorage('refreshToken', '');
+
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const accessToken = urlParams.get('accessToken');
+ const refreshToken = urlParams.get('refreshToken');
+
+ if (accessToken && refreshToken) {
+ setAccessToken(accessToken);
+ setRefreshToken(refreshToken);
+ window.location.href = '/mypage';
+ }
+ }, [setAccessToken, setRefreshToken]);
+
+ return 구글 로그인 리다이렉트 처리 중...
;
+};
+
+export default GoogleLoginRidirectPage;
+
diff --git a/Week8/pocheonLim/mission1/src/pages/LoginPage.tsx b/Week8/pocheonLim/mission1/src/pages/LoginPage.tsx
new file mode 100644
index 00000000..a70b9a39
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/pages/LoginPage.tsx
@@ -0,0 +1,84 @@
+import { Link } from 'react-router-dom';
+import { useLoginForm } from '../hooks/auth/useLoginForm';
+
+const LoginPage = () => {
+ const {
+ emailRegister,
+ passwordRegister,
+ handleSubmit,
+ onSubmit,
+ errors,
+ isValid,
+ } = useLoginForm();
+
+ const handleGoogleLogin = () => {
+ window.location.href = import.meta.env.VITE_SERVER_API_URL + '/v1/auth/google/login';
+ };
+
+ return (
+
+
+
+
+ {'<'}
+
+
로그인
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LoginPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/pages/LpDetailPage.tsx b/Week8/pocheonLim/mission1/src/pages/LpDetailPage.tsx
new file mode 100644
index 00000000..c04b8f98
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/pages/LpDetailPage.tsx
@@ -0,0 +1,207 @@
+import { useParams, useNavigate } from 'react-router-dom';
+import { useRef } from 'react';
+import { useLpDetail } from '../hooks/lp/queries/useLpDetail';
+import { useUpdateLp } from '../hooks/lp/mutations/useUpdateLp';
+import { useDeleteLp } from '../hooks/lp/mutations/useDeleteLp';
+import { useAuth } from '../contexts/AuthContext';
+import { useLpEdit } from '../hooks/lp/mutations/useLpEdit';
+import { useCommentManagement } from '../hooks/comment/useCommentManagement';
+import { useLikeManagement } from '../hooks/like/useLikeManagement';
+import { useInfiniteScroll } from '../hooks/utils/useInfiniteScroll';
+import { useCommentSort } from '../hooks/comment/useCommentSort';
+import { LpHeader } from '../components/lp/LpHeader';
+import { LpContent } from '../components/lp/LpContent';
+import { CommentSection } from '../components/comment/CommentSection';
+
+const LpDetailPage = () => {
+ const { lpId } = useParams();
+ const navigate = useNavigate();
+ const scrollRef = useRef(null) as React.RefObject;
+ const COMMENT_LIMIT = 5;
+
+ const { data, isLoading, isError } = useLpDetail(lpId);
+ const { user } = useAuth();
+
+ const { commentOrder, handleSortChange } = useCommentSort();
+
+ const updateLpMutation = useUpdateLp(lpId!);
+ const deleteLpMutation = useDeleteLp(lpId!, navigate);
+
+ const {
+ isEditing,
+ setIsEditing,
+ editTitle,
+ setEditTitle,
+ editContent,
+ setEditContent,
+ editThumbnail,
+ editTagInput,
+ setEditTagInput,
+ editTags,
+ isUploading,
+ uploadError,
+ handleAddTag,
+ handleRemoveTag,
+ handleTagKeyDown,
+ handleImageUploadChange,
+ resetEditForm
+ } = useLpEdit(data);
+
+ const {
+ commentInput,
+ setCommentInput,
+ commentError,
+ editingCommentId,
+ editedContent,
+ setEditedContent,
+ showMenuId,
+ setShowMenuId,
+ commentData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isCommentLoading,
+ createCommentMutation,
+ updateCommentMutation,
+ handleCommentSubmit,
+ handleCommentEdit,
+ handleCommentDelete,
+ handleCommentUpdate,
+ handleCommentEditCancel,
+ } = useCommentManagement(lpId!, commentOrder);
+
+ useInfiniteScroll({
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage
+ });
+
+ const {
+ isLiked,
+ handleLikeClick,
+ addLikeMutation,
+ removeLikeMutation,
+ } = useLikeManagement(lpId!, commentOrder, data?.likes);
+
+ if (isLoading) return 로딩 중...
;
+ if (isError) {
+ console.error('에러 상세:', isError);
+ return LP 정보를 불러올 수 없습니다.
;
+ }
+ if (!data) return 데이터가 없습니다.
;
+
+ // 댓글 합치기
+ const comments = commentData?.pages.flatMap(page => page.data) ?? [];
+
+ // 본인이 작성한 LP인지 확인
+ const isMyLp = user?.id === data.authorId;
+
+ return (
+
+
+
{
+ if (window.confirm('정말로 이 LP를 삭제하시겠습니까?')) {
+ deleteLpMutation.mutate();
+ }
+ }}
+ />
+
+
+
+ {isMyLp && isEditing && (
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LpDetailPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/pages/LpListPage.tsx b/Week8/pocheonLim/mission1/src/pages/LpListPage.tsx
new file mode 100644
index 00000000..978e4000
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/pages/LpListPage.tsx
@@ -0,0 +1,116 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import { Lp } from '../types/lp';
+import SkeletonLpCard from '../components/skeleton/SkeletonLpCard';
+import { useLpList, LIMIT } from '../hooks/lp/queries/useLpList';
+import useDebounce from '../hooks/utils/useDebounce';
+
+const LpListPage: React.FC = () => {
+ const [order, setOrder] = useState<'asc' | 'desc'>('asc');
+ const [search, setSearch] = useState('');
+ const debouncedSearch = useDebounce(search, 3000);
+ const navigate = useNavigate();
+ const { user } = useAuth();
+
+ const {
+ data,
+ isError,
+ error,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = useLpList(order, debouncedSearch);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ if (
+ window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 100 &&
+ hasNextPage &&
+ !isFetchingNextPage
+ ) {
+ fetchNextPage();
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, [fetchNextPage, hasNextPage, isFetchingNextPage]);
+
+ const handleLpClick = (lpId: number) => {
+ if (!user) {
+ if (window.confirm('로그인이 필요한 서비스입니다. 로그인을 해주세요!')) {
+ navigate('/login');
+ }
+ return;
+ }
+ navigate(`/lp/${lpId}`);
+ };
+ if (isError) return 에러 발생: {error?.message}
;
+
+ const lps = data?.pages.flatMap(page => page.data) ?? [];
+
+ return (
+
+
+
setSearch(e.target.value)}
+ placeholder="LP 제목으로 검색..."
+ className="flex-1 max-w-xs px-4 py-2 rounded bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-pink-500 placeholder-gray-400"
+ />
+
+
+
+
+
+
+ {lps.length === 0 &&
LP가 없습니다.
}
+ {lps.map((lp: Lp) => (
+
handleLpClick(lp.id)}
+ className="relative cursor-pointer bg-[#232b3b] rounded shadow flex flex-col items-center overflow-hidden transition-transform duration-200 hover:scale-105 group"
+ >
+

+ {/* Hover Overlay */}
+
+ {/* 하단 정보 영역 */}
+
+
+
{lp.title}
+
{new Date(lp.createdAt).toLocaleDateString()}
+
+
+ ♥
+ {lp.likes?.length ?? 0}
+
+
+
+ ))}
+
+ {isFetchingNextPage &&
+ Array.from({ length: LIMIT }).map((_, idx) => (
+
+ ))}
+
+
+ );
+};
+
+export default LpListPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/pages/MyPage.tsx b/Week8/pocheonLim/mission1/src/pages/MyPage.tsx
new file mode 100644
index 00000000..42bb2d86
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/pages/MyPage.tsx
@@ -0,0 +1,196 @@
+import { useAuth } from '../contexts/AuthContext';
+import { useNavigate } from 'react-router-dom';
+import { MdSettings } from 'react-icons/md';
+import { useProfileMutation } from '../hooks/profile/useProfileMutation';
+import { useProfileEdit } from '../hooks/profile/useProfileEdit';
+import { useLpSort } from '../hooks/lp/queries/useLpSort';
+
+const MyPage = () => {
+ const { user, logout, updateUser } = useAuth();
+ const navigate = useNavigate();
+
+ const {
+ isEditing,
+ setIsEditing,
+ editedName,
+ setEditedName,
+ editedBio,
+ setEditedBio,
+ previewUrl,
+ setPreviewUrl,
+ handleImageChange,
+ resetEditForm
+ } = useProfileEdit(user);
+
+ const {
+ order,
+ setOrder,
+ data,
+ isLoading,
+ isError,
+ error
+ } = useLpSort();
+
+ const updateProfileMutation = useProfileMutation(user, updateUser);
+
+ const handleLogout = () => {
+ logout();
+ navigate('/');
+ };
+
+ const handleSubmit = () => {
+ if (!editedName.trim()) {
+ alert('닉네임은 필수입니다.');
+ return;
+ }
+
+ const data: { name: string; bio?: string | null; avatar?: string } = {
+ name: editedName,
+ bio: editedBio.trim() || null,
+ };
+
+ if (previewUrl) {
+ data.avatar = previewUrl;
+ }
+
+ updateProfileMutation.mutate(data);
+ setIsEditing(false);
+ setPreviewUrl(null);
+ };
+
+ if (!user) {
+ return 사용자 정보를 불러오는 중...
;
+ }
+
+ const lps = data?.data.data ?? [];
+
+ const handleLpClick = (lpId: number) => {
+ navigate(`/lp/${lpId}`);
+ };
+
+ return (
+
+
마이페이지
+
+
+

isEditing && document.getElementById('avatarInput')?.click()}
+ />
+ {isEditing && (
+
+ )}
+
+
+ {isEditing ? (
+
+ ) : (
+ <>
+
{user.name}
+ {user.bio &&
{user.bio}
}
+
{user.email}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+ {isLoading &&
로딩 중...
}
+ {isError &&
에러 발생: {error instanceof Error ? error.message : '에러'}
}
+
+ {lps.length === 0 &&
LP가 없습니다.
}
+ {lps.map(lp => (
+
handleLpClick(lp.id)}
+ className="relative cursor-pointer bg-[#232b3b] rounded shadow flex flex-col items-center overflow-hidden transition-transform duration-200 hover:scale-105 group"
+ >
+

+ {/* Hover Overlay */}
+
+ {/* 하단 정보 영역 */}
+
+
+
{lp.title}
+
{new Date(lp.createdAt).toLocaleDateString()}
+
+
+ ♥
+ {lp.likes?.length ?? 0}
+
+
+
+ ))}
+
+
+ );
+};
+
+export default MyPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/pages/SignupPage.tsx b/Week8/pocheonLim/mission1/src/pages/SignupPage.tsx
new file mode 100644
index 00000000..4cc52e87
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/pages/SignupPage.tsx
@@ -0,0 +1,102 @@
+import { Link } from 'react-router-dom';
+import { SignupProvider } from '../contexts/SignupContext';
+import { useSignup } from '../contexts/SignupContext';
+import { useEffect, useRef } from 'react';
+import { useLocation } from 'react-router-dom';
+import EmailStep from '../components/signup/EmailStep';
+import PasswordStep from '../components/signup/PasswordStep';
+import NicknameStep from '../components/signup/NicknameStep';
+import CompletionStep from '../components/signup/CompletionStep';
+
+const SignupContent = () => {
+ const { step, setStep } = useSignup();
+ const location = useLocation();
+ const isFirstVisit = useRef(true);
+
+ useEffect(() => {
+ if (location.pathname === '/signup' && isFirstVisit.current) {
+ setStep(1); // 딱 첫 진입 시에만 초기화
+ isFirstVisit.current = false;
+ }
+ }, [location.pathname]);
+
+ const handleBack = () => {
+ if (step > 1) {
+ setStep(step - 1);
+ }
+ };
+
+ const renderStep = () => {
+ switch (step) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const handleGoogleLogin = () => {
+ window.location.href = import.meta.env.VITE_API_URL + '/v1/auth/google/login';
+ };
+
+ return (
+
+
+
+ {step === 1 ? (
+
+ {'<'}
+
+ ) : (
+
+ )}
+
회원가입
+
+
+ {step === 1 && (
+ <>
+
+
+
+ >
+ )}
+
+ {renderStep()}
+
+
+ );
+};
+
+const SignupPage = () => {
+ return (
+
+
+
+ );
+};
+
+export default SignupPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/schemas/login.schema.ts b/Week8/pocheonLim/mission1/src/schemas/login.schema.ts
new file mode 100644
index 00000000..7073e402
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/schemas/login.schema.ts
@@ -0,0 +1,4 @@
+export interface LoginFormData {
+ email: string;
+ password: string;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/schemas/signup.schema.ts b/Week8/pocheonLim/mission1/src/schemas/signup.schema.ts
new file mode 100644
index 00000000..641f0fd4
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/schemas/signup.schema.ts
@@ -0,0 +1,33 @@
+import { z } from 'zod';
+
+export const emailSchema = z.object({
+ email: z
+ .string()
+ .min(1, '이메일을 입력해주세요.')
+ .email('올바른 이메일 형식이 아닙니다.'),
+});
+
+export const passwordSchema = z.object({
+ password: z
+ .string()
+ .min(8, '비밀번호는 8자 이상이어야 합니다.')
+ .regex(/[a-z]/, '소문자를 포함해야 합니다.')
+ .regex(/[A-Z]/, '대문자를 포함해야 합니다.')
+ .regex(/[0-9]/, '숫자를 포함해야 합니다.')
+ .regex(/[^a-zA-Z0-9]/, '특수문자를 포함해야 합니다.'),
+ passwordConfirm: z.string(),
+}).refine((data) => data.password === data.passwordConfirm, {
+ message: '비밀번호가 일치하지 않습니다.',
+ path: ['passwordConfirm'],
+});
+
+export const nicknameSchema = z.object({
+ nickname: z
+ .string()
+ .min(2, '닉네임은 2자 이상이어야 합니다.')
+ .max(10, '닉네임은 10자 이하여야 합니다.'),
+});
+
+export type EmailFormData = z.infer;
+export type PasswordFormData = z.infer;
+export type NicknameFormData = z.infer;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/types/auth.ts b/Week8/pocheonLim/mission1/src/types/auth.ts
new file mode 100644
index 00000000..87e3f0ba
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/types/auth.ts
@@ -0,0 +1,56 @@
+// 로그인 요청
+export interface LoginRequest {
+ email: string;
+ password: string;
+}
+
+// 로그인 응답
+export interface LoginResponse {
+ status: boolean;
+ message: string;
+ statusCode: number;
+ data: {
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+ }
+}
+
+// 회원가입 요청
+export interface SignupRequest {
+ name: string;
+ email: string;
+ password: string;
+ bio?: string;
+ avatar?: string;
+}
+
+// 회원가입 응답
+export interface SignupResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+}
+
+// 토큰 재발급 응답
+export interface RefreshTokenResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+ }
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/types/comment.ts b/Week8/pocheonLim/mission1/src/types/comment.ts
new file mode 100644
index 00000000..7f0e77de
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/types/comment.ts
@@ -0,0 +1,17 @@
+export interface Comment {
+ id: number;
+ content: string;
+ lpId: number;
+ authorId: number;
+ author: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+ createdAt: string;
+ updatedAt: string;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/types/context.ts b/Week8/pocheonLim/mission1/src/types/context.ts
new file mode 100644
index 00000000..f328c782
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/types/context.ts
@@ -0,0 +1,8 @@
+import { User } from './user';
+
+export interface AuthContextType {
+ isLoggedIn: boolean;
+ user: User | null;
+ login: (accessToken: string, refreshToken: string) => void;
+ logout: () => void;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/types/like.ts b/Week8/pocheonLim/mission1/src/types/like.ts
new file mode 100644
index 00000000..be9e7ceb
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/types/like.ts
@@ -0,0 +1,5 @@
+export interface Like {
+ id: number;
+ userId: number;
+ lpId: number;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/types/lp.ts b/Week8/pocheonLim/mission1/src/types/lp.ts
new file mode 100644
index 00000000..27cd5f78
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/types/lp.ts
@@ -0,0 +1,30 @@
+import { Like } from './like';
+
+export interface Lp {
+ id: number;
+ title: string;
+ content: string;
+ thumbnail: string;
+ published: boolean;
+ authorId: number;
+ createdAt: string;
+ updatedAt: string;
+ likes: Like[];
+}
+
+export interface LpDetail extends Lp {
+ author: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+ tags: {
+ id: number;
+ name: string;
+ }[];
+ audioUrl?: string;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/types/user.ts b/Week8/pocheonLim/mission1/src/types/user.ts
new file mode 100644
index 00000000..761f65f8
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/types/user.ts
@@ -0,0 +1,18 @@
+// 사용자 정보
+export interface User {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// 사용자 정보 응답
+export interface UserInfoResponse {
+ status: boolean;
+ message: string;
+ statusCode: number;
+ data: User;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission1/src/vite-env.d.ts b/Week8/pocheonLim/mission1/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/Week8/pocheonLim/mission1/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/Week8/pocheonLim/mission1/tsconfig.app.json b/Week8/pocheonLim/mission1/tsconfig.app.json
new file mode 100644
index 00000000..358ca9ba
--- /dev/null
+++ b/Week8/pocheonLim/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/pocheonLim/mission1/tsconfig.json b/Week8/pocheonLim/mission1/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/Week8/pocheonLim/mission1/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/Week8/pocheonLim/mission1/tsconfig.node.json b/Week8/pocheonLim/mission1/tsconfig.node.json
new file mode 100644
index 00000000..db0becc8
--- /dev/null
+++ b/Week8/pocheonLim/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/pocheonLim/mission1/vite.config.ts b/Week8/pocheonLim/mission1/vite.config.ts
new file mode 100644
index 00000000..4e43a26a
--- /dev/null
+++ b/Week8/pocheonLim/mission1/vite.config.ts
@@ -0,0 +1,17 @@
+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()],
+ server: {
+ proxy: {
+ '/v1': {
+ target: 'http://localhost:8000',
+ changeOrigin: true,
+ secure: false,
+ },
+ },
+ },
+})
diff --git a/Week8/pocheonLim/mission2/.gitignore b/Week8/pocheonLim/mission2/.gitignore
new file mode 100644
index 00000000..f8155a11
--- /dev/null
+++ b/Week8/pocheonLim/mission2/.gitignore
@@ -0,0 +1,25 @@
+# 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/pocheonLim/mission2/README.md b/Week8/pocheonLim/mission2/README.md
new file mode 100644
index 00000000..da984443
--- /dev/null
+++ b/Week8/pocheonLim/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) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/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/pocheonLim/mission2/eslint.config.js b/Week8/pocheonLim/mission2/eslint.config.js
new file mode 100644
index 00000000..092408a9
--- /dev/null
+++ b/Week8/pocheonLim/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/pocheonLim/mission2/index.html b/Week8/pocheonLim/mission2/index.html
new file mode 100644
index 00000000..1c0b0b22
--- /dev/null
+++ b/Week8/pocheonLim/mission2/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
diff --git a/Week8/pocheonLim/mission2/package.json b/Week8/pocheonLim/mission2/package.json
new file mode 100644
index 00000000..a4612a82
--- /dev/null
+++ b/Week8/pocheonLim/mission2/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "mission2",
+ "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",
+ "@tailwindcss/vite": "^4.1.3",
+ "@tanstack/react-query": "^5.75.0",
+ "@tanstack/react-query-devtools": "^5.75.0",
+ "axios": "^1.8.4",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hook-form": "^7.55.0",
+ "react-router-dom": "^7.5.0",
+ "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",
+ "autoprefixer": "^10.4.21",
+ "eslint": "^9.21.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^15.15.0",
+ "postcss": "^8.5.3",
+ "tailwindcss": "^4.1.3",
+ "typescript": "~5.7.2",
+ "typescript-eslint": "^8.24.1",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/Week8/pocheonLim/mission2/pnpm-lock.yaml b/Week8/pocheonLim/mission2/pnpm-lock.yaml
new file mode 100644
index 00000000..015ea46a
--- /dev/null
+++ b/Week8/pocheonLim/mission2/pnpm-lock.yaml
@@ -0,0 +1,2499 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@hookform/resolvers':
+ specifier: ^5.0.1
+ version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
+ '@tailwindcss/vite':
+ specifier: ^4.1.3
+ version: 4.1.4(vite@6.3.3(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.0
+ version: 5.75.0(@tanstack/react-query@5.75.0(react@19.1.0))(react@19.1.0)
+ axios:
+ specifier: ^1.8.4
+ version: 1.8.4
+ 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.56.1(react@19.1.0)
+ react-router-dom:
+ specifier: ^7.5.0
+ version: 7.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ zod:
+ specifier: ^3.24.2
+ version: 3.24.3
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.21.0
+ version: 9.25.1
+ '@types/react':
+ specifier: ^19.0.10
+ version: 19.1.2
+ '@types/react-dom':
+ specifier: ^19.0.4
+ version: 19.1.2(@types/react@19.1.2)
+ '@vitejs/plugin-react-swc':
+ specifier: ^3.8.0
+ version: 3.9.0(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2))
+ autoprefixer:
+ specifier: ^10.4.21
+ version: 10.4.21(postcss@8.5.3)
+ eslint:
+ specifier: ^9.21.0
+ version: 9.25.1(jiti@2.4.2)
+ eslint-plugin-react-hooks:
+ specifier: ^5.1.0
+ version: 5.2.0(eslint@9.25.1(jiti@2.4.2))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.19
+ version: 0.4.20(eslint@9.25.1(jiti@2.4.2))
+ globals:
+ specifier: ^15.15.0
+ version: 15.15.0
+ postcss:
+ specifier: ^8.5.3
+ version: 8.5.3
+ tailwindcss:
+ specifier: ^4.1.3
+ version: 4.1.4
+ typescript:
+ specifier: ~5.7.2
+ version: 5.7.3
+ typescript-eslint:
+ specifier: ^8.24.1
+ version: 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ vite:
+ specifier: ^6.2.0
+ version: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)
+
+packages:
+
+ '@esbuild/aix-ppc64@0.25.3':
+ resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.3':
+ resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.3':
+ resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.3':
+ resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.3':
+ resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.3':
+ resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.3':
+ resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.3':
+ resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.3':
+ resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.3':
+ resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.3':
+ resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.3':
+ resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.3':
+ resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.3':
+ resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.3':
+ resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.3':
+ resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.3':
+ resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.3':
+ resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.3':
+ resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.3':
+ resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.3':
+ resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.25.3':
+ resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.3':
+ resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.3':
+ resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.3':
+ resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.6.1':
+ resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==}
+ 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.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.25.1':
+ resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==}
+ 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'}
+
+ '@rollup/rollup-android-arm-eabi@4.40.0':
+ resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.40.0':
+ resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.40.0':
+ resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.40.0':
+ resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.40.0':
+ resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.40.0':
+ resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.40.0':
+ resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.40.0':
+ resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.40.0':
+ resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.40.0':
+ resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.40.0':
+ resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
+ resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.40.0':
+ resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.40.0':
+ resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.40.0':
+ resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.40.0':
+ resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.40.0':
+ resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.40.0':
+ resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.40.0':
+ resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.40.0':
+ resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
+ '@swc/core-darwin-arm64@1.11.22':
+ resolution: {integrity: sha512-upSiFQfo1TE2QM3+KpBcp5SrOdKKjoc+oUoD1mmBDU2Wv4Bjjv16Z2I5ADvIqMV+b87AhYW+4Qu6iVrQD7j96Q==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@swc/core-darwin-x64@1.11.22':
+ resolution: {integrity: sha512-8PEuF/gxIMJVK21DjuCOtzdqstn2DqnxVhpAYfXEtm3WmMqLIOIZBypF/xafAozyaHws4aB/5xmz8/7rPsjavw==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@swc/core-linux-arm-gnueabihf@1.11.22':
+ resolution: {integrity: sha512-NIPTXvqtn9e7oQHgdaxM9Z/anHoXC3Fg4ZAgw5rSGa1OlnKKupt5sdfJamNggSi+eAtyoFcyfkgqHnfe2u63HA==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@swc/core-linux-arm64-gnu@1.11.22':
+ resolution: {integrity: sha512-xZ+bgS60c5r8kAeYsLNjJJhhQNkXdidQ277pUabSlu5GjR0CkQUPQ+L9hFeHf8DITEqpPBPRiAiiJsWq5eqMBg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-arm64-musl@1.11.22':
+ resolution: {integrity: sha512-JhrP/q5VqQl2eJR0xKYIkKTPjgf8CRsAmRnjJA2PtZhfQ543YbYvUqxyXSRyBOxdyX8JwzuAxIPEAlKlT7PPuQ==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@swc/core-linux-x64-gnu@1.11.22':
+ resolution: {integrity: sha512-htmAVL+U01gk9GyziVUP0UWYaUQBgrsiP7Ytf6uDffrySyn/FclUS3MDPocNydqYsOpj3OpNKPxkaHK+F+X5fg==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-linux-x64-musl@1.11.22':
+ resolution: {integrity: sha512-PL0VHbduWPX+ANoyOzr58jBiL2VnD0xGSFwPy7NRZ1Pr6SNWm4jw3x2u6RjLArGhS5EcWp64BSk9ZxqmTV3FEg==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@swc/core-win32-arm64-msvc@1.11.22':
+ resolution: {integrity: sha512-moJvFhhTVGoMeEThtdF7hQog80Q00CS06v5uB+32VRuv+I31+4WPRyGlTWHO+oY4rReNcXut/mlDHPH7p0LdFg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@swc/core-win32-ia32-msvc@1.11.22':
+ resolution: {integrity: sha512-/jnsPJJz89F1aKHIb5ScHkwyzBciz2AjEq2m9tDvQdIdVufdJ4SpEDEN9FqsRNRLcBHjtbLs6bnboA+B+pRFXw==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@swc/core-win32-x64-msvc@1.11.22':
+ resolution: {integrity: sha512-lc93Y8Mku7LCFGqIxJ91coXZp2HeoDcFZSHCL90Wttg5xhk5xVM9uUCP+OdQsSsEixLF34h5DbT9ObzP8rAdRw==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/core@1.11.22':
+ resolution: {integrity: sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@swc/helpers': '>=0.5.17'
+ 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.4':
+ resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==}
+
+ '@tailwindcss/oxide-android-arm64@4.1.4':
+ resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.4':
+ resolution: {integrity: sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.1.4':
+ resolution: {integrity: sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.4':
+ resolution: {integrity: sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4':
+ resolution: {integrity: sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.4':
+ resolution: {integrity: sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.4':
+ resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.4':
+ resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.4':
+ resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.4':
+ resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.4':
+ resolution: {integrity: sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.4':
+ resolution: {integrity: sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.1.4':
+ resolution: {integrity: sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==}
+ engines: {node: '>= 10'}
+
+ '@tailwindcss/vite@4.1.4':
+ resolution: {integrity: sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==}
+ 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.0':
+ resolution: {integrity: sha512-ogTsCCONHPq8opyeU/NJZlus8+g36KnK+cybC1cs0Knr8iO/wAYgnciz+3DVNq38kRflBb0iJhHkUexp/7WfcQ==}
+ peerDependencies:
+ '@tanstack/react-query': ^5.75.0
+ react: ^18 || ^19
+
+ '@tanstack/react-query@5.75.0':
+ resolution: {integrity: sha512-H+TNgxmTbzH8qQ5MT5xsZEhQ8BG1tUYduDSfeAOzroVZgd/AEjg1rRYSP/9Tl9/hPobZ7iZzV401n77kStrbKw==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@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.2':
+ resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+
+ '@types/react@19.1.2':
+ resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==}
+
+ '@typescript-eslint/eslint-plugin@8.31.0':
+ resolution: {integrity: sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==}
+ 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.31.0':
+ resolution: {integrity: sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==}
+ 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.31.0':
+ resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/type-utils@8.31.0':
+ resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==}
+ 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.31.0':
+ resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.31.0':
+ resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/utils@8.31.0':
+ resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==}
+ 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.31.0':
+ resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react-swc@3.9.0':
+ resolution: {integrity: sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==}
+ 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==}
+
+ autoprefixer@10.4.21:
+ resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+
+ 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'}
+
+ browserslist@4.24.4:
+ resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ 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'}
+
+ caniuse-lite@1.0.30001715:
+ resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==}
+
+ 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.4:
+ resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.143:
+ resolution: {integrity: sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==}
+
+ 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@0.25.3:
+ resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ 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.20:
+ resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==}
+ 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.25.1:
+ resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==}
+ 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==}
+
+ fdir@6.4.4:
+ resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ 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'}
+
+ fraction.js@4.3.7:
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+
+ 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'}
+
+ 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-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==}
+
+ node-releases@2.0.19:
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+
+ normalize-range@0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+
+ 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'}
+
+ picomatch@4.0.2:
+ resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
+ engines: {node: '>=12'}
+
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
+ 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'}
+
+ 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.56.1:
+ resolution: {integrity: sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-router-dom@7.5.1:
+ resolution: {integrity: sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.5.1:
+ resolution: {integrity: sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==}
+ 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.40.0:
+ resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==}
+ 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.4:
+ resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==}
+
+ tapable@2.2.1:
+ resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+ engines: {node: '>=6'}
+
+ tinyglobby@0.2.13:
+ resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
+ engines: {node: '>=12.0.0'}
+
+ 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.31.0:
+ resolution: {integrity: sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ==}
+ 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
+
+ update-browserslist-db@1.1.3:
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite@6.3.3:
+ resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
+ 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.3:
+ resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
+
+snapshots:
+
+ '@esbuild/aix-ppc64@0.25.3':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/android-arm@0.25.3':
+ optional: true
+
+ '@esbuild/android-x64@0.25.3':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.3':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.3':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.3':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.3':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.3':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.3':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.3':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.3':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.3':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.3':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.3':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.3':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.3':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.6.1(eslint@9.25.1(jiti@2.4.2))':
+ dependencies:
+ eslint: 9.25.1(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.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.25.1': {}
+
+ '@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.56.1(react@19.1.0))':
+ dependencies:
+ '@standard-schema/utils': 0.3.0
+ react-hook-form: 7.56.1(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
+
+ '@rollup/rollup-android-arm-eabi@4.40.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.40.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.40.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.40.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.40.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.40.0':
+ optional: true
+
+ '@standard-schema/utils@0.3.0': {}
+
+ '@swc/core-darwin-arm64@1.11.22':
+ optional: true
+
+ '@swc/core-darwin-x64@1.11.22':
+ optional: true
+
+ '@swc/core-linux-arm-gnueabihf@1.11.22':
+ optional: true
+
+ '@swc/core-linux-arm64-gnu@1.11.22':
+ optional: true
+
+ '@swc/core-linux-arm64-musl@1.11.22':
+ optional: true
+
+ '@swc/core-linux-x64-gnu@1.11.22':
+ optional: true
+
+ '@swc/core-linux-x64-musl@1.11.22':
+ optional: true
+
+ '@swc/core-win32-arm64-msvc@1.11.22':
+ optional: true
+
+ '@swc/core-win32-ia32-msvc@1.11.22':
+ optional: true
+
+ '@swc/core-win32-x64-msvc@1.11.22':
+ optional: true
+
+ '@swc/core@1.11.22':
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.21
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.11.22
+ '@swc/core-darwin-x64': 1.11.22
+ '@swc/core-linux-arm-gnueabihf': 1.11.22
+ '@swc/core-linux-arm64-gnu': 1.11.22
+ '@swc/core-linux-arm64-musl': 1.11.22
+ '@swc/core-linux-x64-gnu': 1.11.22
+ '@swc/core-linux-x64-musl': 1.11.22
+ '@swc/core-win32-arm64-msvc': 1.11.22
+ '@swc/core-win32-ia32-msvc': 1.11.22
+ '@swc/core-win32-x64-msvc': 1.11.22
+
+ '@swc/counter@0.1.3': {}
+
+ '@swc/types@0.1.21':
+ dependencies:
+ '@swc/counter': 0.1.3
+
+ '@tailwindcss/node@4.1.4':
+ dependencies:
+ enhanced-resolve: 5.18.1
+ jiti: 2.4.2
+ lightningcss: 1.29.2
+ tailwindcss: 4.1.4
+
+ '@tailwindcss/oxide-android-arm64@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.4':
+ optional: true
+
+ '@tailwindcss/oxide@4.1.4':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.1.4
+ '@tailwindcss/oxide-darwin-arm64': 4.1.4
+ '@tailwindcss/oxide-darwin-x64': 4.1.4
+ '@tailwindcss/oxide-freebsd-x64': 4.1.4
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.4
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.1.4
+ '@tailwindcss/oxide-linux-arm64-musl': 4.1.4
+ '@tailwindcss/oxide-linux-x64-gnu': 4.1.4
+ '@tailwindcss/oxide-linux-x64-musl': 4.1.4
+ '@tailwindcss/oxide-wasm32-wasi': 4.1.4
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
+ '@tailwindcss/oxide-win32-x64-msvc': 4.1.4
+
+ '@tailwindcss/vite@4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2))':
+ dependencies:
+ '@tailwindcss/node': 4.1.4
+ '@tailwindcss/oxide': 4.1.4
+ tailwindcss: 4.1.4
+ vite: 6.3.3(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.0(@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/estree@1.0.7': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/react-dom@19.1.2(@types/react@19.1.2)':
+ dependencies:
+ '@types/react': 19.1.2
+
+ '@types/react@19.1.2':
+ dependencies:
+ csstype: 3.1.3
+
+ '@typescript-eslint/eslint-plugin@8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/scope-manager': 8.31.0
+ '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.31.0
+ eslint: 9.25.1(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.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.31.0
+ '@typescript-eslint/types': 8.31.0
+ '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.7.3)
+ '@typescript-eslint/visitor-keys': 8.31.0
+ debug: 4.4.0
+ eslint: 9.25.1(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.31.0':
+ dependencies:
+ '@typescript-eslint/types': 8.31.0
+ '@typescript-eslint/visitor-keys': 8.31.0
+
+ '@typescript-eslint/type-utils@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ debug: 4.4.0
+ eslint: 9.25.1(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.31.0': {}
+
+ '@typescript-eslint/typescript-estree@8.31.0(typescript@5.7.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.31.0
+ '@typescript-eslint/visitor-keys': 8.31.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.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2))
+ '@typescript-eslint/scope-manager': 8.31.0
+ '@typescript-eslint/types': 8.31.0
+ '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.7.3)
+ eslint: 9.25.1(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.31.0':
+ dependencies:
+ '@typescript-eslint/types': 8.31.0
+ eslint-visitor-keys: 4.2.0
+
+ '@vitejs/plugin-react-swc@3.9.0(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2))':
+ dependencies:
+ '@swc/core': 1.11.22
+ vite: 6.3.3(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: {}
+
+ autoprefixer@10.4.21(postcss@8.5.3):
+ dependencies:
+ browserslist: 4.24.4
+ caniuse-lite: 1.0.30001715
+ fraction.js: 4.3.7
+ normalize-range: 0.1.2
+ picocolors: 1.1.1
+ postcss: 8.5.3
+ postcss-value-parser: 4.2.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
+
+ browserslist@4.24.4:
+ dependencies:
+ caniuse-lite: 1.0.30001715
+ electron-to-chromium: 1.5.143
+ node-releases: 2.0.19
+ update-browserslist-db: 1.1.3(browserslist@4.24.4)
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001715: {}
+
+ 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.4: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ electron-to-chromium@1.5.143: {}
+
+ 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@0.25.3:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.3
+ '@esbuild/android-arm': 0.25.3
+ '@esbuild/android-arm64': 0.25.3
+ '@esbuild/android-x64': 0.25.3
+ '@esbuild/darwin-arm64': 0.25.3
+ '@esbuild/darwin-x64': 0.25.3
+ '@esbuild/freebsd-arm64': 0.25.3
+ '@esbuild/freebsd-x64': 0.25.3
+ '@esbuild/linux-arm': 0.25.3
+ '@esbuild/linux-arm64': 0.25.3
+ '@esbuild/linux-ia32': 0.25.3
+ '@esbuild/linux-loong64': 0.25.3
+ '@esbuild/linux-mips64el': 0.25.3
+ '@esbuild/linux-ppc64': 0.25.3
+ '@esbuild/linux-riscv64': 0.25.3
+ '@esbuild/linux-s390x': 0.25.3
+ '@esbuild/linux-x64': 0.25.3
+ '@esbuild/netbsd-arm64': 0.25.3
+ '@esbuild/netbsd-x64': 0.25.3
+ '@esbuild/openbsd-arm64': 0.25.3
+ '@esbuild/openbsd-x64': 0.25.3
+ '@esbuild/sunos-x64': 0.25.3
+ '@esbuild/win32-arm64': 0.25.3
+ '@esbuild/win32-ia32': 0.25.3
+ '@esbuild/win32-x64': 0.25.3
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@5.2.0(eslint@9.25.1(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.25.1(jiti@2.4.2)
+
+ eslint-plugin-react-refresh@0.4.20(eslint@9.25.1(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.25.1(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.25.1(jiti@2.4.2):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(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.13.0
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.25.1
+ '@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
+
+ fdir@6.4.4(picomatch@4.0.2):
+ optionalDependencies:
+ picomatch: 4.0.2
+
+ 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
+
+ fraction.js@4.3.7: {}
+
+ 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
+
+ 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-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.4
+ 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: {}
+
+ node-releases@2.0.19: {}
+
+ normalize-range@0.1.2: {}
+
+ 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: {}
+
+ picomatch@4.0.2: {}
+
+ postcss-value-parser@4.2.0: {}
+
+ postcss@8.5.3:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ 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.56.1(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ react-router-dom@7.5.1(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.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+
+ react-router@7.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ cookie: 1.0.2
+ react: 19.1.0
+ set-cookie-parser: 2.7.1
+ 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.40.0:
+ dependencies:
+ '@types/estree': 1.0.7
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.40.0
+ '@rollup/rollup-android-arm64': 4.40.0
+ '@rollup/rollup-darwin-arm64': 4.40.0
+ '@rollup/rollup-darwin-x64': 4.40.0
+ '@rollup/rollup-freebsd-arm64': 4.40.0
+ '@rollup/rollup-freebsd-x64': 4.40.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.40.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.40.0
+ '@rollup/rollup-linux-arm64-gnu': 4.40.0
+ '@rollup/rollup-linux-arm64-musl': 4.40.0
+ '@rollup/rollup-linux-loongarch64-gnu': 4.40.0
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.40.0
+ '@rollup/rollup-linux-riscv64-musl': 4.40.0
+ '@rollup/rollup-linux-s390x-gnu': 4.40.0
+ '@rollup/rollup-linux-x64-gnu': 4.40.0
+ '@rollup/rollup-linux-x64-musl': 4.40.0
+ '@rollup/rollup-win32-arm64-msvc': 4.40.0
+ '@rollup/rollup-win32-ia32-msvc': 4.40.0
+ '@rollup/rollup-win32-x64-msvc': 4.40.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.4: {}
+
+ tapable@2.2.1: {}
+
+ tinyglobby@0.2.13:
+ dependencies:
+ fdir: 6.4.4(picomatch@4.0.2)
+ picomatch: 4.0.2
+
+ 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.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/parser': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3)
+ eslint: 9.25.1(jiti@2.4.2)
+ typescript: 5.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.7.3: {}
+
+ update-browserslist-db@1.1.3(browserslist@4.24.4):
+ dependencies:
+ browserslist: 4.24.4
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2):
+ dependencies:
+ esbuild: 0.25.3
+ fdir: 6.4.4(picomatch@4.0.2)
+ picomatch: 4.0.2
+ postcss: 8.5.3
+ rollup: 4.40.0
+ tinyglobby: 0.2.13
+ 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.3: {}
diff --git a/Week8/pocheonLim/mission2/public/images/google.png b/Week8/pocheonLim/mission2/public/images/google.png
new file mode 100644
index 00000000..db9fa09e
Binary files /dev/null and b/Week8/pocheonLim/mission2/public/images/google.png differ
diff --git a/Week8/pocheonLim/mission2/public/images/lp.png b/Week8/pocheonLim/mission2/public/images/lp.png
new file mode 100644
index 00000000..5d936536
Binary files /dev/null and b/Week8/pocheonLim/mission2/public/images/lp.png differ
diff --git a/Week8/pocheonLim/mission2/public/images/user.png b/Week8/pocheonLim/mission2/public/images/user.png
new file mode 100644
index 00000000..6d15e9e6
Binary files /dev/null and b/Week8/pocheonLim/mission2/public/images/user.png differ
diff --git a/Week8/pocheonLim/mission2/src/App.css b/Week8/pocheonLim/mission2/src/App.css
new file mode 100644
index 00000000..e69de29b
diff --git a/Week8/pocheonLim/mission2/src/App.tsx b/Week8/pocheonLim/mission2/src/App.tsx
new file mode 100644
index 00000000..0df26c82
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/App.tsx
@@ -0,0 +1,70 @@
+import { createBrowserRouter, RouterProvider } from 'react-router-dom'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import './App.css'
+import HomeLayout from './layouts/HomeLayout'
+import LoginPage from './pages/LoginPage'
+import SignupPage from './pages/SignupPage'
+import MyPage from './pages/MyPage'
+import { AuthProvider } from './contexts/AuthContext'
+import ProtectedRoute from './components/auth/ProtectedRoute'
+import GoogleLoginRidirectPage from './pages/GoogleLoginRidirectPage'
+import LpListPage from './pages/LpListPage'
+import LpDetailPage from './pages/LpDetailPage'
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ errorElement: Error
,
+ children: [
+ {
+ path: "",
+ element: ,
+ },
+ {
+ path: "login",
+ element: ,
+ },
+ {
+ path: "signup",
+ element: ,
+ },
+ {
+ path: "mypage",
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: "v1/auth/google/callback",
+ element: ,
+ },
+ {
+ path: "lp/:lpId",
+ element: (
+
+
+
+ ),
+ },
+ ],
+ },
+]);
+
+const queryClient = new QueryClient();
+
+function App() {
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/Week8/pocheonLim/mission2/src/apis/auth.ts b/Week8/pocheonLim/mission2/src/apis/auth.ts
new file mode 100644
index 00000000..191d71eb
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/apis/auth.ts
@@ -0,0 +1,196 @@
+import axios from 'axios';
+
+const API_BASE_URL = import.meta.env.VITE_SERVER_API_URL;
+
+// Axios 인스턴스 생성
+export const axiosInstance = axios.create({
+ baseURL: API_BASE_URL,
+});
+
+// 요청 인터셉터
+axiosInstance.interceptors.request.use(
+ (config) => {
+ const accessToken = localStorage.getItem('accessToken');
+ if (accessToken) {
+ console.log('요청에 토큰 추가됨:', accessToken.substring(0, 10) + '...');
+ config.headers.Authorization = `Bearer ${accessToken}`;
+ }
+ return config;
+ },
+ (error) => {
+ console.error('요청 인터셉터 에러:', error);
+ return Promise.reject(error);
+ }
+);
+
+// 응답 인터셉터
+axiosInstance.interceptors.response.use(
+ (response) => {
+ console.log('응답 성공:', response.config.url);
+ return response;
+ },
+ async (error) => {
+ const originalRequest = error.config;
+
+ // 토큰 재발급 요청 자체가 실패한 경우는 인터셉터에서 제외
+ if (originalRequest.url === '/v1/auth/refresh') {
+ console.error('토큰 재발급 실패');
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ window.location.href = '/login';
+ return Promise.reject(error);
+ }
+
+ // 401 에러이고, 이미 재시도한 요청이 아닌 경우
+ if (error.response?.status === 401 && !originalRequest._retry) {
+ console.log('토큰 만료됨, 재발급 시도...');
+ originalRequest._retry = true;
+
+ try {
+ // 토큰 재발급 시도
+ const response = await refreshToken();
+ if (response.status) {
+ console.log('토큰 재발급 성공');
+ // 새로운 토큰 저장
+ localStorage.setItem('accessToken', response.data.accessToken);
+ localStorage.setItem('refreshToken', response.data.refreshToken);
+
+ // 실패한 요청의 Authorization 헤더 업데이트
+ originalRequest.headers.Authorization = `Bearer ${response.data.accessToken}`;
+
+ console.log('실패한 요청 재시도:', originalRequest.url);
+ // 실패한 요청 재시도
+ return axiosInstance(originalRequest);
+ }
+ } catch (refreshError) {
+ console.error('토큰 재발급 실패:', refreshError);
+ // 토큰 재발급 실패 시 로그아웃 처리
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ console.log('로그아웃 처리됨');
+ window.location.href = '/login';
+ return Promise.reject(refreshError);
+ }
+ }
+
+ console.error('❌ API 요청 실패:', error.response?.status, error.config.url);
+ return Promise.reject(error);
+ }
+);
+
+//회원가입 요청
+export interface SignupRequest {
+ name: string;
+ email: string;
+ password: string;
+ bio?: string;
+ avatar?: string;
+}
+
+//회원가입 응답
+export interface SignupResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+}
+
+//회원가입 요청 함수
+export const signup = async (data: SignupRequest): Promise => {
+ const response = await axios.post(`${API_BASE_URL}/v1/auth/signup`, data);
+ return response.data;
+};
+
+//로그인 요청
+export interface LoginRequest {
+ email: string;
+ password: string;
+}
+
+//로그인 응답
+export interface LoginResponse {
+ status: boolean;
+ message: string;
+ statusCode: number;
+ data: {
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+ }
+}
+
+//로그인 요청 함수
+export const login = async (data: LoginRequest): Promise => {
+ const response = await axiosInstance.post('/v1/auth/signin', data);
+ return response.data;
+};
+
+//사용자 정보 응답
+export interface UserInfoResponse {
+ status: boolean;
+ message: string;
+ statusCode: number;
+ data: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ }
+}
+
+//사용자 정보 요청 함수
+export const getUserInfo = async (): Promise => {
+ const response = await axiosInstance.get('/v1/users/me');
+ return response.data;
+};
+
+// 토큰 재발급 응답
+export interface RefreshTokenResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+ }
+}
+
+// 토큰 재발급 요청 함수
+export const refreshToken = async (): Promise => {
+ const refreshToken = localStorage.getItem('refreshToken');
+ if (!refreshToken) {
+ throw new Error('리프레시 토큰이 없습니다.');
+ }
+
+ // axiosInstance 대신 일반 axios를 사용하여 인터셉터 우회
+ const response = await axios.post(`${API_BASE_URL}/v1/auth/refresh`, {
+ refresh: refreshToken
+ }, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+ return response.data;
+};
+
+export const signout = async (): Promise => {
+ await axiosInstance.post('/v1/auth/signout');
+ console.log("로그아웃 연동 성공~");
+};
+
+export const updateProfile = (data: { name: string; bio?: string | null; avatar?: string }) =>
+ axiosInstance.patch('/v1/users', data);
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/apis/lp.ts b/Week8/pocheonLim/mission2/src/apis/lp.ts
new file mode 100644
index 00000000..e6ca0741
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/apis/lp.ts
@@ -0,0 +1,200 @@
+import { Lp } from '../types/lp';
+import { axiosInstance } from './auth';
+
+interface FetchLpListParams {
+ order?: 'asc' | 'desc';
+ cursor?: number;
+ limit?: number;
+ search?: string;
+}
+
+interface LpListResponse {
+ data: {
+ data: Lp[];
+ nextCursor: number;
+ hasNext: boolean;
+ };
+}
+
+interface FetchCommentsParams {
+ lpId: string | number;
+ order?: 'asc' | 'desc';
+ cursor?: number;
+ limit?: number;
+}
+
+export const fetchLpList = async ({ order = 'desc', cursor = 0, limit = 10, search }: FetchLpListParams = {}) => {
+ const searchParams = new URLSearchParams({
+ order,
+ cursor: cursor.toString(),
+ limit: limit.toString(),
+ ...(search && { search }),
+ });
+
+ const url = `${import.meta.env.VITE_SERVER_API_URL}/v1/lps?${searchParams.toString()}`;
+
+ try {
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('API Error:', response.status, response.statusText);
+ throw new Error(`요청 실패 봐라 (${response.status}): ${errorText}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ if (!contentType || !contentType.includes('application/json')) {
+ throw new Error(`틀렸다네;;: ${contentType}`);
+ }
+
+ const data = await response.json();
+ return data.data as LpListResponse['data'];
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`과연: ${error.message}`);
+ }
+ throw new Error('오류입니다');
+ }
+};
+
+export const fetchComments = async ({
+ lpId,
+ order = 'asc',
+ cursor = 0,
+ limit = 10,
+}: FetchCommentsParams) => {
+ const searchParams = new URLSearchParams({
+ order,
+ cursor: cursor.toString(),
+ limit: limit.toString(),
+ });
+ const url = `/v1/lps/${lpId}/comments?${searchParams.toString()}`;
+ const res = await axiosInstance.get(url);
+ const data = res.data;
+ return {
+ data: data.data.data,
+ nextCursor: data.data.nextCursor,
+ hasNext: data.data.hasNext,
+ };
+};
+
+export interface CreateLpRequest {
+ title: string;
+ content: string;
+ thumbnail: string;
+ tags: string[];
+ published: boolean;
+}
+
+export interface CreateLpResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ title: string;
+ content: string;
+ thumbnail: string;
+ published: boolean;
+ authorId: number;
+ createdAt: string;
+ updatedAt: string;
+ };
+}
+
+export const createLp = async (
+ lpData: CreateLpRequest,
+): Promise => {
+ try {
+ console.log("createLp lpData:", lpData);
+ const response = await axiosInstance.post("/v1/lps", lpData);
+ return response.data;
+ } catch (error) {
+ console.log("❌ ~ createLp ~ error:", error);
+ throw error;
+ }
+};
+
+export interface FetchUserLpListParams {
+ order?: 'asc' | 'desc';
+ cursor?: number;
+ limit?: number;
+}
+
+export interface UserLpListResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ data: Lp[];
+ nextCursor: number;
+ hasNext: boolean;
+ };
+}
+
+export const fetchUserLpList = async ({ order = 'desc', cursor = 0, limit = 10 }: FetchUserLpListParams = {}) => {
+ const searchParams = new URLSearchParams({
+ order,
+ cursor: cursor.toString(),
+ limit: limit.toString(),
+ });
+ const res = await axiosInstance.get(`/v1/lps/user?${searchParams.toString()}`);
+ return res.data as UserLpListResponse;
+};
+
+// 댓글 수정
+export const updateComment = (lpId: number, commentId: number, content: string) =>
+ axiosInstance.patch(`/v1/lps/${lpId}/comments/${commentId}`, { content });
+
+// 댓글 삭제
+export const deleteComment = (lpId: number, commentId: number) =>
+ axiosInstance.delete(`/v1/lps/${lpId}/comments/${commentId}`);
+
+// LP 수정
+export interface UpdateLpRequest {
+ title: string;
+ content: string;
+ thumbnail: string;
+ tags: string[];
+ published: boolean;
+}
+
+export interface UpdateLpResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: Lp;
+}
+
+export const updateLp = async (
+ lpId: number,
+ data: UpdateLpRequest
+): Promise => {
+ const res = await axiosInstance.patch(`/v1/lps/${lpId}`, data);
+ return res.data;
+};
+
+export const deleteLp = async (lpId: number) => {
+ const response = await axiosInstance.delete(`/v1/lps/${lpId}`);
+ return response.data;
+};
+
+// 좋아요 추가
+export const addLike = async (lpId: number) => {
+ const response = await axiosInstance.post(`/v1/lps/${lpId}/likes`);
+ return response.data;
+};
+
+// 좋아요 취소
+export const removeLike = async (lpId: number) => {
+ const response = await axiosInstance.delete(`/v1/lps/${lpId}/likes`);
+ return response.data;
+};
+
+// 댓글 생성
+export const createComment = (lpId: number, content: string) =>
+ axiosInstance.post(`/v1/lps/${lpId}/comments`, { content });
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/apis/upload.ts b/Week8/pocheonLim/mission2/src/apis/upload.ts
new file mode 100644
index 00000000..a3afad00
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/apis/upload.ts
@@ -0,0 +1,10 @@
+import { axiosInstance } from './auth';
+
+export const uploadImage = async (file: File): Promise => {
+ const formData = new FormData();
+ formData.append('file', file);
+ const res = await axiosInstance.post('/v1/uploads', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ });
+ return res.data.data.imageUrl;
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/FabButton.tsx b/Week8/pocheonLim/mission2/src/components/FabButton.tsx
new file mode 100644
index 00000000..211a33f5
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/FabButton.tsx
@@ -0,0 +1,27 @@
+import React, { useState } from 'react';
+import { useAuth } from '../contexts/AuthContext';
+import { MdAdd } from 'react-icons/md';
+import LpCreateModal from './LpCreateModal';
+
+const FabButton = () => {
+ const { isLoggedIn } = useAuth();
+ const [open, setOpen] = useState(false);
+
+ if (!isLoggedIn) return null;
+
+ return (
+ <>
+
+ setOpen(false)} />
+ >
+ );
+};
+
+export default FabButton;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/LpCreateModal.tsx b/Week8/pocheonLim/mission2/src/components/LpCreateModal.tsx
new file mode 100644
index 00000000..fab6dedd
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/LpCreateModal.tsx
@@ -0,0 +1,147 @@
+import React, { useRef } from 'react';
+import { IoMdClose } from 'react-icons/io';
+import { useLpCreate } from '../hooks/lp/mutations/useLpCreate';
+import { useTagManagement } from '../hooks/utils/useTagManagement';
+import { uploadImage } from '../apis/upload';
+
+interface LpCreateModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+const LpCreateModal = ({ isOpen, onClose }: LpCreateModalProps) => {
+ const [image, setImage] = React.useState(null);
+ const [name, setName] = React.useState('');
+ const [content, setContent] = React.useState('');
+ const fileInputRef = useRef(null);
+ const { tagInput, setTagInput, tags, handleAddTag, handleRemoveTag, handleTagKeyDown } = useTagManagement();
+ const { mutation, handleAddLp } = useLpCreate();
+
+ if (!isOpen) return null;
+
+ // 모달 바깥 클릭 시 닫기
+ const handleOverlayClick = (e: React.MouseEvent) => {
+ if (e.target === e.currentTarget) onClose();
+ };
+
+ // LP 이미지 클릭 시 파일 input 열기
+ const handleImageClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ // 파일 이미지 변경
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ try {
+ const imageUrl = await uploadImage(file);
+ setImage(imageUrl);
+ } catch {
+ alert('이미지 업로드에 실패했습니다.');
+ }
+ }
+ };
+
+ // 모든 값이 입력되어야 Add LP 활성화
+ const isAddLpEnabled = !!(image && name.trim() && content.trim() && tags.length);
+
+ // LP 생성 요청
+ const handleSubmit = () => {
+ handleAddLp({
+ title: name,
+ content,
+ thumbnail: image || '',
+ tags,
+ published: true,
+ });
+ onClose();
+ };
+
+ return (
+
+
+ {/* X 버튼 */}
+
+ {/* LP 이미지 */}
+
+

+
+
+ {/* 입력 폼 */}
+
setName(e.target.value)}
+ />
+
setContent(e.target.value)}
+ />
+
+ setTagInput(e.target.value)}
+ onKeyDown={handleTagKeyDown}
+ />
+
+
+ {/* 태그 리스트 */}
+
+ {tags.map(tag => (
+
+ {tag}
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default LpCreateModal;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/auth/ProtectedRoute.tsx b/Week8/pocheonLim/mission2/src/components/auth/ProtectedRoute.tsx
new file mode 100644
index 00000000..2584a6be
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/auth/ProtectedRoute.tsx
@@ -0,0 +1,18 @@
+import { Navigate } from 'react-router-dom';
+import { useAuth } from '../../contexts/AuthContext';
+
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+}
+
+const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
+ const { user } = useAuth();
+
+ if (!user) {
+ return ;
+ }
+
+ return <>{children}>;
+};
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/comment/CommentSection.tsx b/Week8/pocheonLim/mission2/src/components/comment/CommentSection.tsx
new file mode 100644
index 00000000..7fd3e1bd
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/comment/CommentSection.tsx
@@ -0,0 +1,174 @@
+import { MdMoreVert, MdEdit, MdDelete } from 'react-icons/md';
+import { Comment } from '../../types/comment';
+import SkeletonComment from '../skeleton/SkeletonComment';
+
+interface CommentSectionProps {
+ commentOrder: 'asc' | 'desc';
+ handleSortChange: (order: 'asc' | 'desc') => void;
+ commentInput: string;
+ setCommentInput: (value: string) => void;
+ commentError: string;
+ isCommentLoading: boolean;
+ isFetchingNextPage: boolean;
+ comments: Comment[];
+ user: { id: number } | null;
+ showMenuId: number | null;
+ setShowMenuId: (id: number | null) => void;
+ editingCommentId: number | null;
+ editedContent: string;
+ setEditedContent: (value: string) => void;
+ handleCommentSubmit: () => void;
+ handleCommentEdit: (id: number, content: string) => void;
+ handleCommentDelete: (id: number) => void;
+ handleCommentUpdate: (id: number, content: string) => void;
+ handleCommentEditCancel: () => void;
+ createCommentMutation: { isPending: boolean };
+ updateCommentMutation: { isPending: boolean };
+ COMMENT_LIMIT: number;
+ scrollRef: React.RefObject;
+}
+
+export const CommentSection = ({
+ commentOrder,
+ handleSortChange,
+ commentInput,
+ setCommentInput,
+ commentError,
+ isCommentLoading,
+ isFetchingNextPage,
+ comments,
+ user,
+ showMenuId,
+ setShowMenuId,
+ editingCommentId,
+ editedContent,
+ setEditedContent,
+ handleCommentSubmit,
+ handleCommentEdit,
+ handleCommentDelete,
+ handleCommentUpdate,
+ handleCommentEditCancel,
+ createCommentMutation,
+ updateCommentMutation,
+ COMMENT_LIMIT,
+ scrollRef,
+}: CommentSectionProps) => {
+ return (
+
+
+
댓글
+
+
+
+
+
+
+
+
+
+
+
+ {commentError &&
{commentError}
}
+
+
+ {(isCommentLoading || isFetchingNextPage) &&
+ Array.from({ length: COMMENT_LIMIT }).map((_, idx) => (
+
+ ))}
+ {!isCommentLoading && !isFetchingNextPage && comments.length === 0 && (
+
아직 댓글이 없습니다.
+ )}
+ {!isCommentLoading && !isFetchingNextPage && comments.map((comment) => {
+ const isMine = user?.id === comment.authorId;
+ return (
+
+

+
+
+
{comment.author.name}
+ {isMine && (
+
+
+ {showMenuId === comment.id && (
+
+
+
+
+ )}
+
+ )}
+
+ {editingCommentId === comment.id ? (
+
+ ) : (
+
{comment.content}
+ )}
+
{new Date(comment.createdAt).toLocaleString()}
+
+
+ );
+ })}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/layout/Header.tsx b/Week8/pocheonLim/mission2/src/components/layout/Header.tsx
new file mode 100644
index 00000000..038c6467
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/layout/Header.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { useAuth } from '../../contexts/AuthContext';
+import { Link } from 'react-router-dom';
+import { useMutation } from '@tanstack/react-query';
+import { signout } from '../../apis/auth';
+
+interface HeaderProps {
+ onHamburgerClick?: () => void;
+}
+
+const Header: React.FC = ({ onHamburgerClick }) => {
+ const { isLoggedIn, user, logout } = useAuth();
+
+ const signoutMutation = useMutation({
+ mutationFn: signout,
+ onSuccess: () => {
+ logout();
+ },
+ onError: () => {
+ alert('로그아웃에 실패했습니다.');
+ }
+ });
+
+ return (
+
+ );
+};
+
+export default Header;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/layout/Sidebar.tsx b/Week8/pocheonLim/mission2/src/components/layout/Sidebar.tsx
new file mode 100644
index 00000000..2ad52436
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/layout/Sidebar.tsx
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { useMutation } from '@tanstack/react-query';
+import { axiosInstance } from '../../apis/auth';
+import { useAuth } from '../../contexts/AuthContext';
+
+interface SidebarProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+const Sidebar: React.FC = ({ open, onClose }) => {
+ const [showWithdrawModal, setShowWithdrawModal] = useState(false);
+ const { logout, isLoggedIn } = useAuth();
+ const navigate = useNavigate();
+
+ const withdrawMutation = useMutation({
+ mutationFn: async () => {
+ const res = await axiosInstance.delete('/v1/users');
+ return res.data;
+ },
+ onSuccess: () => {
+ logout();
+ navigate('/login');
+ },
+ });
+
+ // md 이상에서는 항상 보이고, 그 미만에서는 open일 때만 보임
+ return (
+ <>
+ {/* 오버레이: 모바일/태블릿에서만, 사이드바가 열렸을 때만 보임 */}
+
+ {/* 사이드바 본체 */}
+
+
+
+
+
+
+
+
+ {/* 탈퇴 모달 */}
+ {showWithdrawModal && (
+
+
+
+
정말 탈퇴하시겠습니까?
+
+
+
+
+ {withdrawMutation.isPending &&
탈퇴 처리 중...
}
+ {withdrawMutation.isError &&
탈퇴 중 오류가 발생했습니다.
}
+
+
+ )}
+ >
+ );
+};
+
+export default Sidebar;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/lp/LpContent.tsx b/Week8/pocheonLim/mission2/src/components/lp/LpContent.tsx
new file mode 100644
index 00000000..2ea92db4
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/lp/LpContent.tsx
@@ -0,0 +1,112 @@
+import { useRef } from 'react';
+import { LpDetail } from '../../types/lp';
+
+interface LpContentProps {
+ data: LpDetail;
+ isEditing: boolean;
+ editContent: string;
+ setEditContent: (value: string) => void;
+ editThumbnail: string;
+ editTagInput: string;
+ setEditTagInput: (value: string) => void;
+ editTags: string[];
+ isUploading: boolean;
+ uploadError: string | null;
+ handleAddTag: () => void;
+ handleRemoveTag: (tag: string) => void;
+ handleTagKeyDown: (e: React.KeyboardEvent) => void;
+ handleImageUploadChange: (file: File) => void;
+}
+
+export const LpContent = ({
+ data,
+ isEditing,
+ editContent,
+ setEditContent,
+ editThumbnail,
+ editTagInput,
+ setEditTagInput,
+ editTags,
+ isUploading,
+ uploadError,
+ handleAddTag,
+ handleRemoveTag,
+ handleTagKeyDown,
+ handleImageUploadChange,
+}: LpContentProps) => {
+ const fileInputRef = useRef(null);
+
+ return (
+ <>
+
+ {isEditing ? (
+
fileInputRef.current?.click()}>
+

+
+ {isUploading ? '업로드 중...' : '이미지 변경'}
+
+ {uploadError &&
{uploadError}
}
+
e.target.files?.[0] && handleImageUploadChange(e.target.files[0])}
+ />
+
+ ) : (
+

+ )}
+
+
+ {data.audioUrl && (
+
+ )}
+
+
+ {isEditing ? (
+
+
+
+ {isEditing ? (
+ <>
+ setEditTagInput(e.target.value)}
+ onKeyDown={handleTagKeyDown}
+ className="px-2 py-1 rounded bg-gray-800 border border-gray-700 text-white"
+ placeholder="태그 입력 후 Enter"
+ />
+
+ {editTags.map((tag, idx) => (
+
+ {tag}
+
+
+ ))}
+ >
+ ) : (
+ <>
+ {data.tags?.map((tag: { id: number; name: string }) => (
+ #{tag.name}
+ ))}
+ >
+ )}
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/lp/LpHeader.tsx b/Week8/pocheonLim/mission2/src/components/lp/LpHeader.tsx
new file mode 100644
index 00000000..78acb25c
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/lp/LpHeader.tsx
@@ -0,0 +1,60 @@
+import { MdEdit, MdDelete } from 'react-icons/md';
+import { LpDetail } from '../../types/lp';
+
+interface LpHeaderProps {
+ data: LpDetail;
+ isMyLp: boolean;
+ isEditing: boolean;
+ editTitle: string;
+ setEditTitle: (value: string) => void;
+ setIsEditing: (value: boolean) => void;
+ onDelete: () => void;
+}
+
+export const LpHeader = ({
+ data,
+ isMyLp,
+ isEditing,
+ editTitle,
+ setEditTitle,
+ setIsEditing,
+ onDelete,
+}: LpHeaderProps) => {
+ return (
+ <>
+
+

+
{data.author?.name}
+
{new Date(data.createdAt).toLocaleDateString()}
+
+
+ {isEditing ? (
+ setEditTitle(e.target.value)}
+ className="px-2 py-1 rounded bg-gray-800 border border-gray-700 text-white w-full"
+ autoFocus
+ />
+ ) : (
+ {data.title}
+ )}
+
+ {isMyLp && !isEditing && (
+
+
+
+
+ )}
+ >
+ );
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/signup/CompletionStep.tsx b/Week8/pocheonLim/mission2/src/components/signup/CompletionStep.tsx
new file mode 100644
index 00000000..c3053c59
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/signup/CompletionStep.tsx
@@ -0,0 +1,40 @@
+import { useSignup } from '../../contexts/SignupContext';
+import { useNavigate } from 'react-router-dom';
+
+const CompletionStep = () => {
+ const { email, nickname } = useSignup();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
{nickname}님,
+
환영합니다!
+
+
+
+
가입하신 이메일
+
+ mail
+ {email}
+
+
+
+
+
+ );
+};
+
+export default CompletionStep;
diff --git a/Week8/pocheonLim/mission2/src/components/signup/EmailStep.tsx b/Week8/pocheonLim/mission2/src/components/signup/EmailStep.tsx
new file mode 100644
index 00000000..dbe7dbb5
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/signup/EmailStep.tsx
@@ -0,0 +1,51 @@
+import { useSignup } from '../../contexts/SignupContext';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { emailSchema, type EmailFormData } from '../../schemas/signup.schema';
+
+const EmailStep = () => {
+ const { setEmail, setStep } = useSignup();
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(emailSchema),
+ mode: 'onChange',
+ });
+
+ const onSubmit = (data: EmailFormData) => {
+ setEmail(data.email);
+ setStep(2);
+ };
+
+ return (
+
+ );
+};
+
+export default EmailStep;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/signup/NicknameStep.tsx b/Week8/pocheonLim/mission2/src/components/signup/NicknameStep.tsx
new file mode 100644
index 00000000..05809f00
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/signup/NicknameStep.tsx
@@ -0,0 +1,95 @@
+import { useSignup } from '../../contexts/SignupContext';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { nicknameSchema, type NicknameFormData } from '../../schemas/signup.schema';
+import { signup } from '../../apis/auth';
+import { useNavigate } from 'react-router-dom';
+import { useState } from 'react';
+import { AxiosError } from 'axios';
+
+const NicknameStep = () => {
+ const { email, password, setNickname, clearSignupData } = useSignup();
+ const navigate = useNavigate();
+ const [errorMessage, setErrorMessage] = useState('');
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(nicknameSchema),
+ mode: 'onChange',
+ });
+
+ const onSubmit = async (data: NicknameFormData) => {
+ try {
+ setNickname(data.nickname);
+ setErrorMessage(''); // 에러 메시지 초기화
+
+ //API 요청하고 RESPONSE 받아오기
+ const response = await signup({
+ name: data.nickname,
+ email,
+ password,
+ });
+
+ if (response.status) {
+ // 회원가입 완료 후 데이터 초기화
+ clearSignupData();
+ // 로그인 페이지로 이동
+ navigate('/login');
+ } else {
+ setErrorMessage(response.message || '회원가입에 실패했습니다.');
+ }
+ } catch (error) {
+ if (error instanceof AxiosError && error.response?.status === 409) {
+ setErrorMessage('이미 사용 중인 이메일 또는 닉네임입니다.');
+ } else {
+ setErrorMessage('회원가입 중 오류가 발생했습니다. 다시 시도해주세요.');
+ }
+ console.error('회원가입 중 오류 발생:', error);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default NicknameStep;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/signup/PasswordStep.tsx b/Week8/pocheonLim/mission2/src/components/signup/PasswordStep.tsx
new file mode 100644
index 00000000..501d998a
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/signup/PasswordStep.tsx
@@ -0,0 +1,93 @@
+import { useSignup } from '../../contexts/SignupContext';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { passwordSchema, type PasswordFormData } from '../../schemas/signup.schema';
+import { useState } from 'react';
+
+const PasswordStep = () => {
+ const { setPassword, setStep } = useSignup();
+ const [showPassword, setShowPassword] = useState(false);
+ const [showPasswordConfirm, setShowPasswordConfirm] = useState(false);
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(passwordSchema),
+ mode: 'onChange',
+ });
+
+ const onSubmit = (data: PasswordFormData) => {
+ setPassword(data.password);
+ setStep(3);
+ };
+
+ return (
+
+ );
+};
+
+export default PasswordStep;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/skeleton/SkeletonComment.tsx b/Week8/pocheonLim/mission2/src/components/skeleton/SkeletonComment.tsx
new file mode 100644
index 00000000..16dd8d2e
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/skeleton/SkeletonComment.tsx
@@ -0,0 +1,11 @@
+const SkeletonComment = () => (
+
+);
+
+export default SkeletonComment;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/components/skeleton/SkeletonLpCard.tsx b/Week8/pocheonLim/mission2/src/components/skeleton/SkeletonLpCard.tsx
new file mode 100644
index 00000000..4eb33e48
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/components/skeleton/SkeletonLpCard.tsx
@@ -0,0 +1,19 @@
+const SkeletonLpCard = () => (
+
+);
+
+export default SkeletonLpCard;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/contexts/AuthContext.tsx b/Week8/pocheonLim/mission2/src/contexts/AuthContext.tsx
new file mode 100644
index 00000000..cc182e7a
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/contexts/AuthContext.tsx
@@ -0,0 +1,115 @@
+import { createContext, useContext, ReactNode, useState, useEffect } from 'react';
+import { getUserInfo } from '../apis/auth';
+
+interface User {
+ id: number;
+ email: string;
+ name: string;
+ bio?: string | null;
+ avatar?: string | null;
+ createdAt?: string;
+ updatedAt?: string;
+}
+
+interface AuthContextType {
+ isLoggedIn: boolean;
+ user: User | null;
+ isLoading: boolean;
+ login: (accessToken: string, refreshToken: string) => Promise;
+ logout: () => void;
+ updateUser: (userData: Partial) => void;
+}
+
+const AuthContext = createContext(undefined);
+
+const useAuthStorage = () => {
+ const getTokens = () => ({
+ accessToken: localStorage.getItem('accessToken'),
+ refreshToken: localStorage.getItem('refreshToken'),
+ });
+
+ const setTokens = (accessToken: string, refreshToken: string) => {
+ localStorage.setItem('accessToken', accessToken);
+ localStorage.setItem('refreshToken', refreshToken);
+ };
+
+ const clearTokens = () => {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ };
+
+ return { getTokens, setTokens, clearTokens };
+};
+
+export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const { getTokens, setTokens, clearTokens } = useAuthStorage();
+
+ const fetchUserInfo = async () => {
+ try {
+ const response = await getUserInfo();
+ if (response.status) {
+ setUser(response.data);
+ setIsLoggedIn(true);
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('사용자 정보 조회 중 오류 발생:', error);
+ return false;
+ }
+ };
+
+ useEffect(() => {
+ const initializeAuth = async () => {
+ const { accessToken } = getTokens();
+ if (!accessToken) {
+ setIsLoading(false);
+ return;
+ }
+
+ const success = await fetchUserInfo();
+ if (!success) {
+ logout();
+ }
+ setIsLoading(false);
+ };
+
+ initializeAuth();
+ }, []);
+
+ const login = async (accessToken: string, refreshToken: string) => {
+ setTokens(accessToken, refreshToken);
+ await fetchUserInfo();
+ };
+
+ const logout = () => {
+ clearTokens();
+ setIsLoggedIn(false);
+ setUser(null);
+ };
+
+ const updateUser = (userData: Partial) => {
+ setUser(prev => prev ? { ...prev, ...userData } : null);
+ };
+
+ if (isLoading) {
+ return 로딩 중...
;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth는 AuthProvider 내에서만 사용할 수 있습니다.');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/contexts/SignupContext.tsx b/Week8/pocheonLim/mission2/src/contexts/SignupContext.tsx
new file mode 100644
index 00000000..3913e303
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/contexts/SignupContext.tsx
@@ -0,0 +1,61 @@
+import { createContext, useContext, ReactNode } from 'react';
+import useLocalStorage from '../hooks/auth/useLocalStorage';
+
+interface SignupContextType {
+ email: string;
+ setEmail: (email: string) => void;
+ password: string;
+ setPassword: (password: string) => void;
+ nickname: string;
+ setNickname: (nickname: string) => void;
+ step: number;
+ setStep: (step: number) => void;
+ clearSignupData: () => void; // 회원가입 완료 후 데이터 초기화
+}
+
+const SignupContext = createContext(undefined);
+
+export const SignupProvider = ({ children }: { children: ReactNode }) => {
+ const [email, setEmail] = useLocalStorage('signup_email', '');
+ const [password, setPassword] = useLocalStorage('signup_password', '');
+ const [nickname, setNickname] = useLocalStorage('signup_nickname', '');
+ const [step, setStep] = useLocalStorage('signup_step', 1);
+
+ // 회원가입 완료 후 모든 데이터 초기화
+ const clearSignupData = () => {
+ localStorage.removeItem('signup_email');
+ localStorage.removeItem('signup_password');
+ localStorage.removeItem('signup_nickname');
+ localStorage.removeItem('signup_step');
+ setEmail('');
+ setPassword('');
+ setNickname('');
+ setStep(1);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useSignup = () => {
+ const context = useContext(SignupContext);
+ if (context === undefined) {
+ throw new Error('useSignup must be used within a SignupProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/auth/useLocalStorage.ts b/Week8/pocheonLim/mission2/src/hooks/auth/useLocalStorage.ts
new file mode 100644
index 00000000..f76977da
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/auth/useLocalStorage.ts
@@ -0,0 +1,35 @@
+import { useState } from 'react';
+
+function useLocalStorage(key: string, initialValue: T) {
+ // 초기값을 가져오는 함수
+ const getStoredValue = () => {
+ try {
+ const item = window.localStorage.getItem(key);
+ return item ? JSON.parse(item) : initialValue;
+ } catch (error) {
+ console.error('Error reading from localStorage:', error);
+ return initialValue;
+ }
+ };
+
+ // 상태 초기화
+ const [storedValue, setStoredValue] = useState(getStoredValue);
+
+ // localStorage에 값을 저장하는 함수
+ const setValue = (value: T | ((val: T) => T)) => {
+ try {
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
+ setStoredValue(valueToStore);
+ window.localStorage.setItem(
+ key,
+ typeof valueToStore === 'string' ? valueToStore : JSON.stringify(valueToStore)
+ );
+ } catch (error) {
+ console.error('Error saving to localStorage:', error);
+ }
+ };
+
+ return [storedValue, setValue] as const;
+}
+
+export default useLocalStorage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/auth/useLoginForm.ts b/Week8/pocheonLim/mission2/src/hooks/auth/useLoginForm.ts
new file mode 100644
index 00000000..9c81948a
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/auth/useLoginForm.ts
@@ -0,0 +1,65 @@
+import { useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router-dom';
+import { useMutation } from '@tanstack/react-query';
+import { axiosInstance } from '../../apis/auth';
+import type { LoginFormData } from '../../schemas/login.schema';
+import { useAuth } from '../../contexts/AuthContext';
+
+export const useLoginForm = () => {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid },
+ } = useForm({
+ mode: 'onChange',
+ });
+
+ const navigate = useNavigate();
+ const { login: authLogin } = useAuth();
+
+ const loginMutation = useMutation({
+ mutationFn: async (data: LoginFormData) => {
+ const response = await axiosInstance.post('/v1/auth/signin', data);
+ return response.data;
+ },
+ onSuccess: async (response) => {
+ if (response.status) {
+ await authLogin(response.data.accessToken, response.data.refreshToken);
+ navigate('/');
+ }
+ },
+ onError: (error) => {
+ console.error('로그인 중 오류 발생:', error);
+ },
+ });
+
+ const emailRegister = register('email', {
+ required: true,
+ pattern: {
+ value: /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i,
+ message: '올바른 이메일 형식을 입력해주세요.',
+ },
+ });
+
+ const passwordRegister = register('password', {
+ required: true,
+ minLength: {
+ value: 8,
+ message: '비밀번호는 8자 이상이어야 합니다.',
+ },
+ });
+
+ const onSubmit = async (data: LoginFormData) => {
+ loginMutation.mutate(data);
+ };
+
+ return {
+ emailRegister,
+ passwordRegister,
+ handleSubmit,
+ onSubmit,
+ errors,
+ isValid,
+ isPending: loginMutation.isPending,
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/comment/useCommentManagement.ts b/Week8/pocheonLim/mission2/src/hooks/comment/useCommentManagement.ts
new file mode 100644
index 00000000..448c8e98
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/comment/useCommentManagement.ts
@@ -0,0 +1,78 @@
+import { useState } from 'react';
+import { useComments, COMMENT_LIMIT } from './useComments';
+import { useCreateComment } from '../lp/comments/useCreateComment';
+import { useUpdateComment } from '../lp/comments/useUpdateComment';
+import { useDeleteComment } from '../lp/comments/useDeleteComment';
+import { useAuth } from '../../contexts/AuthContext';
+
+export const useCommentManagement = (lpId: string, commentOrder: 'asc' | 'desc') => {
+ const [commentInput, setCommentInput] = useState('');
+ const [commentError, setCommentError] = useState('');
+ const [editingCommentId, setEditingCommentId] = useState(null);
+ const [editedContent, setEditedContent] = useState('');
+ const [showMenuId, setShowMenuId] = useState(null);
+ const { user } = useAuth();
+
+ const {
+ data: commentData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading: isCommentLoading,
+ } = useComments(lpId, commentOrder);
+
+ const createCommentMutation = useCreateComment(lpId, commentOrder, setCommentInput, setCommentError);
+ const updateCommentMutation = useUpdateComment(lpId, commentOrder, setEditingCommentId, setEditedContent, setCommentError);
+ const deleteCommentMutation = useDeleteComment(lpId, commentOrder, setCommentError);
+
+ const handleCommentSubmit = () => {
+ if (!lpId) return;
+ createCommentMutation.mutate({ lpId: Number(lpId), content: commentInput });
+ };
+
+ const handleCommentEdit = (commentId: number, content: string) => {
+ setEditingCommentId(commentId);
+ setEditedContent(content);
+ setShowMenuId(null);
+ };
+
+ const handleCommentDelete = (commentId: number) => {
+ setShowMenuId(null);
+ deleteCommentMutation.mutate(commentId);
+ };
+
+ const handleCommentUpdate = (commentId: number, content: string) => {
+ updateCommentMutation.mutate({ commentId, content });
+ };
+
+ const handleCommentEditCancel = () => {
+ setEditingCommentId(null);
+ setEditedContent('');
+ };
+
+ return {
+ commentInput,
+ setCommentInput,
+ commentError,
+ editingCommentId,
+ editedContent,
+ setEditedContent,
+ showMenuId,
+ setShowMenuId,
+ user,
+ commentData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isCommentLoading,
+ createCommentMutation,
+ updateCommentMutation,
+ deleteCommentMutation,
+ handleCommentSubmit,
+ handleCommentEdit,
+ handleCommentDelete,
+ handleCommentUpdate,
+ handleCommentEditCancel,
+ COMMENT_LIMIT
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/comment/useCommentSort.ts b/Week8/pocheonLim/mission2/src/hooks/comment/useCommentSort.ts
new file mode 100644
index 00000000..c825f0b6
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/comment/useCommentSort.ts
@@ -0,0 +1,16 @@
+import { useState } from 'react';
+
+export type CommentOrder = 'asc' | 'desc';
+
+export const useCommentSort = () => {
+ const [commentOrder, setCommentOrder] = useState('asc');
+
+ const handleSortChange = (order: CommentOrder) => {
+ setCommentOrder(order);
+ };
+
+ return {
+ commentOrder,
+ handleSortChange
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/comment/useComments.ts b/Week8/pocheonLim/mission2/src/hooks/comment/useComments.ts
new file mode 100644
index 00000000..3796a43c
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/comment/useComments.ts
@@ -0,0 +1,18 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { fetchComments } from '../../apis/lp';
+
+export const COMMENT_LIMIT = 5;
+
+export const useComments = (lpId: string | undefined, order: 'asc' | 'desc') => {
+ return useInfiniteQuery({
+ queryKey: ['comments', lpId, order],
+ queryFn: ({ pageParam = 0 }) => fetchComments({ lpId: lpId!, order, cursor: pageParam, limit: COMMENT_LIMIT }),
+ getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.nextCursor : undefined,
+ initialPageParam: 0,
+ enabled: !!lpId,
+ staleTime: 0,
+ gcTime: 1000 * 60 * 5,
+ refetchInterval: false,
+ refetchIntervalInBackground: false,
+ });
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/like/useLikeManagement.ts b/Week8/pocheonLim/mission2/src/hooks/like/useLikeManagement.ts
new file mode 100644
index 00000000..3c9ec04a
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/like/useLikeManagement.ts
@@ -0,0 +1,30 @@
+import { useAddLike } from '../lp/likes/useAddLike';
+import { useRemoveLike } from '../lp/likes/useRemoveLike';
+import { useAuth } from '../../contexts/AuthContext';
+
+export const useLikeManagement = (lpId: string, commentOrder: 'asc' | 'desc', likes: { userId: number }[] | undefined) => {
+ const { user } = useAuth();
+ const addLikeMutation = useAddLike(lpId);
+ const removeLikeMutation = useRemoveLike(lpId);
+
+ const isLiked = likes?.some(like => like.userId === user?.id);
+
+ const handleLikeClick = () => {
+ if (!user) {
+ alert('로그인이 필요합니다.');
+ return;
+ }
+ if (isLiked) {
+ removeLikeMutation.mutate();
+ } else {
+ addLikeMutation.mutate();
+ }
+ };
+
+ return {
+ isLiked,
+ handleLikeClick,
+ addLikeMutation,
+ removeLikeMutation
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/comments/useCreateComment.ts b/Week8/pocheonLim/mission2/src/hooks/lp/comments/useCreateComment.ts
new file mode 100644
index 00000000..85dd5c33
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/comments/useCreateComment.ts
@@ -0,0 +1,35 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { createComment } from '../../../apis/lp';
+
+export function useCreateComment(
+ lpId: number | string,
+ commentOrder: 'asc' | 'desc',
+ setCommentInput: (v: string) => void,
+ setCommentError: (v: string) => void
+) {
+ const queryClient = useQueryClient();
+
+ const createCommentMutation = useMutation({
+ mutationFn: ({ lpId, content }: { lpId: number; content: string }) =>
+ createComment(Number(lpId), content),
+ onSuccess: () => {
+ setCommentInput('');
+ setCommentError('');
+ queryClient.invalidateQueries({ queryKey: ['comments', lpId, commentOrder] });
+ },
+ onError: (err: unknown) => {
+ if (
+ typeof err === 'object' &&
+ err !== null &&
+ 'response' in err &&
+ typeof (err as { response?: { data?: { message?: string } } }).response?.data?.message === 'string'
+ ) {
+ setCommentError((err as { response: { data: { message: string } } }).response.data.message);
+ } else {
+ setCommentError('댓글 등록 중 오류가 발생했습니다.');
+ }
+ },
+ });
+
+ return createCommentMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/comments/useDeleteComment.ts b/Week8/pocheonLim/mission2/src/hooks/lp/comments/useDeleteComment.ts
new file mode 100644
index 00000000..b6c7c67f
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/comments/useDeleteComment.ts
@@ -0,0 +1,29 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deleteComment } from '../../../apis/lp';
+
+export function useDeleteComment(
+ lpId: string,
+ commentOrder: 'asc' | 'desc',
+ setCommentError: (v: string) => void
+) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (commentId: number) => deleteComment(Number(lpId), commentId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['comments', lpId, commentOrder] });
+ },
+ onError: (err: unknown) => {
+ if (
+ typeof err === 'object' &&
+ err !== null &&
+ 'response' in err &&
+ typeof (err as { response?: { data?: { message?: string } } }).response?.data?.message === 'string'
+ ) {
+ setCommentError((err as { response: { data: { message: string } } }).response.data.message);
+ } else {
+ setCommentError('댓글 삭제 중 오류가 발생했습니다.');
+ }
+ },
+ });
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/comments/useUpdateComment.ts b/Week8/pocheonLim/mission2/src/hooks/lp/comments/useUpdateComment.ts
new file mode 100644
index 00000000..3caeeaf6
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/comments/useUpdateComment.ts
@@ -0,0 +1,34 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateComment } from '../../../apis/lp';
+
+export function useUpdateComment(
+ lpId: string,
+ commentOrder: 'asc' | 'desc',
+ setEditingCommentId: (v: number | null) => void,
+ setEditedContent: (v: string) => void,
+ setCommentError: (v: string) => void
+) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ commentId, content }: { commentId: number; content: string }) =>
+ updateComment(Number(lpId), commentId, content),
+ onSuccess: () => {
+ setEditingCommentId(null);
+ setEditedContent('');
+ queryClient.invalidateQueries({ queryKey: ['comments', lpId, commentOrder] });
+ },
+ onError: (err: unknown) => {
+ if (
+ typeof err === 'object' &&
+ err !== null &&
+ 'response' in err &&
+ typeof (err as { response?: { data?: { message?: string } } }).response?.data?.message === 'string'
+ ) {
+ setCommentError((err as { response: { data: { message: string } } }).response.data.message);
+ } else {
+ setCommentError('댓글 수정 중 오류가 발생했습니다.');
+ }
+ },
+ });
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/likes/useAddLike.ts b/Week8/pocheonLim/mission2/src/hooks/lp/likes/useAddLike.ts
new file mode 100644
index 00000000..94c66ecc
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/likes/useAddLike.ts
@@ -0,0 +1,33 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { addLike } from '../../../apis/lp';
+import { Lp } from '../../../types/lp';
+
+export function useAddLike(lpId: number | string) {
+ const queryClient = useQueryClient();
+
+ const addLikeMutation = useMutation({
+ mutationFn: () => addLike(Number(lpId)),
+ onMutate: async () => {
+ const previousData = queryClient.getQueryData(['lpDetail', lpId]);
+ queryClient.setQueryData(['lpDetail', lpId], (old: Lp | undefined) => {
+ if (!old) return old;
+ return {
+ ...old,
+ likes: [...(old.likes || []), { userId: null, lpId: Number(lpId) }]
+ };
+ });
+ return { previousData };
+ },
+ onError: (_, __, context) => {
+ if (context?.previousData) {
+ queryClient.setQueryData(['lpDetail', lpId], context.previousData);
+ }
+ alert('좋아요 추가에 실패했습니다.');
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpDetail', lpId] });
+ }
+ });
+
+ return addLikeMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/likes/useRemoveLike.ts b/Week8/pocheonLim/mission2/src/hooks/lp/likes/useRemoveLike.ts
new file mode 100644
index 00000000..3e8254d9
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/likes/useRemoveLike.ts
@@ -0,0 +1,33 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { removeLike } from '../../../apis/lp';
+import { Lp } from '../../../types/lp';
+
+export function useRemoveLike(lpId: number | string) {
+ const queryClient = useQueryClient();
+
+ const removeLikeMutation = useMutation({
+ mutationFn: () => removeLike(Number(lpId)),
+ onMutate: async () => {
+ const previousData = queryClient.getQueryData(['lpDetail', lpId]);
+ queryClient.setQueryData(['lpDetail', lpId], (old: Lp | undefined) => {
+ if (!old) return old;
+ return {
+ ...old,
+ likes: (old.likes || []).filter((like: Lp['likes'][number]) => like.userId !== null)
+ };
+ });
+ return { previousData };
+ },
+ onError: (_, __, context) => {
+ if (context?.previousData) {
+ queryClient.setQueryData(['lpDetail', lpId], context.previousData);
+ }
+ alert('좋아요 취소에 실패했습니다.');
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpDetail', lpId] });
+ }
+ });
+
+ return removeLikeMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useDeleteLp.ts b/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useDeleteLp.ts
new file mode 100644
index 00000000..86897090
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useDeleteLp.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deleteLp } from '../../../apis/lp';
+
+export function useDeleteLp(lpId: number | string, navigate: (path: string) => void) {
+ const queryClient = useQueryClient();
+
+ const deleteLpMutation = useMutation({
+ mutationFn: () => deleteLp(Number(lpId)),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['lps'] });
+ queryClient.invalidateQueries({ queryKey: ['myLps'] });
+ navigate('/mypage');
+ },
+ onError: () => {
+ alert('LP 삭제에 실패했습니다.');
+ }
+ });
+
+ return deleteLpMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useLpCreate.ts b/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useLpCreate.ts
new file mode 100644
index 00000000..f26cef8a
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useLpCreate.ts
@@ -0,0 +1,31 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { createLp } from '../../../apis/lp';
+
+export const useLpCreate = () => {
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: createLp,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['userLpList'] });
+ },
+ onError: () => {
+ alert('LP 생성에 실패했습니다.');
+ }
+ });
+
+ const handleAddLp = (data: {
+ title: string;
+ content: string;
+ thumbnail: string;
+ tags: string[];
+ published: boolean;
+ }) => {
+ mutation.mutate(data);
+ };
+
+ return {
+ mutation,
+ handleAddLp
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useLpEdit.ts b/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useLpEdit.ts
new file mode 100644
index 00000000..f8169301
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useLpEdit.ts
@@ -0,0 +1,78 @@
+import { useState, useEffect } from 'react';
+import { useImageUpload } from '../../utils/useImageUpload';
+import { LpDetail } from '../../../types/lp';
+
+export const useLpEdit = (initialData: LpDetail | undefined) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editTitle, setEditTitle] = useState('');
+ const [editContent, setEditContent] = useState('');
+ const [editThumbnail, setEditThumbnail] = useState('');
+ const [editTagInput, setEditTagInput] = useState('');
+ const [editTags, setEditTags] = useState([]);
+ const { handleImageUpload, isUploading, error: uploadError } = useImageUpload();
+
+ useEffect(() => {
+ if (initialData) {
+ setEditTitle(initialData.title);
+ setEditContent(initialData.content);
+ setEditThumbnail(initialData.thumbnail);
+ setEditTags(initialData.tags ? initialData.tags.map(tag => tag.name) : []);
+ }
+ }, [initialData]);
+
+ const handleAddTag = () => {
+ const tag = editTagInput.trim();
+ if (tag && !editTags.includes(tag)) {
+ setEditTags([...editTags, tag]);
+ setEditTagInput('');
+ }
+ };
+
+ const handleRemoveTag = (tag: string) => {
+ setEditTags(editTags.filter(t => t !== tag));
+ };
+
+ const handleTagKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && editTagInput.trim()) {
+ e.preventDefault();
+ handleAddTag();
+ }
+ };
+
+ const handleImageUploadChange = async (file: File) => {
+ const imageUrl = await handleImageUpload(file);
+ if (imageUrl) {
+ setEditThumbnail(imageUrl);
+ }
+ };
+
+ const resetEditForm = () => {
+ if (initialData) {
+ setEditTitle(initialData.title);
+ setEditContent(initialData.content);
+ setEditThumbnail(initialData.thumbnail);
+ setEditTags(initialData.tags ? initialData.tags.map(tag => tag.name) : []);
+ }
+ setIsEditing(false);
+ };
+
+ return {
+ isEditing,
+ setIsEditing,
+ editTitle,
+ setEditTitle,
+ editContent,
+ setEditContent,
+ editThumbnail,
+ editTagInput,
+ setEditTagInput,
+ editTags,
+ isUploading,
+ uploadError,
+ handleAddTag,
+ handleRemoveTag,
+ handleTagKeyDown,
+ handleImageUploadChange,
+ resetEditForm
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useUpdateLp.ts b/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useUpdateLp.ts
new file mode 100644
index 00000000..abc27286
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/mutations/useUpdateLp.ts
@@ -0,0 +1,21 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateLp } from '../../../apis/lp';
+
+export function useUpdateLp(lpId: number | string) {
+ const queryClient = useQueryClient();
+
+ const updateLpMutation = useMutation({
+ mutationFn: (updateData: { title: string; content: string; thumbnail: string; tags: string[]; published: boolean }) =>
+ updateLp(Number(lpId), updateData),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpDetail', lpId] });
+ queryClient.invalidateQueries({ queryKey: ['lps'] });
+ queryClient.invalidateQueries({ queryKey: ['myLps'] });
+ },
+ onError: () => {
+ alert('LP 수정에 실패했습니다.');
+ }
+ });
+
+ return updateLpMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpDetail.ts b/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpDetail.ts
new file mode 100644
index 00000000..de60f170
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpDetail.ts
@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query';
+import { axiosInstance } from '../../../apis/auth';
+import { LpDetail } from '../../../types/lp';
+
+const fetchLpDetail = async (lpId: string): Promise => {
+ try {
+ const res = await axiosInstance.get(`/v1/lps/${lpId}`);
+ return res.data.data;
+ } catch (error) {
+ console.error('LP 상세 정보 조회 실패:', error);
+ throw error;
+ }
+};
+
+export const useLpDetail = (lpId: string | undefined) => {
+ return useQuery({
+ queryKey: ['lpDetail', lpId],
+ queryFn: () => fetchLpDetail(lpId!),
+ enabled: !!lpId,
+ retry: 1,
+ });
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpList.ts b/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpList.ts
new file mode 100644
index 00000000..dcf37072
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpList.ts
@@ -0,0 +1,17 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { fetchLpList } from '../../../apis/lp';
+
+export const LIMIT = 30;
+
+export const useLpList = (order: 'asc' | 'desc', search?: string) => {
+ return useInfiniteQuery({
+ queryKey: ['lpList', order, search],
+ queryFn: ({ pageParam = 0 }) => fetchLpList({ order, cursor: pageParam, limit: LIMIT, search }),
+ getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.nextCursor : undefined,
+ initialPageParam: 0,
+ staleTime: 1000 * 60 * 2,
+ gcTime: 1000 * 60 * 10,
+ refetchOnMount: true,
+ refetchOnWindowFocus: false,
+ });
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpSort.ts b/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpSort.ts
new file mode 100644
index 00000000..3d9e3f42
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/lp/queries/useLpSort.ts
@@ -0,0 +1,21 @@
+import { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { fetchUserLpList } from '../../../apis/lp';
+
+export const useLpSort = () => {
+ const [order, setOrder] = useState<'asc' | 'desc'>('asc');
+
+ const { data, isLoading, isError, error } = useQuery({
+ queryKey: ['userLpList', order],
+ queryFn: () => fetchUserLpList({ order }),
+ });
+
+ return {
+ order,
+ setOrder,
+ data,
+ isLoading,
+ isError,
+ error
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/profile/useProfileEdit.ts b/Week8/pocheonLim/mission2/src/hooks/profile/useProfileEdit.ts
new file mode 100644
index 00000000..96a36fd8
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/profile/useProfileEdit.ts
@@ -0,0 +1,43 @@
+import { useState } from 'react';
+import { User } from '../../types/user';
+
+type UserWithOptionalFields = Omit & {
+ bio?: string | null;
+ avatar?: string | null;
+ createdAt?: string;
+ updatedAt?: string;
+};
+
+export const useProfileEdit = (user: UserWithOptionalFields | null) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedName, setEditedName] = useState(user?.name || '');
+ const [editedBio, setEditedBio] = useState(user?.bio || '');
+ const [previewUrl, setPreviewUrl] = useState(null);
+
+ const handleImageChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setPreviewUrl(URL.createObjectURL(file));
+ }
+ };
+
+ const resetEditForm = () => {
+ setIsEditing(false);
+ setEditedName(user?.name || '');
+ setEditedBio(user?.bio || '');
+ setPreviewUrl(null);
+ };
+
+ return {
+ isEditing,
+ setIsEditing,
+ editedName,
+ setEditedName,
+ editedBio,
+ setEditedBio,
+ previewUrl,
+ setPreviewUrl,
+ handleImageChange,
+ resetEditForm
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/profile/useProfileMutation.ts b/Week8/pocheonLim/mission2/src/hooks/profile/useProfileMutation.ts
new file mode 100644
index 00000000..cf19fa53
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/profile/useProfileMutation.ts
@@ -0,0 +1,44 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updateProfile } from '../../apis/auth';
+import { User } from '../../types/user';
+
+type UserWithOptionalFields = Omit & {
+ bio?: string | null;
+ avatar?: string | null;
+ createdAt?: string;
+ updatedAt?: string;
+};
+
+export function useProfileMutation(user: UserWithOptionalFields | null, updateUser: (u: UserWithOptionalFields) => void) {
+ const queryClient = useQueryClient();
+
+ const updateProfileMutation = useMutation({
+ mutationFn: async (data: { name: string; bio?: string | null; avatar?: string }) => {
+ const response = await updateProfile(data);
+ return response.data;
+ },
+ onMutate: async (newData) => {
+ const previousUser = user;
+ if (previousUser) {
+ updateUser({
+ ...previousUser,
+ name: newData.name,
+ bio: newData.bio || null,
+ avatar: newData.avatar || previousUser.avatar,
+ });
+ }
+ return { previousUser };
+ },
+ onError: (_, __, context) => {
+ if (context?.previousUser) {
+ updateUser(context.previousUser);
+ }
+ alert('프로필 업데이트에 실패했습니다.');
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: ['user'] });
+ },
+ });
+
+ return updateProfileMutation;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/utils/useDebounce.ts b/Week8/pocheonLim/mission2/src/hooks/utils/useDebounce.ts
new file mode 100644
index 00000000..8a3adf9a
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/utils/useDebounce.ts
@@ -0,0 +1,14 @@
+import { useEffect, useState } from "react";
+
+function useDebounce(value: T, delay: number) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const handler = setTimeout(() => setDebouncedValue(value), delay);
+ return () => clearTimeout(handler);
+ }, [value, delay]);
+
+ return debouncedValue;
+}
+
+export default useDebounce;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/utils/useImageUpload.ts b/Week8/pocheonLim/mission2/src/hooks/utils/useImageUpload.ts
new file mode 100644
index 00000000..38a93f1d
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/utils/useImageUpload.ts
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+import { uploadImage } from '../../apis/upload';
+
+export const useImageUpload = () => {
+ const [isUploading, setIsUploading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleImageUpload = async (file: File) => {
+ try {
+ setIsUploading(true);
+ setError(null);
+ const imageUrl = await uploadImage(file);
+ return imageUrl;
+ } catch (err) {
+ setError('이미지 업로드에 실패했습니다.');
+ throw err;
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ return {
+ handleImageUpload,
+ isUploading,
+ error
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/utils/useInfiniteScroll.ts b/Week8/pocheonLim/mission2/src/hooks/utils/useInfiniteScroll.ts
new file mode 100644
index 00000000..7e3dd697
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/utils/useInfiniteScroll.ts
@@ -0,0 +1,27 @@
+import { useEffect, useRef } from 'react';
+
+interface UseInfiniteScrollProps {
+ hasNextPage: boolean;
+ isFetchingNextPage: boolean;
+ fetchNextPage: () => void;
+}
+
+export const useInfiniteScroll = ({ hasNextPage, isFetchingNextPage, fetchNextPage }: UseInfiniteScrollProps) => {
+ const scrollRef = useRef(null);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ if (!scrollRef.current) return;
+ const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
+ if (scrollTop + clientHeight >= scrollHeight - 100 && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ };
+
+ const ref = scrollRef.current;
+ if (ref) ref.addEventListener('scroll', handleScroll);
+ return () => { if (ref) ref.removeEventListener('scroll', handleScroll); };
+ }, [fetchNextPage, hasNextPage, isFetchingNextPage]);
+
+ return { scrollRef };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/utils/useTagManagement.ts b/Week8/pocheonLim/mission2/src/hooks/utils/useTagManagement.ts
new file mode 100644
index 00000000..4d721902
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/utils/useTagManagement.ts
@@ -0,0 +1,33 @@
+import { useState } from 'react';
+
+export const useTagManagement = () => {
+ const [tagInput, setTagInput] = useState('');
+ const [tags, setTags] = useState([]);
+
+ const handleAddTag = () => {
+ if (tagInput.trim() && !tags.includes(tagInput.trim())) {
+ setTags([...tags, tagInput.trim()]);
+ setTagInput('');
+ }
+ };
+
+ const handleRemoveTag = (tag: string) => {
+ setTags(tags.filter(t => t !== tag));
+ };
+
+ const handleTagKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && tagInput.trim()) {
+ e.preventDefault();
+ handleAddTag();
+ }
+ };
+
+ return {
+ tagInput,
+ setTagInput,
+ tags,
+ handleAddTag,
+ handleRemoveTag,
+ handleTagKeyDown
+ };
+};
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/hooks/utils/useThrottle.ts b/Week8/pocheonLim/mission2/src/hooks/utils/useThrottle.ts
new file mode 100644
index 00000000..3dc497c7
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/hooks/utils/useThrottle.ts
@@ -0,0 +1,24 @@
+import { useEffect, useRef, useState } from "react";
+
+function useThrottle(value: T, delay: number = 10000): T {
+ const [throttledValue, setThrottledValue] = useState(value);
+
+ const lastExecuted = useRef(Date.now());
+
+ useEffect(() => {
+ if (Date.now() >= lastExecuted.current + delay) {
+ lastExecuted.current = Date.now();
+ setThrottledValue(value);
+ } else {
+ const timerId = setTimeout(() => {
+ lastExecuted.current = Date.now();
+ setThrottledValue(value);
+ }, delay);
+ return () => clearTimeout(timerId);
+ }
+ }, [value, delay]);
+
+ return throttledValue;
+}
+
+export default useThrottle;
diff --git a/Week8/pocheonLim/mission2/src/index.css b/Week8/pocheonLim/mission2/src/index.css
new file mode 100644
index 00000000..a461c505
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/index.css
@@ -0,0 +1 @@
+@import "tailwindcss";
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/layouts/HomeLayout.tsx b/Week8/pocheonLim/mission2/src/layouts/HomeLayout.tsx
new file mode 100644
index 00000000..fc8f1af3
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/layouts/HomeLayout.tsx
@@ -0,0 +1,54 @@
+import { Outlet } from 'react-router-dom';
+import React, { useState, useEffect, useRef } from 'react';
+import Header from '../components/layout/Header';
+import Sidebar from '../components/layout/Sidebar';
+import FabButton from '../components/FabButton';
+
+const SIDEBAR_WIDTH = 240; // w-60 = 240px
+const BREAKPOINT = 768;
+
+const HomeLayout = () => {
+ const [sidebarOpen, setSidebarOpen] = useState(window.innerWidth >= BREAKPOINT);
+ const prevIsDesktop = useRef(window.innerWidth >= BREAKPOINT);
+
+ useEffect(() => {
+ const handleResize = () => {
+ const isDesktop = window.innerWidth >= BREAKPOINT;
+
+ if (!prevIsDesktop.current && isDesktop) {
+ setSidebarOpen(true);
+ }
+
+ if (prevIsDesktop.current && !isDesktop) {
+ setSidebarOpen(false);
+ }
+ prevIsDesktop.current = isDesktop;
+ };
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ // 햄버거 버튼: 언제든 토글
+ const handleHamburgerClick = () => setSidebarOpen((prev) => !prev);
+
+ const marginLeft = sidebarOpen ? SIDEBAR_WIDTH : 0;
+
+ return (
+
+
setSidebarOpen(false)} />
+
+
+ {/* Main Content */}
+
+
+
+
+
+
+ );
+};
+
+export default HomeLayout;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/main.tsx b/Week8/pocheonLim/mission2/src/main.tsx
new file mode 100644
index 00000000..bef5202a
--- /dev/null
+++ b/Week8/pocheonLim/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/pocheonLim/mission2/src/pages/GoogleLoginRidirectPage.tsx b/Week8/pocheonLim/mission2/src/pages/GoogleLoginRidirectPage.tsx
new file mode 100644
index 00000000..ac2fb4af
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/pages/GoogleLoginRidirectPage.tsx
@@ -0,0 +1,24 @@
+import { useEffect } from 'react';
+import useLocalStorage from '../hooks/auth/useLocalStorage';
+
+const GoogleLoginRidirectPage = () => {
+ const [, setAccessToken] = useLocalStorage('accessToken', '');
+ const [, setRefreshToken] = useLocalStorage('refreshToken', '');
+
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const accessToken = urlParams.get('accessToken');
+ const refreshToken = urlParams.get('refreshToken');
+
+ if (accessToken && refreshToken) {
+ setAccessToken(accessToken);
+ setRefreshToken(refreshToken);
+ window.location.href = '/mypage';
+ }
+ }, [setAccessToken, setRefreshToken]);
+
+ return 구글 로그인 리다이렉트 처리 중...
;
+};
+
+export default GoogleLoginRidirectPage;
+
diff --git a/Week8/pocheonLim/mission2/src/pages/LoginPage.tsx b/Week8/pocheonLim/mission2/src/pages/LoginPage.tsx
new file mode 100644
index 00000000..a70b9a39
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/pages/LoginPage.tsx
@@ -0,0 +1,84 @@
+import { Link } from 'react-router-dom';
+import { useLoginForm } from '../hooks/auth/useLoginForm';
+
+const LoginPage = () => {
+ const {
+ emailRegister,
+ passwordRegister,
+ handleSubmit,
+ onSubmit,
+ errors,
+ isValid,
+ } = useLoginForm();
+
+ const handleGoogleLogin = () => {
+ window.location.href = import.meta.env.VITE_SERVER_API_URL + '/v1/auth/google/login';
+ };
+
+ return (
+
+
+
+
+ {'<'}
+
+
로그인
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LoginPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/pages/LpDetailPage.tsx b/Week8/pocheonLim/mission2/src/pages/LpDetailPage.tsx
new file mode 100644
index 00000000..c04b8f98
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/pages/LpDetailPage.tsx
@@ -0,0 +1,207 @@
+import { useParams, useNavigate } from 'react-router-dom';
+import { useRef } from 'react';
+import { useLpDetail } from '../hooks/lp/queries/useLpDetail';
+import { useUpdateLp } from '../hooks/lp/mutations/useUpdateLp';
+import { useDeleteLp } from '../hooks/lp/mutations/useDeleteLp';
+import { useAuth } from '../contexts/AuthContext';
+import { useLpEdit } from '../hooks/lp/mutations/useLpEdit';
+import { useCommentManagement } from '../hooks/comment/useCommentManagement';
+import { useLikeManagement } from '../hooks/like/useLikeManagement';
+import { useInfiniteScroll } from '../hooks/utils/useInfiniteScroll';
+import { useCommentSort } from '../hooks/comment/useCommentSort';
+import { LpHeader } from '../components/lp/LpHeader';
+import { LpContent } from '../components/lp/LpContent';
+import { CommentSection } from '../components/comment/CommentSection';
+
+const LpDetailPage = () => {
+ const { lpId } = useParams();
+ const navigate = useNavigate();
+ const scrollRef = useRef(null) as React.RefObject;
+ const COMMENT_LIMIT = 5;
+
+ const { data, isLoading, isError } = useLpDetail(lpId);
+ const { user } = useAuth();
+
+ const { commentOrder, handleSortChange } = useCommentSort();
+
+ const updateLpMutation = useUpdateLp(lpId!);
+ const deleteLpMutation = useDeleteLp(lpId!, navigate);
+
+ const {
+ isEditing,
+ setIsEditing,
+ editTitle,
+ setEditTitle,
+ editContent,
+ setEditContent,
+ editThumbnail,
+ editTagInput,
+ setEditTagInput,
+ editTags,
+ isUploading,
+ uploadError,
+ handleAddTag,
+ handleRemoveTag,
+ handleTagKeyDown,
+ handleImageUploadChange,
+ resetEditForm
+ } = useLpEdit(data);
+
+ const {
+ commentInput,
+ setCommentInput,
+ commentError,
+ editingCommentId,
+ editedContent,
+ setEditedContent,
+ showMenuId,
+ setShowMenuId,
+ commentData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isCommentLoading,
+ createCommentMutation,
+ updateCommentMutation,
+ handleCommentSubmit,
+ handleCommentEdit,
+ handleCommentDelete,
+ handleCommentUpdate,
+ handleCommentEditCancel,
+ } = useCommentManagement(lpId!, commentOrder);
+
+ useInfiniteScroll({
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage
+ });
+
+ const {
+ isLiked,
+ handleLikeClick,
+ addLikeMutation,
+ removeLikeMutation,
+ } = useLikeManagement(lpId!, commentOrder, data?.likes);
+
+ if (isLoading) return 로딩 중...
;
+ if (isError) {
+ console.error('에러 상세:', isError);
+ return LP 정보를 불러올 수 없습니다.
;
+ }
+ if (!data) return 데이터가 없습니다.
;
+
+ // 댓글 합치기
+ const comments = commentData?.pages.flatMap(page => page.data) ?? [];
+
+ // 본인이 작성한 LP인지 확인
+ const isMyLp = user?.id === data.authorId;
+
+ return (
+
+
+
{
+ if (window.confirm('정말로 이 LP를 삭제하시겠습니까?')) {
+ deleteLpMutation.mutate();
+ }
+ }}
+ />
+
+
+
+ {isMyLp && isEditing && (
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LpDetailPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/pages/LpListPage.tsx b/Week8/pocheonLim/mission2/src/pages/LpListPage.tsx
new file mode 100644
index 00000000..dcfaf95d
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/pages/LpListPage.tsx
@@ -0,0 +1,128 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import { Lp } from '../types/lp';
+import SkeletonLpCard from '../components/skeleton/SkeletonLpCard';
+import { useLpList, LIMIT } from '../hooks/lp/queries/useLpList';
+import useDebounce from '../hooks/utils/useDebounce';
+import useThrottle from '../hooks/utils/useThrottle';
+
+const LpListPage: React.FC = () => {
+ const [order, setOrder] = useState<'asc' | 'desc'>('asc');
+ const [search, setSearch] = useState('');
+ const debouncedSearch = useDebounce(search, 3000);
+ const navigate = useNavigate();
+ const { user } = useAuth();
+
+ const {
+ data,
+ isError,
+ error,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = useLpList(order, debouncedSearch);
+
+ // fetchNextPage 호출 여부를 throttle된 값으로 관리
+ const [shouldFetch, setShouldFetch] = useState(false);
+ const throttledShouldFetch = useThrottle(shouldFetch, 1000); // 1초에 한 번만 true로 바뀜
+
+ useEffect(() => {
+ if (throttledShouldFetch) {
+ fetchNextPage();
+ setShouldFetch(false);
+ }
+ }, [throttledShouldFetch, fetchNextPage]);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ if (
+ window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 100 &&
+ hasNextPage &&
+ !isFetchingNextPage
+ ) {
+ setShouldFetch(true);
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, [hasNextPage, isFetchingNextPage]);
+
+ const handleLpClick = (lpId: number) => {
+ if (!user) {
+ if (window.confirm('로그인이 필요한 서비스입니다. 로그인을 해주세요!')) {
+ navigate('/login');
+ }
+ return;
+ }
+ navigate(`/lp/${lpId}`);
+ };
+ if (isError) return 에러 발생: {error?.message}
;
+
+ const lps = data?.pages.flatMap(page => page.data) ?? [];
+
+ return (
+
+
+
setSearch(e.target.value)}
+ placeholder="LP 제목으로 검색..."
+ className="flex-1 max-w-xs px-4 py-2 rounded bg-gray-800 text-white focus:outline-none focus:ring-2 focus:ring-pink-500 placeholder-gray-400"
+ />
+
+
+
+
+
+
+ {lps.length === 0 &&
LP가 없습니다.
}
+ {lps.map((lp: Lp) => (
+
handleLpClick(lp.id)}
+ className="relative cursor-pointer bg-[#232b3b] rounded shadow flex flex-col items-center overflow-hidden transition-transform duration-200 hover:scale-105 group"
+ >
+

+ {/* Hover Overlay */}
+
+ {/* 하단 정보 영역 */}
+
+
+
{lp.title}
+
{new Date(lp.createdAt).toLocaleDateString()}
+
+
+ ♥
+ {lp.likes?.length ?? 0}
+
+
+
+ ))}
+
+ {isFetchingNextPage &&
+ Array.from({ length: LIMIT }).map((_, idx) => (
+
+ ))}
+
+
+ );
+};
+
+export default LpListPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/pages/MyPage.tsx b/Week8/pocheonLim/mission2/src/pages/MyPage.tsx
new file mode 100644
index 00000000..42bb2d86
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/pages/MyPage.tsx
@@ -0,0 +1,196 @@
+import { useAuth } from '../contexts/AuthContext';
+import { useNavigate } from 'react-router-dom';
+import { MdSettings } from 'react-icons/md';
+import { useProfileMutation } from '../hooks/profile/useProfileMutation';
+import { useProfileEdit } from '../hooks/profile/useProfileEdit';
+import { useLpSort } from '../hooks/lp/queries/useLpSort';
+
+const MyPage = () => {
+ const { user, logout, updateUser } = useAuth();
+ const navigate = useNavigate();
+
+ const {
+ isEditing,
+ setIsEditing,
+ editedName,
+ setEditedName,
+ editedBio,
+ setEditedBio,
+ previewUrl,
+ setPreviewUrl,
+ handleImageChange,
+ resetEditForm
+ } = useProfileEdit(user);
+
+ const {
+ order,
+ setOrder,
+ data,
+ isLoading,
+ isError,
+ error
+ } = useLpSort();
+
+ const updateProfileMutation = useProfileMutation(user, updateUser);
+
+ const handleLogout = () => {
+ logout();
+ navigate('/');
+ };
+
+ const handleSubmit = () => {
+ if (!editedName.trim()) {
+ alert('닉네임은 필수입니다.');
+ return;
+ }
+
+ const data: { name: string; bio?: string | null; avatar?: string } = {
+ name: editedName,
+ bio: editedBio.trim() || null,
+ };
+
+ if (previewUrl) {
+ data.avatar = previewUrl;
+ }
+
+ updateProfileMutation.mutate(data);
+ setIsEditing(false);
+ setPreviewUrl(null);
+ };
+
+ if (!user) {
+ return 사용자 정보를 불러오는 중...
;
+ }
+
+ const lps = data?.data.data ?? [];
+
+ const handleLpClick = (lpId: number) => {
+ navigate(`/lp/${lpId}`);
+ };
+
+ return (
+
+
마이페이지
+
+
+

isEditing && document.getElementById('avatarInput')?.click()}
+ />
+ {isEditing && (
+
+ )}
+
+
+ {isEditing ? (
+
+ ) : (
+ <>
+
{user.name}
+ {user.bio &&
{user.bio}
}
+
{user.email}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+ {isLoading &&
로딩 중...
}
+ {isError &&
에러 발생: {error instanceof Error ? error.message : '에러'}
}
+
+ {lps.length === 0 &&
LP가 없습니다.
}
+ {lps.map(lp => (
+
handleLpClick(lp.id)}
+ className="relative cursor-pointer bg-[#232b3b] rounded shadow flex flex-col items-center overflow-hidden transition-transform duration-200 hover:scale-105 group"
+ >
+

+ {/* Hover Overlay */}
+
+ {/* 하단 정보 영역 */}
+
+
+
{lp.title}
+
{new Date(lp.createdAt).toLocaleDateString()}
+
+
+ ♥
+ {lp.likes?.length ?? 0}
+
+
+
+ ))}
+
+
+ );
+};
+
+export default MyPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/pages/SignupPage.tsx b/Week8/pocheonLim/mission2/src/pages/SignupPage.tsx
new file mode 100644
index 00000000..4cc52e87
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/pages/SignupPage.tsx
@@ -0,0 +1,102 @@
+import { Link } from 'react-router-dom';
+import { SignupProvider } from '../contexts/SignupContext';
+import { useSignup } from '../contexts/SignupContext';
+import { useEffect, useRef } from 'react';
+import { useLocation } from 'react-router-dom';
+import EmailStep from '../components/signup/EmailStep';
+import PasswordStep from '../components/signup/PasswordStep';
+import NicknameStep from '../components/signup/NicknameStep';
+import CompletionStep from '../components/signup/CompletionStep';
+
+const SignupContent = () => {
+ const { step, setStep } = useSignup();
+ const location = useLocation();
+ const isFirstVisit = useRef(true);
+
+ useEffect(() => {
+ if (location.pathname === '/signup' && isFirstVisit.current) {
+ setStep(1); // 딱 첫 진입 시에만 초기화
+ isFirstVisit.current = false;
+ }
+ }, [location.pathname]);
+
+ const handleBack = () => {
+ if (step > 1) {
+ setStep(step - 1);
+ }
+ };
+
+ const renderStep = () => {
+ switch (step) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const handleGoogleLogin = () => {
+ window.location.href = import.meta.env.VITE_API_URL + '/v1/auth/google/login';
+ };
+
+ return (
+
+
+
+ {step === 1 ? (
+
+ {'<'}
+
+ ) : (
+
+ )}
+
회원가입
+
+
+ {step === 1 && (
+ <>
+
+
+
+ >
+ )}
+
+ {renderStep()}
+
+
+ );
+};
+
+const SignupPage = () => {
+ return (
+
+
+
+ );
+};
+
+export default SignupPage;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/schemas/login.schema.ts b/Week8/pocheonLim/mission2/src/schemas/login.schema.ts
new file mode 100644
index 00000000..7073e402
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/schemas/login.schema.ts
@@ -0,0 +1,4 @@
+export interface LoginFormData {
+ email: string;
+ password: string;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/schemas/signup.schema.ts b/Week8/pocheonLim/mission2/src/schemas/signup.schema.ts
new file mode 100644
index 00000000..641f0fd4
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/schemas/signup.schema.ts
@@ -0,0 +1,33 @@
+import { z } from 'zod';
+
+export const emailSchema = z.object({
+ email: z
+ .string()
+ .min(1, '이메일을 입력해주세요.')
+ .email('올바른 이메일 형식이 아닙니다.'),
+});
+
+export const passwordSchema = z.object({
+ password: z
+ .string()
+ .min(8, '비밀번호는 8자 이상이어야 합니다.')
+ .regex(/[a-z]/, '소문자를 포함해야 합니다.')
+ .regex(/[A-Z]/, '대문자를 포함해야 합니다.')
+ .regex(/[0-9]/, '숫자를 포함해야 합니다.')
+ .regex(/[^a-zA-Z0-9]/, '특수문자를 포함해야 합니다.'),
+ passwordConfirm: z.string(),
+}).refine((data) => data.password === data.passwordConfirm, {
+ message: '비밀번호가 일치하지 않습니다.',
+ path: ['passwordConfirm'],
+});
+
+export const nicknameSchema = z.object({
+ nickname: z
+ .string()
+ .min(2, '닉네임은 2자 이상이어야 합니다.')
+ .max(10, '닉네임은 10자 이하여야 합니다.'),
+});
+
+export type EmailFormData = z.infer;
+export type PasswordFormData = z.infer;
+export type NicknameFormData = z.infer;
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/types/auth.ts b/Week8/pocheonLim/mission2/src/types/auth.ts
new file mode 100644
index 00000000..87e3f0ba
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/types/auth.ts
@@ -0,0 +1,56 @@
+// 로그인 요청
+export interface LoginRequest {
+ email: string;
+ password: string;
+}
+
+// 로그인 응답
+export interface LoginResponse {
+ status: boolean;
+ message: string;
+ statusCode: number;
+ data: {
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+ }
+}
+
+// 회원가입 요청
+export interface SignupRequest {
+ name: string;
+ email: string;
+ password: string;
+ bio?: string;
+ avatar?: string;
+}
+
+// 회원가입 응답
+export interface SignupResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+}
+
+// 토큰 재발급 응답
+export interface RefreshTokenResponse {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: {
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+ }
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/types/comment.ts b/Week8/pocheonLim/mission2/src/types/comment.ts
new file mode 100644
index 00000000..7f0e77de
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/types/comment.ts
@@ -0,0 +1,17 @@
+export interface Comment {
+ id: number;
+ content: string;
+ lpId: number;
+ authorId: number;
+ author: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+ createdAt: string;
+ updatedAt: string;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/types/context.ts b/Week8/pocheonLim/mission2/src/types/context.ts
new file mode 100644
index 00000000..f328c782
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/types/context.ts
@@ -0,0 +1,8 @@
+import { User } from './user';
+
+export interface AuthContextType {
+ isLoggedIn: boolean;
+ user: User | null;
+ login: (accessToken: string, refreshToken: string) => void;
+ logout: () => void;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/types/like.ts b/Week8/pocheonLim/mission2/src/types/like.ts
new file mode 100644
index 00000000..be9e7ceb
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/types/like.ts
@@ -0,0 +1,5 @@
+export interface Like {
+ id: number;
+ userId: number;
+ lpId: number;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/types/lp.ts b/Week8/pocheonLim/mission2/src/types/lp.ts
new file mode 100644
index 00000000..27cd5f78
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/types/lp.ts
@@ -0,0 +1,30 @@
+import { Like } from './like';
+
+export interface Lp {
+ id: number;
+ title: string;
+ content: string;
+ thumbnail: string;
+ published: boolean;
+ authorId: number;
+ createdAt: string;
+ updatedAt: string;
+ likes: Like[];
+}
+
+export interface LpDetail extends Lp {
+ author: {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+ tags: {
+ id: number;
+ name: string;
+ }[];
+ audioUrl?: string;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/types/user.ts b/Week8/pocheonLim/mission2/src/types/user.ts
new file mode 100644
index 00000000..761f65f8
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/types/user.ts
@@ -0,0 +1,18 @@
+// 사용자 정보
+export interface User {
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// 사용자 정보 응답
+export interface UserInfoResponse {
+ status: boolean;
+ message: string;
+ statusCode: number;
+ data: User;
+}
\ No newline at end of file
diff --git a/Week8/pocheonLim/mission2/src/vite-env.d.ts b/Week8/pocheonLim/mission2/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/Week8/pocheonLim/mission2/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/Week8/pocheonLim/mission2/tsconfig.app.json b/Week8/pocheonLim/mission2/tsconfig.app.json
new file mode 100644
index 00000000..358ca9ba
--- /dev/null
+++ b/Week8/pocheonLim/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/pocheonLim/mission2/tsconfig.json b/Week8/pocheonLim/mission2/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/Week8/pocheonLim/mission2/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/Week8/pocheonLim/mission2/tsconfig.node.json b/Week8/pocheonLim/mission2/tsconfig.node.json
new file mode 100644
index 00000000..db0becc8
--- /dev/null
+++ b/Week8/pocheonLim/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/pocheonLim/mission2/vite.config.ts b/Week8/pocheonLim/mission2/vite.config.ts
new file mode 100644
index 00000000..4e43a26a
--- /dev/null
+++ b/Week8/pocheonLim/mission2/vite.config.ts
@@ -0,0 +1,17 @@
+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()],
+ server: {
+ proxy: {
+ '/v1': {
+ target: 'http://localhost:8000',
+ changeOrigin: true,
+ secure: false,
+ },
+ },
+ },
+})