diff --git a/UMC-8th-BE b/UMC-8th-BE
new file mode 160000
index 00000000..9edf36af
--- /dev/null
+++ b/UMC-8th-BE
@@ -0,0 +1 @@
+Subproject commit 9edf36af0fe6f3faee5a60b06cec0006564ffa12
diff --git a/UMC-8th-BE_old b/UMC-8th-BE_old
new file mode 160000
index 00000000..9edf36af
--- /dev/null
+++ b/UMC-8th-BE_old
@@ -0,0 +1 @@
+Subproject commit 9edf36af0fe6f3faee5a60b06cec0006564ffa12
diff --git a/week4/m2/UMC-8th-BE b/week4/m2/UMC-8th-BE
new file mode 160000
index 00000000..9edf36af
--- /dev/null
+++ b/week4/m2/UMC-8th-BE
@@ -0,0 +1 @@
+Subproject commit 9edf36af0fe6f3faee5a60b06cec0006564ffa12
diff --git a/week7/m1/package.json b/week7/m1/package.json
new file mode 100644
index 00000000..0fab3282
--- /dev/null
+++ b/week7/m1/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "m1",
+ "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.2.2",
+ "@tailwindcss/vite": "^4.2.4",
+ "@tanstack/react-query": "^5.100.9",
+ "@tanstack/react-query-devtools": "^5.100.9",
+ "axios": "^1.16.0",
+ "lucide-react": "^1.14.0",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
+ "react-hook-form": "^7.75.0",
+ "react-intersection-observer": "^10.0.3",
+ "react-router": "^7.15.0",
+ "react-router-dom": "^7.15.0",
+ "tailwindcss": "^4.2.4",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.12.2",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.2.1",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.5.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.58.2",
+ "vite": "^8.0.10"
+ }
+}
diff --git a/week7/m1/pnpm-lock.yaml b/week7/m1/pnpm-lock.yaml
new file mode 100644
index 00000000..b446aa51
--- /dev/null
+++ b/week7/m1/pnpm-lock.yaml
@@ -0,0 +1,2272 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@hookform/resolvers':
+ specifier: ^5.2.2
+ version: 5.2.2(react-hook-form@7.75.0(react@19.2.5))
+ '@tailwindcss/vite':
+ specifier: ^4.2.4
+ version: 4.2.4(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0))
+ '@tanstack/react-query':
+ specifier: ^5.100.9
+ version: 5.100.9(react@19.2.5)
+ '@tanstack/react-query-devtools':
+ specifier: ^5.100.9
+ version: 5.100.9(@tanstack/react-query@5.100.9(react@19.2.5))(react@19.2.5)
+ axios:
+ specifier: ^1.16.0
+ version: 1.16.0
+ lucide-react:
+ specifier: ^1.14.0
+ version: 1.14.0(react@19.2.5)
+ react:
+ specifier: ^19.2.5
+ version: 19.2.5
+ react-dom:
+ specifier: ^19.2.5
+ version: 19.2.5(react@19.2.5)
+ react-hook-form:
+ specifier: ^7.75.0
+ version: 7.75.0(react@19.2.5)
+ react-intersection-observer:
+ specifier: ^10.0.3
+ version: 10.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react-router:
+ specifier: ^7.15.0
+ version: 7.15.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react-router-dom:
+ specifier: ^7.15.0
+ version: 7.15.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ tailwindcss:
+ specifier: ^4.2.4
+ version: 4.2.4
+ zod:
+ specifier: ^4.4.3
+ version: 4.4.3
+ devDependencies:
+ '@eslint/js':
+ specifier: ^10.0.1
+ version: 10.0.1(eslint@10.3.0(jiti@2.7.0))
+ '@types/node':
+ specifier: ^24.12.2
+ version: 24.12.2
+ '@types/react':
+ specifier: ^19.2.14
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.14)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.1
+ version: 6.0.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0))
+ eslint:
+ specifier: ^10.2.1
+ version: 10.3.0(jiti@2.7.0)
+ eslint-plugin-react-hooks:
+ specifier: ^7.1.1
+ version: 7.1.1(eslint@10.3.0(jiti@2.7.0))
+ eslint-plugin-react-refresh:
+ specifier: ^0.5.2
+ version: 0.5.2(eslint@10.3.0(jiti@2.7.0))
+ globals:
+ specifier: ^17.5.0
+ version: 17.6.0
+ typescript:
+ specifier: ~6.0.2
+ version: 6.0.3
+ typescript-eslint:
+ specifier: ^8.58.2
+ version: 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)
+ vite:
+ specifier: ^8.0.10
+ version: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)
+
+packages:
+
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.3':
+ resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.2':
+ resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.3':
+ resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
+
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
+
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
+
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ 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.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.23.5':
+ resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/config-helpers@0.5.5':
+ resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/core@1.2.1':
+ resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/js@10.0.1':
+ resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ peerDependencies:
+ eslint: ^10.0.0
+ peerDependenciesMeta:
+ eslint:
+ optional: true
+
+ '@eslint/object-schema@3.0.5':
+ resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/plugin-kit@0.7.1':
+ resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@hookform/resolvers@5.2.2':
+ resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
+ peerDependencies:
+ react-hook-form: ^7.55.0
+
+ '@humanfs/core@0.19.2':
+ resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.8':
+ resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/types@0.15.0':
+ resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/wasm-runtime@1.1.4':
+ resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.127.0':
+ resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==}
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.17':
+ resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.17':
+ resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.17':
+ resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.17':
+ resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
+ resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
+ resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
+ resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
+ resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
+ resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
+ resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
+ resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
+ resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
+ resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
+ resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
+ resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.0-rc.17':
+ resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==}
+
+ '@rolldown/pluginutils@1.0.0-rc.7':
+ resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
+ '@tailwindcss/node@4.2.4':
+ resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==}
+
+ '@tailwindcss/oxide-android-arm64@4.2.4':
+ resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.4':
+ resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.2.4':
+ resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.4':
+ resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4':
+ resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.4':
+ resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.4':
+ resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.4':
+ resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.4':
+ resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.4':
+ resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==}
+ 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.2.4':
+ resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.4':
+ resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.2.4':
+ resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.2.4':
+ resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@tanstack/query-core@5.100.9':
+ resolution: {integrity: sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==}
+
+ '@tanstack/query-devtools@5.100.9':
+ resolution: {integrity: sha512-gqiptrTIhbK2PuCaPRHmWXfJG1NGYVFpAr0HqogEqiSBNB5xDz6fmesQt7w4WgMOqOQPnPHJ3ZDMuhDaXvNO8g==}
+
+ '@tanstack/react-query-devtools@5.100.9':
+ resolution: {integrity: sha512-mM3slaVGXJmz+pOLgXdANj75ikgQCyudyl3kmFvm6brI1JyVeY/+IeD17uDHIvZrD8hfoO2sdZ54RFsHdYAuhA==}
+ peerDependencies:
+ '@tanstack/react-query': ^5.100.9
+ react: ^18 || ^19
+
+ '@tanstack/react-query@5.100.9':
+ resolution: {integrity: sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@tybys/wasm-util@0.10.2':
+ resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
+
+ '@types/esrecurse@4.3.1':
+ resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/node@24.12.2':
+ resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.14':
+ resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
+
+ '@typescript-eslint/eslint-plugin@8.59.2':
+ resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.59.2
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/parser@8.59.2':
+ resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/project-service@8.59.2':
+ resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/scope-manager@8.59.2':
+ resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.59.2':
+ resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/type-utils@8.59.2':
+ resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/types@8.59.2':
+ resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.59.2':
+ resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/utils@8.59.2':
+ resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/visitor-keys@8.59.2':
+ resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react@6.0.1':
+ resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
+ babel-plugin-react-compiler: ^1.0.0
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@rolldown/plugin-babel':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ axios@1.16.0:
+ resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ baseline-browser-mapping@2.10.27:
+ resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ brace-expansion@5.0.5:
+ resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
+ engines: {node: 18 || 20 || >=22}
+
+ browserslist@4.28.2:
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
+ 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'}
+
+ caniuse-lite@1.0.30001792:
+ resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ 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.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.351:
+ resolution: {integrity: sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==}
+
+ enhanced-resolve@5.21.0:
+ resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==}
+ 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'}
+
+ 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@7.1.1:
+ resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0
+
+ eslint-plugin-react-refresh@0.5.2:
+ resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==}
+ peerDependencies:
+ eslint: ^9 || ^10
+
+ eslint-scope@9.1.2:
+ resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ 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@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@10.3.0:
+ resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@11.2.0:
+ resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ 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-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ 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'}
+
+ 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.4.2:
+ resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+
+ follow-redirects@1.16.0:
+ resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ 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@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@17.6.0:
+ resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==}
+ 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==}
+
+ 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.3:
+ resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
+ engines: {node: '>= 0.4'}
+
+ hermes-estree@0.25.1:
+ resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+ hermes-parser@0.25.1:
+ resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ 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'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ 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==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ 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-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ lucide-react@1.14.0:
+ resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ 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@10.2.5:
+ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
+ engines: {node: 18 || 20 || >=22}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.12:
+ resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
+ 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.38:
+ resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
+
+ 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'}
+
+ 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@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.14:
+ resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ proxy-from-env@2.1.0:
+ resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
+ engines: {node: '>=10'}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ react-dom@19.2.5:
+ resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
+ peerDependencies:
+ react: ^19.2.5
+
+ react-hook-form@7.75.0:
+ resolution: {integrity: sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-intersection-observer@10.0.3:
+ resolution: {integrity: sha512-luICLMbs0zxTO/70Zy7K5jOXkABPEVSAF8T3FdZUlctsrIaPLmx8TZe2SSA+CY2HGWfz2INyNTnp82pxNNsShA==}
+ peerDependencies:
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react-router-dom@7.15.0:
+ resolution: {integrity: sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.15.0:
+ resolution: {integrity: sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react@19.2.5:
+ resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
+ engines: {node: '>=0.10.0'}
+
+ rolldown@1.0.0-rc.17:
+ resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+
+ 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'}
+
+ tailwindcss@4.2.4:
+ resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==}
+
+ tapable@2.3.3:
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
+ engines: {node: '>=6'}
+
+ tinyglobby@0.2.16:
+ resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
+ engines: {node: '>=12.0.0'}
+
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.59.2:
+ resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite@8.0.10:
+ resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ 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'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod-validation-error@4.0.2:
+ resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+
+snapshots:
+
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.3': {}
+
+ '@babel/core@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.29.2
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.1':
+ dependencies:
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.3
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.29.2':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+
+ '@babel/parser@7.29.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@emnapi/core@1.10.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0(jiti@2.7.0))':
+ dependencies:
+ eslint: 10.3.0(jiti@2.7.0)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.23.5':
+ dependencies:
+ '@eslint/object-schema': 3.0.5
+ debug: 4.4.3
+ minimatch: 10.2.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.5.5':
+ dependencies:
+ '@eslint/core': 1.2.1
+
+ '@eslint/core@1.2.1':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/js@10.0.1(eslint@10.3.0(jiti@2.7.0))':
+ optionalDependencies:
+ eslint: 10.3.0(jiti@2.7.0)
+
+ '@eslint/object-schema@3.0.5': {}
+
+ '@eslint/plugin-kit@0.7.1':
+ dependencies:
+ '@eslint/core': 1.2.1
+ levn: 0.4.1
+
+ '@hookform/resolvers@5.2.2(react-hook-form@7.75.0(react@19.2.5))':
+ dependencies:
+ '@standard-schema/utils': 0.3.0
+ react-hook-form: 7.75.0(react@19.2.5)
+
+ '@humanfs/core@0.19.2':
+ dependencies:
+ '@humanfs/types': 0.15.0
+
+ '@humanfs/node@0.16.8':
+ dependencies:
+ '@humanfs/core': 0.19.2
+ '@humanfs/types': 0.15.0
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanfs/types@0.15.0': {}
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@tybys/wasm-util': 0.10.2
+ optional: true
+
+ '@oxc-project/types@0.127.0': {}
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-rc.17': {}
+
+ '@rolldown/pluginutils@1.0.0-rc.7': {}
+
+ '@standard-schema/utils@0.3.0': {}
+
+ '@tailwindcss/node@4.2.4':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.21.0
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.2.4
+
+ '@tailwindcss/oxide-android-arm64@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.4':
+ optional: true
+
+ '@tailwindcss/oxide@4.2.4':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.2.4
+ '@tailwindcss/oxide-darwin-arm64': 4.2.4
+ '@tailwindcss/oxide-darwin-x64': 4.2.4
+ '@tailwindcss/oxide-freebsd-x64': 4.2.4
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4
+ '@tailwindcss/oxide-linux-arm64-musl': 4.2.4
+ '@tailwindcss/oxide-linux-x64-gnu': 4.2.4
+ '@tailwindcss/oxide-linux-x64-musl': 4.2.4
+ '@tailwindcss/oxide-wasm32-wasi': 4.2.4
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4
+ '@tailwindcss/oxide-win32-x64-msvc': 4.2.4
+
+ '@tailwindcss/vite@4.2.4(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0))':
+ dependencies:
+ '@tailwindcss/node': 4.2.4
+ '@tailwindcss/oxide': 4.2.4
+ tailwindcss: 4.2.4
+ vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)
+
+ '@tanstack/query-core@5.100.9': {}
+
+ '@tanstack/query-devtools@5.100.9': {}
+
+ '@tanstack/react-query-devtools@5.100.9(@tanstack/react-query@5.100.9(react@19.2.5))(react@19.2.5)':
+ dependencies:
+ '@tanstack/query-devtools': 5.100.9
+ '@tanstack/react-query': 5.100.9(react@19.2.5)
+ react: 19.2.5
+
+ '@tanstack/react-query@5.100.9(react@19.2.5)':
+ dependencies:
+ '@tanstack/query-core': 5.100.9
+ react: 19.2.5
+
+ '@tybys/wasm-util@0.10.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/esrecurse@4.3.1': {}
+
+ '@types/estree@1.0.8': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/node@24.12.2':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/react-dom@19.2.3(@types/react@19.2.14)':
+ dependencies:
+ '@types/react': 19.2.14
+
+ '@types/react@19.2.14':
+ dependencies:
+ csstype: 3.2.3
+
+ '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.59.2
+ eslint: 10.3.0(jiti@2.7.0)
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.59.2
+ debug: 4.4.3
+ eslint: 10.3.0(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/types': 8.59.2
+ debug: 4.4.3
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.59.2':
+ dependencies:
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/visitor-keys': 8.59.2
+
+ '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)':
+ dependencies:
+ typescript: 6.0.3
+
+ '@typescript-eslint/type-utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)
+ debug: 4.4.3
+ eslint: 10.3.0(jiti@2.7.0)
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.59.2': {}
+
+ '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/visitor-keys': 8.59.2
+ debug: 4.4.3
+ minimatch: 10.2.5
+ semver: 7.7.4
+ tinyglobby: 0.2.16
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0))
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.59.2':
+ dependencies:
+ '@typescript-eslint/types': 8.59.2
+ eslint-visitor-keys: 5.0.1
+
+ '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.0-rc.7
+ vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)
+
+ acorn-jsx@5.3.2(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
+ acorn@8.16.0: {}
+
+ ajv@6.15.0:
+ 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
+
+ asynckit@0.4.0: {}
+
+ axios@1.16.0:
+ dependencies:
+ follow-redirects: 1.16.0
+ form-data: 4.0.5
+ proxy-from-env: 2.1.0
+ transitivePeerDependencies:
+ - debug
+
+ balanced-match@4.0.4: {}
+
+ baseline-browser-mapping@2.10.27: {}
+
+ brace-expansion@5.0.5:
+ dependencies:
+ balanced-match: 4.0.4
+
+ browserslist@4.28.2:
+ dependencies:
+ baseline-browser-mapping: 2.10.27
+ caniuse-lite: 1.0.30001792
+ electron-to-chromium: 1.5.351
+ node-releases: 2.0.38
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ caniuse-lite@1.0.30001792: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ convert-source-map@2.0.0: {}
+
+ cookie@1.1.1: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ delayed-stream@1.0.0: {}
+
+ detect-libc@2.1.2: {}
+
+ 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.351: {}
+
+ enhanced-resolve@5.21.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
+ 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.3
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@7.1.1(eslint@10.3.0(jiti@2.7.0)):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.3
+ eslint: 10.3.0(jiti@2.7.0)
+ hermes-parser: 0.25.1
+ zod: 4.4.3
+ zod-validation-error: 4.0.2(zod@4.4.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react-refresh@0.5.2(eslint@10.3.0(jiti@2.7.0)):
+ dependencies:
+ eslint: 10.3.0(jiti@2.7.0)
+
+ eslint-scope@9.1.2:
+ dependencies:
+ '@types/esrecurse': 4.3.1
+ '@types/estree': 1.0.8
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@10.3.0(jiti@2.7.0):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.23.5
+ '@eslint/config-helpers': 0.5.5
+ '@eslint/core': 1.2.1
+ '@eslint/plugin-kit': 0.7.1
+ '@humanfs/node': 0.16.8
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ ajv: 6.15.0
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 9.1.2
+ eslint-visitor-keys: 5.0.1
+ espree: 11.2.0
+ esquery: 1.7.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
+ minimatch: 10.2.5
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.7.0
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@11.2.0:
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ eslint-visitor-keys: 5.0.1
+
+ esquery@1.7.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-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.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.4.2
+ keyv: 4.5.4
+
+ flatted@3.4.2: {}
+
+ follow-redirects@1.16.0: {}
+
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.3
+ mime-types: 2.1.35
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.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.3
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@17.6.0: {}
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.3:
+ dependencies:
+ function-bind: 1.1.2
+
+ hermes-estree@0.25.1: {}
+
+ hermes-parser@0.25.1:
+ dependencies:
+ hermes-estree: 0.25.1
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ imurmurhash@0.1.4: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ isexe@2.0.0: {}
+
+ jiti@2.7.0: {}
+
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ 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-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ lucide-react@1.14.0(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ math-intrinsics@1.1.0: {}
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ minimatch@10.2.5:
+ dependencies:
+ brace-expansion: 5.0.5
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.12: {}
+
+ natural-compare@1.4.0: {}
+
+ node-releases@2.0.38: {}
+
+ 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
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.4: {}
+
+ postcss@8.5.14:
+ dependencies:
+ nanoid: 3.3.12
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ proxy-from-env@2.1.0: {}
+
+ punycode@2.3.1: {}
+
+ react-dom@19.2.5(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+ scheduler: 0.27.0
+
+ react-hook-form@7.75.0(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+
+ react-intersection-observer@10.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+ optionalDependencies:
+ react-dom: 19.2.5(react@19.2.5)
+
+ react-router-dom@7.15.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ dependencies:
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-router: 7.15.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+
+ react-router@7.15.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ dependencies:
+ cookie: 1.1.1
+ react: 19.2.5
+ set-cookie-parser: 2.7.2
+ optionalDependencies:
+ react-dom: 19.2.5(react@19.2.5)
+
+ react@19.2.5: {}
+
+ rolldown@1.0.0-rc.17:
+ dependencies:
+ '@oxc-project/types': 0.127.0
+ '@rolldown/pluginutils': 1.0.0-rc.17
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-rc.17
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.17
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.17
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.17
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.7.4: {}
+
+ set-cookie-parser@2.7.2: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ tailwindcss@4.2.4: {}
+
+ tapable@2.3.3: {}
+
+ tinyglobby@0.2.16:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ ts-api-utils@2.5.0(typescript@6.0.3):
+ dependencies:
+ typescript: 6.0.3
+
+ tslib@2.8.1:
+ optional: true
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@6.0.3: {}
+
+ undici-types@7.16.0: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
+ dependencies:
+ browserslist: 4.28.2
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.14
+ rolldown: 1.0.0-rc.17
+ tinyglobby: 0.2.16
+ optionalDependencies:
+ '@types/node': 24.12.2
+ fsevents: 2.3.3
+ jiti: 2.7.0
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ yallist@3.1.1: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod-validation-error@4.0.2(zod@4.4.3):
+ dependencies:
+ zod: 4.4.3
+
+ zod@4.4.3: {}
diff --git a/week7/m1/tsconfig.app.json b/week7/m1/tsconfig.app.json
new file mode 100644
index 00000000..7f42e5f7
--- /dev/null
+++ b/week7/m1/tsconfig.app.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/week7/m1/tsconfig.node.json b/week7/m1/tsconfig.node.json
new file mode 100644
index 00000000..d3c52ea6
--- /dev/null
+++ b/week7/m1/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "module": "esnext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/week8/m1/.gitignore b/week8/m1/.gitignore
new file mode 100644
index 00000000..92ef6c1d
--- /dev/null
+++ b/week8/m1/.gitignore
@@ -0,0 +1,36 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
+lerna-lock.json
+tsconfig.app.json
+tsconfig.jason
+tsconfig.node.json
+package.json
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+/package.json
+/pnpm-lock.yaml
+
+.env
\ No newline at end of file
diff --git a/week8/m1/README.md b/week8/m1/README.md
new file mode 100644
index 00000000..7dbf7ebf
--- /dev/null
+++ b/week8/m1/README.md
@@ -0,0 +1,73 @@
+# 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 [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## 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 defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // 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,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+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 defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/week8/m1/eslint.config.js b/week8/m1/eslint.config.js
new file mode 100644
index 00000000..ef614d25
--- /dev/null
+++ b/week8/m1/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/week8/m1/index.html b/week8/m1/index.html
new file mode 100644
index 00000000..2e9d48ac
--- /dev/null
+++ b/week8/m1/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ m1
+
+
+
+
+
+
diff --git a/week8/m1/public/favicon.svg b/week8/m1/public/favicon.svg
new file mode 100644
index 00000000..6893eb13
--- /dev/null
+++ b/week8/m1/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/week8/m1/public/icons.svg b/week8/m1/public/icons.svg
new file mode 100644
index 00000000..e9522193
--- /dev/null
+++ b/week8/m1/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/week8/m1/src/App.css b/week8/m1/src/App.css
new file mode 100644
index 00000000..e69de29b
diff --git a/week8/m1/src/App.tsx b/week8/m1/src/App.tsx
new file mode 100644
index 00000000..47ca9c76
--- /dev/null
+++ b/week8/m1/src/App.tsx
@@ -0,0 +1,64 @@
+import './App.css'
+import { createBrowserRouter, RouterProvider, type RouteObject } from 'react-router-dom'
+import Home from './pages/Home'
+import NotFoundErr from './pages/NotFoundErr'
+import Login from './pages/Login'
+import HomeLayout from './layouts/HomeLayout'
+import SignUp from './pages/SignUp'
+import MyPage from './pages/Mypage'
+import { AuthProvider } from './context/AuthContext'
+import PrivateLayout from './layouts/PrivateLayout'
+import GoogleLoginRedirect from './pages/GoogleLoginRedirect'
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import LpDetailPage from './pages/LpDetail'
+
+const publicRoutes:RouteObject[] = [
+ {
+ path: "/",
+ element: ,
+ errorElement: ,
+ children: [
+ {index: true, element: },
+ {path: 'login', element: },
+ {path: 'signup', element: },
+ {path: '/v1/auth/google/callback', element: }, //추가
+ {path: 'lp/:lpId', element: }, //추가
+ {path: '/v1/auth/google/callback', element: }, //구글 로그인 리다이렉트 페이지 추가
+ ]
+ },
+];
+
+const privateRoutes:RouteObject[] = [
+ {
+ path: "/",
+ element: ,
+ errorElement: ,
+ children: [
+ {path: 'my', element: },
+ ],
+ },
+];
+
+export const queryClient = new QueryClient();
+
+function App() {
+ const router = createBrowserRouter([
+ ...publicRoutes,
+ ...privateRoutes,
+ ]);
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+
+ )
+}
+
+export default App
diff --git a/week8/m1/src/apis/auth.ts b/week8/m1/src/apis/auth.ts
new file mode 100644
index 00000000..4afc96aa
--- /dev/null
+++ b/week8/m1/src/apis/auth.ts
@@ -0,0 +1,68 @@
+import axios from "axios";
+import type { ReqSignInDto,
+ ReqSignUpDto,
+ ResMyInfoDto,
+ ResSignInDto,
+ ResSignUpDto,
+ ReqUpdateMyInfoDto } from "../types/auth";
+import { useLocalStorage } from "../hooks/useLocalStorage";
+import { LOCAL_STORAGE_KEY } from "../constants/key";
+
+
+const getAuthHeader = () => {
+ try {
+ const token = JSON.parse(
+ localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) ?? "null"
+ );
+ return token ? { Authorization: `Bearer ${token}` } : {};
+ } catch {
+ return {};
+ }
+};
+
+export const postSignup = async (body: ReqSignUpDto):Promise => {
+ const { data } = await axios.post(`${import.meta.env.VITE_SERVER_API_URL}/v1/auth/signup`, body);
+ return data;
+}
+
+export const postSignin = async (body: ReqSignInDto):Promise => {
+ const { data } = await axios.post(`${import.meta.env.VITE_SERVER_API_URL}/v1/auth/signin`, body);
+ return data;
+}
+
+export const getMyInfo = async ():Promise => {
+ const { getItem } = useLocalStorage(LOCAL_STORAGE_KEY.ACCESS_TOKEN);
+ const { data } = await axios.get(`${import.meta.env.VITE_SERVER_API_URL}/v1/users/me`, {
+ headers: {
+ Authorization: `Bearer ${getItem()}`,
+ }
+ });
+ return data;
+}
+// 로그아웃 API
+export const postLogout = async () => {
+ const { data } = await axios.post(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/auth/signout`,
+ {},
+ { headers: getAuthHeader() }
+ );
+ return data;
+};
+
+// 내 정보 수정 API
+export const patchMyInfo = async (
+ body: ReqUpdateMyInfoDto
+): Promise => {
+ const { data } = await axios.patch(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/users`,
+ body,
+ { headers: getAuthHeader() }
+ );
+ return data;
+};
+
+export const deleteAccount = async () => {
+ await axios.delete(`${import.meta.env.VITE_SERVER_API_URL}/v1/users`, {
+ headers: getAuthHeader(),
+ });
+};
\ No newline at end of file
diff --git a/week8/m1/src/apis/lp.ts b/week8/m1/src/apis/lp.ts
new file mode 100644
index 00000000..40001ccd
--- /dev/null
+++ b/week8/m1/src/apis/lp.ts
@@ -0,0 +1,126 @@
+import axios from "axios"
+import type { PaginationDto } from "../types/common"
+import type { ResponseLpListDto, ReqCreateLpDto } from "../types/lp";
+import { LOCAL_STORAGE_KEY } from "../constants/key";
+
+const getAuthHeader = () => {
+ try {
+ const token = JSON.parse(
+ localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) ?? "null"
+ );
+ return token ? { Authorization: `Bearer ${token}` } : {};
+ } catch {
+ return {};
+ }
+};
+
+export const getLpList = async (paginationDto: PaginationDto): Promise => {
+ const { data } = await axios.get(`${import.meta.env.VITE_SERVER_API_URL}/v1/lps`, {
+ params: paginationDto,
+ });
+
+ return data;
+}; // LP 목록 조회 API
+
+export const postLp = async (body: ReqCreateLpDto) => {
+ const { data } = await axios.post(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps`,
+ body,
+ { headers: getAuthHeader() }
+ );
+ return data;
+}; // LP 생성 API 추가
+
+export const updateLp = async ({
+ lpId,
+ body,
+}: {
+ lpId: number;
+ body: Partial;
+}) => {
+ const { data } = await axios.patch(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps/${lpId}`,
+ body,
+ { headers: getAuthHeader() }
+ );
+ return data;
+}; // LP 수정 API 추가
+
+export const deleteLpApi = async (lpId: number) => {
+ await axios.delete(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps/${lpId}`,
+ { headers: getAuthHeader() }
+ );
+};// LP 삭제 API 추가
+
+export const toggleLike = async (lpId: number) => {
+ const { data } = await axios.post(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps/${lpId}/likes`,
+ {},
+ { headers: getAuthHeader() }
+ );
+ return data;
+}; // LP 좋아요 토글 API 추가
+
+export const postComment = async ({
+ lpId,
+ content,
+}: {
+ lpId: number;
+ content: string;
+}) => {
+ const { data } = await axios.post(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps/${lpId}/comments`,
+ { content },
+ { headers: getAuthHeader() }
+ );
+ return data;
+}; // 댓글 작성 API 추가
+
+export const updateComment = async ({
+ lpId,
+ commentId,
+ content,
+}: {
+ lpId: number;
+ commentId: number;
+ content: string;
+}) => {
+ const { data } = await axios.patch(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps/${lpId}/comments/${commentId}`,
+ { content },
+ { headers: getAuthHeader() }
+ );
+ return data;
+}; // 댓글 수정 API 추가
+
+export const deleteComment = async ({
+ lpId,
+ commentId,
+}: {
+ lpId: number;
+ commentId: number;
+}) => {
+ await axios.delete(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps/${lpId}/comments/${commentId}`,
+ { headers: getAuthHeader() }
+ );
+}; // 댓글 삭제 API 추가
+
+export const uploadImage = async (file: File) => {
+ const formData = new FormData();
+ formData.append("file", file);
+
+ const { data } = await axios.post(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/uploads`,
+ formData,
+ {
+ headers: {
+ ...getAuthHeader(),
+ "Content-Type": "multipart/form-data",
+ },
+ }
+ );
+
+ return data.data.imageUrl;
+}; // 이미지 업로드 API 추가
\ No newline at end of file
diff --git a/week8/m1/src/assets/hero.png b/week8/m1/src/assets/hero.png
new file mode 100644
index 00000000..02251f4b
Binary files /dev/null and b/week8/m1/src/assets/hero.png differ
diff --git a/week8/m1/src/assets/react.svg b/week8/m1/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/week8/m1/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/week8/m1/src/assets/vite.svg b/week8/m1/src/assets/vite.svg
new file mode 100644
index 00000000..5101b674
--- /dev/null
+++ b/week8/m1/src/assets/vite.svg
@@ -0,0 +1 @@
+
diff --git a/week8/m1/src/components/Footer.tsx b/week8/m1/src/components/Footer.tsx
new file mode 100644
index 00000000..5468da44
--- /dev/null
+++ b/week8/m1/src/components/Footer.tsx
@@ -0,0 +1,21 @@
+import { Link } from "react-router-dom"
+
+const Footer = () => {
+ return (
+
+ )
+}
+
+export default Footer
+// Footer 컴포넌트는 사이트의 하단에 위치하는 공통 요소로, 저작권 정보와 함께 개인정보 처리방침, 이용약관, 연락처 등의 링크를 포함합니다. 디자인은 간단하면서도 깔끔하게 유지하여 사용자에게 필요한 정보를 제공하는 역할을 합니다.
\ No newline at end of file
diff --git a/week8/m1/src/components/Sidebar.tsx b/week8/m1/src/components/Sidebar.tsx
new file mode 100644
index 00000000..466f5f98
--- /dev/null
+++ b/week8/m1/src/components/Sidebar.tsx
@@ -0,0 +1,211 @@
+import { useRef, useEffect, useState } from "react";
+import { useNavigate, useLocation } from "react-router-dom";
+import { useMutation } from "@tanstack/react-query";
+import { useAuth } from "../context/AuthContext";
+
+type ActiveNav = "search" | "mypage";
+
+const COLORS = {
+ bg: "#F4FFFC",
+ soft: "#E9FFFC",
+ main: "#0ECFD3",
+ dark: "#0BAEB3",
+ text: "#063B3D",
+ border: "#B8F3EE",
+ muted: "#5B8C8E",
+ faint: "#7AA6A8",
+};
+
+const NAV_ITEMS: { key: ActiveNav; icon: string; label: string; path: string }[] = [
+ { key: "search", icon: "🔍", label: "찾기", path: "/" },
+ { key: "mypage", icon: "👤", label: "마이페이지", path: "/my" },
+];
+
+interface SidebarProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export default function Sidebar({ isOpen, onClose }: SidebarProps) {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const sidebarRef = useRef(null);
+ const { deleteAccount } = useAuth();
+
+ const [showWithdrawModal, setShowWithdrawModal] = useState(false);
+
+ const activeKey: ActiveNav =
+ location.pathname.startsWith("/my") ? "mypage" : "search";
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => {
+ if (isOpen && sidebarRef.current && !sidebarRef.current.contains(e.target as Node)) {
+ onClose();
+ }
+ };
+
+ document.addEventListener("mousedown", handler);
+ return () => document.removeEventListener("mousedown", handler);
+ }, [isOpen, onClose]);
+
+ const withdrawMutation = useMutation({
+ mutationFn: deleteAccount,
+ onSuccess: () => {
+ onClose();
+ navigate("/login");
+ },
+ });
+
+ return (
+ <>
+
+
+
+
+ {showWithdrawModal && (
+
+
+
+
⚠️
+
+ 정말 탈퇴하시겠습니까?
+
+
+ 탈퇴 시 모든 데이터가 삭제되며 복구할 수 없습니다.
+
+
+
+ {withdrawMutation.isError && (
+
+ 탈퇴 처리 중 오류가 발생했습니다.
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
\ No newline at end of file
diff --git a/week8/m1/src/constants/key.ts b/week8/m1/src/constants/key.ts
new file mode 100644
index 00000000..36f080f2
--- /dev/null
+++ b/week8/m1/src/constants/key.ts
@@ -0,0 +1,18 @@
+export const LOCAL_STORAGE_KEY = {
+ 'ACCESS_TOKEN': 'accessToken',
+ 'REFRESH_TOKEN': 'refreshToken',
+ 'USER_ID': 'userId',
+}
+
+export const QUERY_KEY = {
+ lps: 'lps',
+}
+
+export const NAV_ITEMS = [
+ { icon: "💿", label: "전체 LP" },
+ { icon: "⭐", label: "즐겨찾기" },
+ { icon: "🕒", label: "최근 재생" },
+ { icon: "🎸", label: "장르별" },
+ { icon: "📅", label: "연도별" },
+ { icon: "🔍", label: "찾기" },
+];
\ No newline at end of file
diff --git a/week8/m1/src/context/AuthContext.tsx b/week8/m1/src/context/AuthContext.tsx
new file mode 100644
index 00000000..2b7290dc
--- /dev/null
+++ b/week8/m1/src/context/AuthContext.tsx
@@ -0,0 +1,112 @@
+import { postSignin } from "../apis/auth";
+import { LOCAL_STORAGE_KEY } from "../constants/key";
+import { useLocalStorage } from "../hooks/useLocalStorage";
+import { type ReqSignInDto } from "../types/auth";
+import { postLogout, deleteAccount as deleteAccountApi } from "../apis/auth";
+import { createContext, useState, useContext, type PropsWithChildren } from "react";
+
+interface AuthContextType {
+ isAuthenticated: boolean;
+ accessToken: string | null;
+ refreshToken: string | null;
+ userId: number | null;
+ login: (signInData: ReqSignInDto) => Promise;
+ logout: () => Promise;
+ deleteAccount: () => Promise;
+}
+
+export const AuthContext = createContext({
+ isAuthenticated: false,
+ accessToken: null,
+ refreshToken: null,
+ userId: null,
+ login: async () => {},
+ logout: async () => {},
+ deleteAccount: async () => {},
+});
+
+export const AuthProvider = ({ children }: PropsWithChildren) => {
+ const {
+ getItem: getAccessTokenInStorage,
+ setItem: setAccessTokenInStorage,
+ removeItem: removeAccessTokenInStorage,
+ } = useLocalStorage(LOCAL_STORAGE_KEY.ACCESS_TOKEN);
+ const {
+ getItem: getRefreshTokenInStorage,
+ setItem: setRefreshTokenInStorage,
+ removeItem: removeRefreshTokenInStorage,
+ } = useLocalStorage(LOCAL_STORAGE_KEY.REFRESH_TOKEN);
+ const {
+ getItem: getUserIdFromStorage,
+ setItem: setUserIdInStorage,
+ removeItem: removeUserIdInStorage,
+ } = useLocalStorage(LOCAL_STORAGE_KEY.USER_ID);
+
+ const [accessToken, setAccessToken] = useState(
+ getAccessTokenInStorage()
+ );
+ const [refreshToken, setRefreshToken] = useState(
+ getRefreshTokenInStorage()
+ );
+ const [userId, setUserId] = useState(
+ getUserIdFromStorage()
+ );
+
+ const login = async (signInData: ReqSignInDto) => {
+ const { data } = await postSignin(signInData);
+
+ if (data) {
+ setAccessTokenInStorage(data.accessToken);
+ setRefreshTokenInStorage(data.refreshToken);
+ setUserIdInStorage(data.id);
+
+ setAccessToken(data.accessToken);
+ setRefreshToken(data.refreshToken);
+ setUserId(data.id);
+ }
+ };
+
+ const logout = async () => {
+ try {
+ await postLogout();
+ } catch (err) {
+ console.error("Logout API failed:", err);
+ } finally {
+ removeAccessTokenInStorage();
+ removeRefreshTokenInStorage();
+ removeUserIdInStorage();
+
+ setAccessToken(null);
+ setRefreshToken(null);
+ setUserId(null);
+ }
+ };
+
+ const deleteAccount = async () => {
+ await deleteAccountApi();
+ removeAccessTokenInStorage();
+ removeRefreshTokenInStorage();
+ removeUserIdInStorage();
+
+ setAccessToken(null);
+ setRefreshToken(null);
+ setUserId(null);
+ };
+
+ return (
+
+ {children}
+
+ );
+
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error("useAuth must be used within an AuthProvider");
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/week8/m1/src/hooks/queries/useGetLpList.ts b/week8/m1/src/hooks/queries/useGetLpList.ts
new file mode 100644
index 00000000..6339a912
--- /dev/null
+++ b/week8/m1/src/hooks/queries/useGetLpList.ts
@@ -0,0 +1,25 @@
+import { useQuery } from "@tanstack/react-query";
+import type { PaginationDto } from "../../types/common";
+import { getLpList } from "../../apis/lp";
+import { QUERY_KEY } from "../../constants/key";
+
+function useGetLpList({cursor, search, order, limit}: PaginationDto) {
+ return useQuery({
+ queryKey:[QUERY_KEY.lps],
+ queryFn: () => getLpList({
+ cursor,
+ search,
+ order,
+ limit
+ }),
+ // 데이터가 신선하다고 간주하는 시간 ~ 해당 시간 동안에는 캐시된 데이터를 그대로 사용
+ staleTime: 1000 * 60 * 5, // ms단위이기 때문에 해당 시간은 5분 의미
+ // 사용되지 않는 상태인 쿼리 데이터가 캐시에 남아있는 시간
+ // staleTime이 지나고 데이터가 신선하지 않더라도 일정 시간 동안 메모리에 보관
+ gcTime: 1000 * 60 * 10, // 10분
+ // 조건에 따라 쿼리 실행 여부 제어
+ enabled: true
+ });
+}
+
+export default useGetLpList;
\ No newline at end of file
diff --git a/week8/m1/src/hooks/useDebounce.ts b/week8/m1/src/hooks/useDebounce.ts
new file mode 100644
index 00000000..928387ca
--- /dev/null
+++ b/week8/m1/src/hooks/useDebounce.ts
@@ -0,0 +1,17 @@
+import { useEffect, useState } from "react";
+
+export function useDebounce(value: T, delay: number): T {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const timer = window.setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ return () => {
+ window.clearTimeout(timer);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
diff --git a/week8/m1/src/hooks/useForm.ts b/week8/m1/src/hooks/useForm.ts
new file mode 100644
index 00000000..52a652c2
--- /dev/null
+++ b/week8/m1/src/hooks/useForm.ts
@@ -0,0 +1,72 @@
+/*
+useForm
+→ input 값 관리 + 에러 검증 도와주는 훅
+*/
+
+import { useEffect, useState, type ChangeEvent } from "react";
+
+/*
+useState → 입력값, 에러, touched 상태 저장
+useEffect → 입력값이 바뀔 때마다 검증 실행
+ChangeEvent → input 변경 이벤트 타입
+*/
+
+interface UseFormProps {
+ init_val: T; // input들의 초기값
+ validate: (values: T) => Record; // 입력값이 맞는지 검사하는 함수
+}
+
+function useForm({ init_val, validate }: UseFormProps) {
+ const [values, setValues] = useState(init_val); // 현재 input 값들 저장
+ const [touched, setTouched] = useState>({});
+ // 사용자가 해당 input을 한 번이라도 건드렸는지 저장
+
+ const [errors, setErrors] = useState>({});
+ // 각 input의 에러 메시지 저장
+
+ const handleChange = (name: keyof T, text: string) => {
+ setValues({
+ ...values, // 기존 값 유지
+ [name]: text, // 바뀐 input만 새 값으로 바꾸기
+ });
+ };
+ // 사용자가 input에 글자를 입력하면 그 input의 값을 values에 저장
+
+ const handleBlur = (name: keyof T) => {
+ setTouched({
+ ...touched, // 기존 touched 상태 유지
+ [name]: true, // 해당 input은 이제 touched 됐다고 표시
+ });
+ };
+
+ const getInputProps = (name: keyof T) => {
+ const value = values[name];
+
+ const onChange = (
+ e: ChangeEvent
+ ) => {
+ handleChange(name, e.target.value);
+ };
+
+ const onBlur = () => {
+ handleBlur(name);
+ };
+
+ return { value, onChange, onBlur };
+ };
+ // input 컴포넌트에 getInputProps("username") 이렇게 쓰면
+ // 그 input의 value, onChange, onBlur가 자동으로 연결되도록 해주는 함수
+
+ useEffect(() => {
+ const newErrors = validate(values);
+ setErrors(newErrors);
+ }, [values, validate]);
+ /*
+ values가 바뀔 때마다 validate 함수로 에러 검사
+ 검사 결과를 errors에 저장
+ */
+
+ return { values, errors, touched, getInputProps };
+}
+
+export default useForm;
\ No newline at end of file
diff --git a/week8/m1/src/hooks/useLocalStorage.ts b/week8/m1/src/hooks/useLocalStorage.ts
new file mode 100644
index 00000000..a93c3106
--- /dev/null
+++ b/week8/m1/src/hooks/useLocalStorage.ts
@@ -0,0 +1,42 @@
+/*
+useLocalStorage
+→ localStorage 저장/조회/삭제 쉽게 해주는 훅
+새로고침해도 남아 있음
+브라우저 꺼도 남아 있음
+로그인 토큰 저장할 때 자주 씀
+*/
+
+export const useLocalStorage = (key: string) => {
+ // key 이름을 받아서 해당하는 localStorage 조작하는 함수들 만들어줌
+
+ const setItem = (value: unknown) => {
+ try {
+ localStorage.setItem(key, JSON.stringify(value));
+ // localStorage는 문자열만 저장할 수 있어서 JSON.stringify로 감싸서 저장
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const getItem = () => {
+ try {
+ const item = localStorage.getItem(key);
+ return item ? JSON.parse(item) : null;
+ // 저장할 때 JSON.stringify로 감싸줬으니까 꺼낼 때는 JSON.parse로 다시 객체로 만들어주기
+ } catch (err) {
+ console.log(err);
+ return null;
+ }
+ };
+
+ const removeItem = () => {
+ try {
+ localStorage.removeItem(key);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+ // localStorage에서 해당 key 삭제
+
+ return { setItem, getItem, removeItem };
+};
\ No newline at end of file
diff --git a/week8/m1/src/imgs/google.png b/week8/m1/src/imgs/google.png
new file mode 100644
index 00000000..9035a67e
Binary files /dev/null and b/week8/m1/src/imgs/google.png differ
diff --git a/week8/m1/src/index.css b/week8/m1/src/index.css
new file mode 100644
index 00000000..a461c505
--- /dev/null
+++ b/week8/m1/src/index.css
@@ -0,0 +1 @@
+@import "tailwindcss";
\ No newline at end of file
diff --git a/week8/m1/src/layouts/HomeLayout.tsx b/week8/m1/src/layouts/HomeLayout.tsx
new file mode 100644
index 00000000..c30e5843
--- /dev/null
+++ b/week8/m1/src/layouts/HomeLayout.tsx
@@ -0,0 +1,101 @@
+import { Outlet, useNavigate } from "react-router-dom";
+import { useAuth } from "../context/AuthContext";
+import { getMyInfo } from "../apis/auth";
+import { useState, useEffect } from "react";
+import type { ResMyInfoDto } from "../types/auth";
+import Footer from "../components/Footer";
+import Sidebar from "../components/Sidebar";
+import { useMutation } from "@tanstack/react-query";
+// 로그인 여부에 따라 헤더에 로그인/회원가입 버튼 또는 사용자 이름과 로그아웃 버튼을 보여주는 레이아웃
+
+export default function HomeLayout() {
+ const navigate = useNavigate();
+ const { accessToken, logout } = useAuth();
+
+ const [data, setData] = useState({} as ResMyInfoDto);
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+
+ const logoutMutation = useMutation({
+ mutationFn: logout,
+ onSuccess: () => navigate("/"),
+ });
+
+ useEffect(() => {
+ if (!accessToken) return;
+ const myData = async () => {
+ try {
+ const response = await getMyInfo();
+ setData(response);
+ } catch {
+ // 토큰 만료 등으로 실패해도 레이아웃 유지
+ }
+ };
+ myData();
+ }, [accessToken]);
+
+ return (
+
+
+ {/* 헤더 */}
+
+
+ {/* 사이드바 */}
+
setSidebarOpen(false)} />
+
+ {/* 페이지 콘텐츠 */}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/week8/m1/src/layouts/PrivateLayout.tsx b/week8/m1/src/layouts/PrivateLayout.tsx
new file mode 100644
index 00000000..519b919c
--- /dev/null
+++ b/week8/m1/src/layouts/PrivateLayout.tsx
@@ -0,0 +1,56 @@
+import { Outlet, useNavigate, Navigate } from "react-router-dom";
+import { useAuth } from "../context/AuthContext";
+import { getMyInfo } from "../apis/auth";
+import { useState, useEffect } from "react";
+import type { ResMyInfoDto } from "../types/auth";
+
+const PrivateLayout = () => {
+ const navigate = useNavigate();
+ const {accessToken, logout} = useAuth();
+
+ const handleLogout = async () => {
+ await logout();
+ navigate("/");
+ }
+
+ const [data, setData] = useState({} as ResMyInfoDto);
+
+ useEffect(() => {
+ const myData = async () => {
+ const response = await getMyInfo();
+ console.log(response);
+ setData(response);
+ }
+
+ myData();
+ }, [accessToken]);
+
+ if (!accessToken) {
+ return ; // 뒤로 가기 했을 때 history에 로그인 페이지가 남지 않도록 replace 옵션 사용
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default PrivateLayout;
\ No newline at end of file
diff --git a/week8/m1/src/main.tsx b/week8/m1/src/main.tsx
new file mode 100644
index 00000000..bef5202a
--- /dev/null
+++ b/week8/m1/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/m1/src/pages/GoogleLoginRedirect.tsx b/week8/m1/src/pages/GoogleLoginRedirect.tsx
new file mode 100644
index 00000000..e98d57ef
--- /dev/null
+++ b/week8/m1/src/pages/GoogleLoginRedirect.tsx
@@ -0,0 +1,26 @@
+import { LOCAL_STORAGE_KEY } from "../constants/key";
+import { useLocalStorage } from "../hooks/useLocalStorage";
+import { useEffect } from "react";
+
+const GoogleLoginRedirect = () => {
+ const {setItem: setAccessToken} = useLocalStorage(LOCAL_STORAGE_KEY.ACCESS_TOKEN);
+ const {setItem: setRefreshToken} = useLocalStorage(LOCAL_STORAGE_KEY.REFRESH_TOKEN);
+
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const accessToken = urlParams.get("accessToken");
+ const refreshToken = urlParams.get("refreshToken");
+
+ if (accessToken) {
+ setAccessToken(accessToken);
+ setRefreshToken(refreshToken);
+ window.location.href = "/my";
+ }
+ }, [setAccessToken, setRefreshToken]);
+ return (
+
+ 구글 로그인 리다이렉트 페이지
+
+ );
+}
+export default GoogleLoginRedirect;
\ No newline at end of file
diff --git a/week8/m1/src/pages/Home.tsx b/week8/m1/src/pages/Home.tsx
new file mode 100644
index 00000000..85095824
--- /dev/null
+++ b/week8/m1/src/pages/Home.tsx
@@ -0,0 +1,575 @@
+import { useState, useEffect, useRef, useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import axios from "axios";
+import { postLp, uploadImage } from "../apis/lp";
+import type { ReqCreateLpDto } from "../types/lp";
+import { useDebounce } from "../hooks/useDebounce";
+
+interface Tag { id: number; name: string; }
+interface Likes { id: number; userId: number; }
+interface Lp {
+ id: number; title: string; content: string; thumbnail: string;
+ published: boolean; authorId: number; createdAt: Date; updatedAt: Date;
+ tags: Tag[]; likes: Likes[];
+}
+
+interface LpPage {
+ data: Lp[];
+ nextCursor: number | null;
+}
+
+type SortOrder = "newest" | "oldest";
+
+const fileToBase64 = (file: File): Promise =>
+ new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result as string);
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+const fetchLps = async (sort: SortOrder, search: string, cursor?: number): Promise => {
+ const order = sort === "newest" ? "desc" : "asc";
+ const params = new URLSearchParams({ order, limit: "20" });
+ if (cursor) params.append("cursor", String(cursor));
+ if (search.trim()) params.append("search", search.trim());
+
+ const { data } = await axios.get(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps?${params}`
+ );
+
+ const list: Lp[] = data?.data?.data ?? data?.data ?? data ?? [];
+ const nextCursor: number | null = data?.data?.nextCursor ?? null;
+ return { data: list, nextCursor };
+};
+
+// ── 스켈레톤 카드 (카드와 동일 크기) ─────────────────
+const SkeletonCard = () => (
+
+);
+
+const GridSkeleton = ({ count = 10 }: { count?: number }) => (
+
+ {Array.from({ length: count }).map((_, i) => )}
+
+);
+
+const GridError = ({ onRetry }: { onRetry: () => void }) => (
+
+
💿
+
데이터를 불러오지 못했습니다.
+
+
+);
+
+// ── LP 카드 ────────────────────────────────────────────
+const LpCard = ({ lp, onClick }: { lp: Lp; onClick: () => void }) => {
+ const [hovered, setHovered] = useState(false);
+
+ return (
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ onClick={onClick}
+ >
+ {lp.thumbnail ? (
+

+ ) : (
+
+ )}
+
+
+ {!lp.published && hovered && (
+
미발행
+ )}
+ {hovered && (
+
+ ❤️ {lp.likes.length}
+
+ )}
+
+
{lp.title}
+
{lp.content}
+ {lp.tags.length > 0 && (
+
+ {lp.tags.slice(0, 3).map((tag) => (
+
+ #{tag.name}
+
+ ))}
+ {lp.tags.length > 3 && (
+
+ +{lp.tags.length - 3}
+
+ )}
+
+ )}
+
+ {new Date(lp.createdAt).toLocaleDateString("ko-KR", { year: "numeric", month: "short", day: "numeric" })}
+
+
+
+
+ );
+};
+// ── LP 작성 모달 ───────────────────────────────────────
+interface CreateLpModalProps {
+ onClose: () => void;
+}
+
+const CreateLpModal = ({ onClose }: CreateLpModalProps) => {
+ const queryClient = useQueryClient();
+
+ const [title, setTitle] = useState("");
+ const [content, setContent] = useState("");
+ const [published, setPublished] = useState(true);
+ const [thumbnailFile, setThumbnailFile] = useState(null);
+ const [thumbnailPreview, setThumbnailPreview] = useState("");
+ const [tagInput, setTagInput] = useState("");
+ const [tags, setTags] = useState([]);
+
+ const overlayRef = useRef(null);
+
+ const createMutation = useMutation({
+ mutationFn: async (body: ReqCreateLpDto) => postLp(body),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["lps"] });
+ onClose();
+ },
+ });
+
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ setThumbnailFile(file);
+ const preview = await fileToBase64(file);
+ setThumbnailPreview(preview);
+ };
+
+ const handleAddTag = () => {
+ const trimmed = tagInput.trim();
+ if (trimmed && !tags.includes(trimmed)) {
+ setTags((prev) => [...prev, trimmed]);
+ }
+ setTagInput("");
+ };
+
+ const handleRemoveTag = (tag: string) => {
+ setTags((prev) => prev.filter((t) => t !== tag));
+ };
+
+ const handleTagKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ handleAddTag();
+ }
+ };
+
+ const handleSubmit = async () => {
+ if (!title.trim()) return;
+
+ let thumbnail: string | undefined;
+
+ if (thumbnailFile) {
+ thumbnail = await uploadImage(thumbnailFile);
+ }
+
+ createMutation.mutate({
+ title: title.trim(),
+ content: content.trim(),
+ thumbnail,
+ published,
+ tags,
+ });
+ };
+
+ const handleOverlayClick = (e: React.MouseEvent) => {
+ if (e.target === overlayRef.current) onClose();
+ };
+
+ return (
+
+
+ {/* 헤더 */}
+
+
새 LP 추가
+
+
+
+ {/* 썸네일 */}
+
+
+ {thumbnailPreview && (
+

+ )}
+
+
+
+ {/* 제목 */}
+
+
+ setTitle(e.target.value)}
+ placeholder="LP 제목을 입력하세요"
+ className="w-full rounded-lg border px-3 py-2 text-sm outline-none transition-colors"
+ style={{ borderColor: "#e0e9f8", color: "#1e738a" }} // 기본 테두리와 텍스트 색상
+ onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = "#3bf6f0")} // 포커스 시 테두리 색상 변경
+ onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = "#e0e9f8")}
+ />
+
+
+ {/* 내용 */}
+
+
+
+
+ {/* 태그 */}
+
+
+
+ setTagInput(e.target.value)}
+ onKeyDown={handleTagKeyDown}
+ placeholder="태그 입력 후 추가 버튼 클릭"
+ className="flex-1 rounded-lg border px-3 py-2 text-sm outline-none transition-colors"
+ style={{ borderColor: "#e0e9f8", color: "#1e738a" }}
+ onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = "#3bf6f0")}
+ onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = "#e0e9f8")}
+ />
+
+
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+
+ #{tag}
+
+
+ ))}
+
+ )}
+
+
+ {/* 공개 여부 */}
+
+
+
+ {published ? "공개" : "비공개"}
+
+
+ {/* 에러 */}
+ {createMutation.isError && (
+
+ LP 생성에 실패했습니다. 다시 시도해주세요.
+
+ )}
+
+ {/* 제출 */}
+
+
+
+ );
+};
+
+// ── 메인 ──────────────────────────────────────────────
+export default function Home() {
+ const navigate = useNavigate();
+ const isLoggedIn = true; // 실제 auth 훅으로 교체
+
+ const [sortOrder, setSortOrder] = useState("newest");
+ const [searchQuery, setSearchQuery] = useState("");
+ const debouncedSearchQuery = useDebounce(searchQuery, 300);
+ const [isModalOpen, setIsModalOpen] = useState(false); // LP 생성 모달 상태
+
+ // 무한스크롤 감지용 ref
+ const bottomRef = useRef(null);
+
+ const {
+ data,
+ isLoading,
+ isError,
+ isFetchingNextPage,
+ fetchNextPage,
+ hasNextPage,
+ refetch,
+ } = useInfiniteQuery({
+ queryKey: ["lps", sortOrder, debouncedSearchQuery],
+ queryFn: ({ pageParam }) => fetchLps(sortOrder, debouncedSearchQuery, pageParam as number | undefined),
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
+ initialPageParam: undefined,
+ staleTime: 1000 * 60,
+ gcTime: 1000 * 60 * 5,
+ });
+
+ // 모든 페이지 LP 평탄화
+ const lpList = data?.pages.flatMap((p) => p.data) ?? [];
+
+ const filtered = lpList;
+
+ // IntersectionObserver — 바닥 감지 시 fetchNextPage
+ const handleObserver = useCallback(
+ (entries: IntersectionObserverEntry[]) => {
+ if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ },
+ [hasNextPage, isFetchingNextPage, fetchNextPage]
+ );
+
+ useEffect(() => {
+ const el = bottomRef.current;
+ if (!el) return;
+ const observer = new IntersectionObserver(handleObserver, { threshold: 0.1 });
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, [handleObserver]);
+
+ const handleCardClick = (lp: Lp) => {
+ if (!isLoggedIn) {
+ if (window.confirm("로그인이 필요한 서비스입니다.\n로그인 페이지로 이동하시겠습니까?")) {
+ navigate("/login");
+ }
+ return;
+ }
+ navigate(`/lp/${lp.id}`);
+ };
+
+ return (
+
+
+
+ {/* 서브헤더: 검색 + 정렬 */}
+
+
+ 🔍
+ setSearchQuery(e.target.value)}
+ className="w-full pl-9 pr-4 py-2 rounded-xl text-sm outline-none border transition-colors"
+ style={{ background: "white", borderColor: searchQuery ? "#3b82f6" : "#dbeafe", color: "#1e3a8a" }}
+ />
+
+
+ {/* 정렬 토글 — 변경 시 첫 페이지부터 다시 로딩 */}
+
+ {(["newest", "oldest"] as SortOrder[]).map((order) => (
+
+ ))}
+
+
+
+ {/* 결과 수 */}
+
+
+ {isLoading ? "불러오는 중..." : `${filtered.length}개의 LP`}
+
+
+
+ {/* 그리드 영역 */}
+
+
+ {/* 초기 로딩 — 상단 스켈레톤 */}
+ {isLoading &&
}
+
+ {isError &&
}
+
+ {!isLoading && !isError && (
+ <>
+ {filtered.length === 0 ? (
+
+ ) : (
+
+ {filtered.map((lp) => (
+ handleCardClick(lp)} />
+ ))}
+
+ )}
+
+ {/* 추가 로딩 — 하단 스켈레톤 */}
+ {isFetchingNextPage && (
+
+
+
+ )}
+
+ {/* IntersectionObserver 트리거 */}
+
+
+ {/* 마지막 페이지 안내 */}
+ {!hasNextPage && lpList.length > 0 && (
+
+ 모든 LP를 불러왔습니다
+
+ )}
+ >
+ )}
+
+
+
+ {/* 플로팅 + 버튼 */}
+
+
+ {isModalOpen &&
setIsModalOpen(false)} />} {/* 모달 컴포넌트는 추후 구현 */}
+
+ );
+}
diff --git a/week8/m1/src/pages/Login.tsx b/week8/m1/src/pages/Login.tsx
new file mode 100644
index 00000000..1c909524
--- /dev/null
+++ b/week8/m1/src/pages/Login.tsx
@@ -0,0 +1,123 @@
+import useForm from "../hooks/useForm";
+import { validateSignin, type UserSigninInfo } from "../utils/validate";
+import googleLogo from "../imgs/google.png";
+import { useNavigate } from "react-router-dom";
+import { useAuth } from "../context/AuthContext";
+import { useEffect } from "react";
+import { ChevronLeft } from "lucide-react";
+import { useMutation } from "@tanstack/react-query";
+
+export default function Login() {
+ const navigate = useNavigate();
+
+ const { login, isAuthenticated } = useAuth();
+
+ const { values, errors, touched, getInputProps } = useForm({
+ init_val: {
+ email: "",
+ password: "",
+ },
+ validate: validateSignin,
+ });
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate("/");
+ }
+ }, [navigate, isAuthenticated]);
+
+ const loginMutation = useMutation({
+ mutationFn: () => login(values),
+ onSuccess: () => {
+ navigate("/");
+ },
+ onError: (err) => {
+ console.error("Login failed:", err);
+ },
+ });
+
+ const handleSubmit = () => {
+ loginMutation.mutate();
+ };
+
+ const handleGoogleLogin = () => {
+ window.location.href =
+ import.meta.env.VITE_SERVER_API_URL + "/v1/auth/google/login";
+ };
+
+ const isDisabled =
+ Object.values(errors || {}).some((err) => err) ||
+ !values.email ||
+ !values.password ||
+ loginMutation.isPending;
+
+ return (
+
+
+
+
로그인
+
+
+
+
+
+
+
+ OR
+
+
+
+
+
+ {errors?.email && touched?.email && (
+
{errors.email}
+ )}
+
+
+ {errors?.password && touched?.password && (
+
+ {errors.password}
+
+ )}
+
+
+ {loginMutation.isError && (
+
+ 이메일 또는 비밀번호가 올바르지 않습니다.
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/week8/m1/src/pages/LpDetail.tsx b/week8/m1/src/pages/LpDetail.tsx
new file mode 100644
index 00000000..c8608934
--- /dev/null
+++ b/week8/m1/src/pages/LpDetail.tsx
@@ -0,0 +1,632 @@
+import { useState, useEffect, useRef, useCallback } from "react";
+import { useParams, useNavigate } from "react-router-dom";
+import { useQuery, useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { useAuth } from "../context/AuthContext";
+import {
+ deleteLpApi,
+ toggleLike,
+ postComment,
+ updateComment,
+ deleteComment,
+} from "../apis/lp";
+import { LOCAL_STORAGE_KEY } from "../constants/key";
+import axios from "axios";
+
+const COLORS = {
+ bg: "#F4FFFC",
+ soft: "#E9FFFC",
+ main: "#0ECFD3",
+ dark: "#0BAEB3",
+ text: "#063B3D",
+ border: "#B8F3EE",
+ muted: "#5B8C8E",
+ faint: "#7AA6A8",
+};
+
+const getAuthHeader = () => {
+ try {
+ const token = JSON.parse(
+ localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) ?? "null"
+ );
+ return token ? { Authorization: `Bearer ${token}` } : {};
+ } catch {
+ return {};
+ }
+};
+
+interface Tag { id: number; name: string; }
+interface Likes { id: number; userId: number; }
+interface Lp {
+ id: number; title: string; content: string; thumbnail: string;
+ published: boolean; authorId: number; createdAt: Date; updatedAt: Date;
+ tags: Tag[]; likes: Likes[];
+}
+interface Comment {
+ id: number; content: string; authorId: number;
+ author: { name: string };
+ createdAt: Date;
+}
+interface CommentPage {
+ data: Comment[];
+ nextCursor: number | null;
+}
+
+type CommentOrder = "newest" | "oldest";
+
+const fetchLpDetail = async (lpId: string): Promise => {
+ const { data } = await axios.get(`${import.meta.env.VITE_SERVER_API_URL}/v1/lps/${lpId}`);
+ const lp = data?.data ?? data;
+ if (!lp) throw new Error("LP 데이터가 없습니다.");
+ return lp;
+};
+
+const fetchComments = async (
+ lpId: string,
+ order: CommentOrder,
+ cursor?: number
+): Promise => {
+ const params = new URLSearchParams({
+ order: order === "newest" ? "desc" : "asc",
+ limit: "10",
+ });
+ if (cursor) params.append("cursor", String(cursor));
+
+ const { data } = await axios.get(
+ `${import.meta.env.VITE_SERVER_API_URL}/v1/lps/${lpId}/comments?${params}`,
+ { headers: getAuthHeader() }
+ );
+
+ return {
+ data: data?.data?.data ?? data?.data ?? [],
+ nextCursor: data?.data?.nextCursor ?? null,
+ };
+};
+
+const DetailSkeleton = () => (
+
+);
+
+const CommentSkeleton = () => (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+);
+
+const DetailError = ({ onRetry }: { onRetry: () => void }) => (
+
+
💿
+
데이터를 불러오지 못했습니다.
+
+
+);
+
+interface CommentMenuProps {
+ onEdit: () => void;
+ onDelete: () => void;
+}
+
+const CommentMenu = ({ onEdit, onDelete }: CommentMenuProps) => {
+ const [open, setOpen] = useState(false);
+ const menuRef = useRef(null);
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ setOpen(false);
+ }
+ };
+ document.addEventListener("mousedown", handler);
+ return () => document.removeEventListener("mousedown", handler);
+ }, []);
+
+ return (
+
+
+
+ {open && (
+
+
+
+
+ )}
+
+ );
+};
+
+export default function LpDetailPage() {
+ const { lpId } = useParams<{ lpId: string }>();
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const { userId: currentUserId } = useAuth();
+
+ const [commentOrder, setCommentOrder] = useState("newest");
+ const [commentInput, setCommentInput] = useState("");
+ const [commentError, setCommentError] = useState("");
+ const [editingCommentId, setEditingCommentId] = useState(null);
+ const [editingContent, setEditingContent] = useState("");
+
+ const commentBottomRef = useRef(null);
+
+ const { data: lp, isLoading, isError, refetch } = useQuery({
+ queryKey: ["lp", lpId],
+ queryFn: () => fetchLpDetail(lpId!),
+ enabled: !!lpId,
+ staleTime: 1000 * 60,
+ gcTime: 1000 * 60 * 5,
+ });
+
+ const {
+ data: commentData,
+ isLoading: isCommentLoading,
+ isFetchingNextPage: isCommentFetchingNext,
+ fetchNextPage: fetchNextComments,
+ hasNextPage: hasNextComments,
+ } = useInfiniteQuery({
+ queryKey: ["lpComments", lpId, commentOrder],
+ queryFn: ({ pageParam }) =>
+ fetchComments(lpId!, commentOrder, pageParam as number | undefined),
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
+ initialPageParam: undefined,
+ enabled: !!lpId,
+ staleTime: 1000 * 30,
+ });
+
+ const comments = commentData?.pages.flatMap((p) => p.data) ?? [];
+
+ const handleCommentObserver = useCallback(
+ (entries: IntersectionObserverEntry[]) => {
+ if (entries[0].isIntersecting && hasNextComments && !isCommentFetchingNext) {
+ fetchNextComments();
+ }
+ },
+ [hasNextComments, isCommentFetchingNext, fetchNextComments]
+ );
+
+ useEffect(() => {
+ const el = commentBottomRef.current;
+ if (!el) return;
+
+ const observer = new IntersectionObserver(handleCommentObserver, { threshold: 0.1 });
+ observer.observe(el);
+
+ return () => observer.disconnect();
+ }, [handleCommentObserver]);
+
+ const deleteMutation = useMutation({
+ mutationFn: () => deleteLpApi(Number(lpId)),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["lps"] });
+ navigate(-1);
+ },
+ });
+
+ const likeMutation = useMutation({
+ mutationFn: () => toggleLike(Number(lpId)),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["lp", lpId] }),
+ });
+
+ const commentMutation = useMutation({
+ mutationFn: () =>
+ postComment({ lpId: Number(lpId), content: commentInput.trim() }),
+ onSuccess: () => {
+ setCommentInput("");
+ setCommentError("");
+ queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] });
+ },
+ });
+
+ const editCommentMutation = useMutation({
+ mutationFn: ({ commentId, content }: { commentId: number; content: string }) =>
+ updateComment({ lpId: Number(lpId), commentId, content }),
+ onSuccess: () => {
+ setEditingCommentId(null);
+ setEditingContent("");
+ queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] });
+ },
+ });
+
+ const deleteCommentMutation = useMutation({
+ mutationFn: (commentId: number) =>
+ deleteComment({ lpId: Number(lpId), commentId }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] });
+ },
+ });
+
+ const handleCommentSubmit = () => {
+ if (commentInput.trim().length < 1) {
+ setCommentError("댓글을 입력해주세요.");
+ return;
+ }
+ if (commentInput.trim().length > 300) {
+ setCommentError("댓글은 300자 이하로 입력해주세요.");
+ return;
+ }
+ setCommentError("");
+ commentMutation.mutate();
+ };
+
+ const handleDelete = () => {
+ if (window.confirm("정말 삭제하시겠습니까?")) deleteMutation.mutate();
+ };
+
+ const handleEditStart = (comment: Comment) => {
+ setEditingCommentId(comment.id);
+ setEditingContent(comment.content);
+ };
+
+ const handleEditSubmit = (commentId: number) => {
+ if (!editingContent.trim()) return;
+ editCommentMutation.mutate({ commentId, content: editingContent.trim() });
+ };
+
+ const handleDeleteComment = (commentId: number) => {
+ if (window.confirm("댓글을 삭제하시겠습니까?")) {
+ deleteCommentMutation.mutate(commentId);
+ }
+ };
+
+ const isOwner = lp?.authorId === currentUserId;
+ const isLiked = lp?.likes.some((l) => l.userId === currentUserId) ?? false;
+
+ return (
+
+
+
+
+ {isOwner && lp && (
+
+
+
+
+ )}
+
+
+
+ {isLoading && }
+ {isError && }
+
+ {!isLoading && !isError && lp && (
+
+
+
+ {lp.thumbnail ? (
+

+ ) : (
+
+ )}
+
+
+
+
+ {!lp.published && (
+
+ 미발행
+
+ )}
+
+
+ {lp.title}
+
+
+
+ {new Date(lp.createdAt).toLocaleDateString("ko-KR", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ })}
+
+
+ {lp.tags.length > 0 && (
+
+ {lp.tags.map((tag) => (
+
+ #{tag.name}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+
+ {lp.content}
+
+
+
+
+
+ 댓글 {comments.length}
+
+
+
+ {(["newest", "oldest"] as CommentOrder[]).map((order) => (
+
+ ))}
+
+
+
+
+
+ {isCommentLoading && }
+
+ {!isCommentLoading && (
+
+ {comments.length === 0 ? (
+
+ 첫 댓글을 남겨보세요!
+
+ ) : (
+ comments.map((comment) => {
+ const isMyComment = comment.authorId === currentUserId;
+ const isEditing = editingCommentId === comment.id;
+
+ return (
+
+
+
+ {comment.author?.name?.[0] ?? "?"}
+
+
+
+ {comment.author?.name ?? "익명"}
+
+
+
+ {new Date(comment.createdAt).toLocaleDateString("ko-KR", {
+ month: "short",
+ day: "numeric",
+ })}
+
+
+ {isMyComment && (
+
handleEditStart(comment)}
+ onDelete={() => handleDeleteComment(comment.id)}
+ />
+ )}
+
+
+ {isEditing ? (
+
+ ) : (
+
+ {comment.content}
+
+ )}
+
+ );
+ })
+ )}
+
+ {isCommentFetchingNext && (
+
+ )}
+
+
+
+ {!hasNextComments && comments.length > 0 && (
+
+ 모든 댓글을 불러왔습니다
+
+ )}
+
+ )}
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/week8/m1/src/pages/Mypage.tsx b/week8/m1/src/pages/Mypage.tsx
new file mode 100644
index 00000000..9138655f
--- /dev/null
+++ b/week8/m1/src/pages/Mypage.tsx
@@ -0,0 +1,297 @@
+import { useState, useRef } from "react";
+import { useNavigate } from "react-router";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { getMyInfo, patchMyInfo } from "../apis/auth";
+import { useAuth } from "../context/AuthContext";
+import type { ReqUpdateMyInfoDto } from "../types/auth";
+
+const fileToBase64 = (file: File): Promise =>
+ new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result as string);
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+
+export default function MyPage() {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const { logout } = useAuth();
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [editName, setEditName] = useState("");
+ const [editBio, setEditBio] = useState("");
+ const [avatarFile, setAvatarFile] = useState(null);
+ const [avatarPreview, setAvatarPreview] = useState("");
+
+ const fileInputRef = useRef(null);
+
+ const { data, isLoading } = useQuery({
+ queryKey: ["myInfo"],
+ queryFn: getMyInfo,
+ staleTime: 1000 * 60,
+ });
+
+ const user = data?.data;
+
+ const updateMutation = useMutation({
+ mutationFn: (body: ReqUpdateMyInfoDto) => patchMyInfo(body),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["myInfo"] });
+ setIsEditing(false);
+ setAvatarFile(null);
+ setAvatarPreview("");
+ },
+ });
+
+ const logoutMutation = useMutation({
+ mutationFn: logout,
+ onSuccess: () => navigate("/"),
+ });
+
+ const openEdit = () => {
+ setEditName(user?.name ?? "");
+ setEditBio(user?.bio ?? "");
+ setAvatarPreview(user?.avatar ?? "");
+ setIsEditing(true);
+ };
+
+ const handleAvatarChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ setAvatarFile(file);
+ const preview = await fileToBase64(file);
+ setAvatarPreview(preview);
+ };
+
+ const handleSave = async () => {
+ const body: ReqUpdateMyInfoDto = {};
+ if (editName.trim()) body.name = editName.trim();
+ if (editBio !== undefined) body.bio = editBio;
+ if (avatarFile) {
+ body.avatar = await fileToBase64(avatarFile);
+ }
+ updateMutation.mutate(body);
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {/* 상단 헤더 */}
+
+
+ {/* 프로필 카드 */}
+
+ {/* 아바타 */}
+
+
+ {user?.avatar ? (
+

+ ) : (
+ user?.name?.[0] ?? "?"
+ )}
+
+
+
+ {/* 이름 */}
+
+
{user?.name}
+
{user?.email}
+ {user?.bio && (
+
{user.bio}
+ )}
+
+
+ {/* 버튼 영역 */}
+
+
+
+
+
+ {/* 가입일 */}
+ {user?.createdAt && (
+
+ 가입일: {new Date(user.createdAt).toLocaleDateString("ko-KR")}
+
+ )}
+
+
+ {/* 프로필 수정 모달 */}
+ {isEditing && (
+
{ if (e.target === e.currentTarget) setIsEditing(false); }}
+ >
+
+ {/* 헤더 */}
+
+
프로필 수정
+
+
+
+ {/* 아바타 수정 */}
+
+
fileInputRef.current?.click()}
+ >
+ {avatarPreview ? (
+

+ ) : (
+ user?.name?.[0] ?? "?"
+ )}
+
+
+
+
+
+ {/* 이름 */}
+
+
+ setEditName(e.target.value)}
+ placeholder="이름을 입력하세요"
+ className="w-full rounded-lg border px-3 py-2 text-sm outline-none transition-colors"
+ style={{ borderColor: "#e0e9f8", color: "#1e3a8a" }}
+ onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = "#0ECFD3")}
+ onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = "#e0e9f8")}
+ />
+
+
+ {/* Bio */}
+
+
+
+
+ {updateMutation.isError && (
+
저장에 실패했습니다. 다시 시도해주세요.
+ )}
+
+
+
+
+
+
+
+ )}
+
+
+
+
+ );
+
+}
diff --git a/week8/m1/src/pages/NotFoundErr.tsx b/week8/m1/src/pages/NotFoundErr.tsx
new file mode 100644
index 00000000..4f78dc76
--- /dev/null
+++ b/week8/m1/src/pages/NotFoundErr.tsx
@@ -0,0 +1,7 @@
+export default function NotFoundErr() {
+ return <>error>
+}
+
+/*
+라우터에서 경로 못 찾으면 NotFoundErr 페이지 보여주기
+*/
\ No newline at end of file
diff --git a/week8/m1/src/pages/SignUp.tsx b/week8/m1/src/pages/SignUp.tsx
new file mode 100644
index 00000000..427028da
--- /dev/null
+++ b/week8/m1/src/pages/SignUp.tsx
@@ -0,0 +1,178 @@
+import { useState } from "react";
+import { z } from "zod"; // 유효성 검사 라이브러리
+import { useForm, type SubmitHandler } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { postSignup } from "../apis/auth";
+import googleLogo from "../imgs/google.png";
+import { useNavigate } from "react-router-dom";
+import { Eye, EyeOff, ChevronLeft, Camera } from "lucide-react";
+
+
+// Schema 정의
+const schema = z.object({
+ email: z.string().email({ message: "올바른 이메일 형식이 아닙니다!" }),
+ password: z
+ .string()
+ .min(8, { message: "비밀번호는 8자 이상이어야 합니다!" })
+ .max(20, { message: "비밀번호는 20자 이하여야 합니다!" }),
+ passwordCheck: z.string(),
+ name: z
+ .string()
+ .min(2, { message: "닉네임은 2자 이상이어야 합니다." })
+ .max(10, { message: "닉네임은 10자 이하로 설정해주세요." })
+}).refine((data) => data.password === data.passwordCheck, {
+ message: "비밀번호가 일치하지 않습니다.",
+ path: ["passwordCheck"]
+});
+
+type FormFields = z.infer;
+
+export default function SignUp() {
+ const [step, setStep] = useState<1 | 2 | 3>(1);
+ const [showPw, setShowPw] = useState(false);
+ const [showPwCheck, setShowPwCheck] = useState(false);
+ const navigate = useNavigate();
+
+ const { register, handleSubmit, trigger, watch, formState: { errors, isSubmitting, isValid } } = useForm({
+ mode: "onChange",
+ defaultValues: { email: "", password: "", passwordCheck: "", name: "" },
+ resolver: zodResolver(schema),
+ });
+
+ const emailValue = watch("email");
+
+ // 단계별 유효성 검사 후 다음 단계로 이동
+ const goNext = async (fields: (keyof FormFields)[], nextStep: 1 | 2 | 3) => {
+ const isStepValid = await trigger(fields);
+ if (isStepValid) setStep(nextStep);
+ };
+
+ const onSubmit: SubmitHandler = async (data) => {
+ try {
+ const { passwordCheck, ...rest } = data;
+ await postSignup(rest); // rest에는 email, password, name(닉네임)이 포함됨
+ alert("회원가입이 완료되었습니다!");
+ navigate("/");
+ } catch (error: any) {
+ alert(error.response?.data?.message || "회원가입 중 오류가 발생했습니다.");
+ }
+ };
+
+ return (
+
+ {/* 상단 헤더 */}
+
+
+
회원가입
+
+
+
+ {/* STEP 1: 이메일 입력 */}
+ {step === 1 && (
+
+
+
+
+ OR
+
+
+
+
+ {errors.email &&
{errors.email.message}
}
+
+
+
+ )}
+
+ {/* STEP 2: 비밀번호 설정 */}
+ {step === 2 && (
+
+
+
+
+
+
+ {errors.password &&
{errors.password.message}
}
+
+
+
+
+
+ {errors.passwordCheck &&
{errors.passwordCheck.message}
}
+
+
+
+ )}
+
+ {/* STEP 3: 닉네임(name) 및 프로필 설정 */}
+ {step === 3 && (
+
+
+
거의 다 왔어요!
+
사용하실 닉네임을 설정해주세요.
+
+
+ {/* 프로필 이미지 UI (Placeholder) */}
+
+
+
+
+ {errors.name &&
{errors.name.message}
}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/week8/m1/src/types/auth.ts b/week8/m1/src/types/auth.ts
new file mode 100644
index 00000000..8b4ae23f
--- /dev/null
+++ b/week8/m1/src/types/auth.ts
@@ -0,0 +1,53 @@
+import type { CommonRes } from "./common";
+
+// 회원 가입
+export type ReqSignUpDto = { // 회원 가입 요청 시 필요한 데이터
+ name: string;
+ email: string;
+ bio?: string;
+ avatar?: string;
+ password: string;
+};
+
+export type ResSignUpDto = CommonRes<{ // 회원 가입 성공 시 반환되는 데이터
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}>;
+
+// 로그인
+
+export type ReqSignInDto = { // 로그인 요청 시 필요한 데이터
+ email: string;
+ password: string;
+}
+
+export type ResSignInDto = CommonRes<{ // 로그인 성공 시 반환되는 데이터
+ id: number;
+ name: string;
+ accessToken: string;
+ refreshToken: string;
+}>
+
+// 내 정보 조회
+
+export type ResMyInfoDto = CommonRes<{ // 내 정보 조회 성공 시 반환되는 데이터
+ id: number;
+ name: string;
+ email: string;
+ bio: string | null;
+ avatar: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}>;
+
+// 프로필 수정
+export type ReqUpdateMyInfoDto = {
+ name?: string;
+ bio?: string;
+ avatar?: string;
+};
\ No newline at end of file
diff --git a/week8/m1/src/types/common.ts b/week8/m1/src/types/common.ts
new file mode 100644
index 00000000..ba384ece
--- /dev/null
+++ b/week8/m1/src/types/common.ts
@@ -0,0 +1,21 @@
+export type CommonRes = {
+ status: boolean;
+ message: string;
+ data: T
+}
+
+export type CursorBasedResponse = {
+ status: boolean;
+ statusCode: number;
+ message: string;
+ data: T;
+ nextCursor: number;
+ hasNext: boolean;
+}
+
+export type PaginationDto = {
+ cursor?: number;
+ limit?: number;
+ search?: string;
+ order?: "asc" | "desc";
+}
\ No newline at end of file
diff --git a/week8/m1/src/types/lp.ts b/week8/m1/src/types/lp.ts
new file mode 100644
index 00000000..a6444eab
--- /dev/null
+++ b/week8/m1/src/types/lp.ts
@@ -0,0 +1,51 @@
+import type { CursorBasedResponse, CommonRes } from "./common";
+
+export type Tag = {
+ id: number;
+ name: string;
+};
+
+export type Likes = {
+ id: number;
+ userId: number;
+ lpId: number;
+};
+
+export type LpData = {
+ id: number;
+ title: string;
+ content: string;
+ thumbnail: string;
+ published: boolean;
+ authorId: number;
+ createdAt: Date;
+ updatedAt: Date;
+ tags: Tag[];
+ likes: Likes[];
+};
+
+export type ResponseLpListDto = CursorBasedResponse<{
+ data: LpData;
+}>;
+
+export type ResLpDto = CommonRes;
+
+export type ReqCreateLpDto = {
+ title: string;
+ content: string;
+ thumbnail?: string;
+ published: boolean;
+ tags: string[];
+};
+
+export type CommentData = {
+ id: number;
+ content: string;
+ authorId: number;
+ author: { name: string };
+ createdAt: Date;
+};
+
+export type ResCommentListDto = CursorBasedResponse<{
+ data: CommentData[];
+}>;
\ No newline at end of file
diff --git a/week8/m1/src/utils/validate.ts b/week8/m1/src/utils/validate.ts
new file mode 100644
index 00000000..6ac21aaf
--- /dev/null
+++ b/week8/m1/src/utils/validate.ts
@@ -0,0 +1,27 @@
+export type UserSigninInfo = {
+ email: string;
+ password: string;
+}
+
+function validateUser(vals: UserSigninInfo) {
+ const errors = {
+ email: "",
+ password: ""
+ };
+
+ if (!(/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i.test(vals.email)))
+ errors.email = "올바른 이메일 형식이 아닙니다!";
+
+ if (!(vals.password.length >= 8 && vals.password.length < 20))
+ errors.password = "비밀번호는 8 ~ 20자 사이로 입력해주세요!";
+
+ return errors;
+}
+
+// 로그인 유효성 검사
+
+function validateSignin(vals: UserSigninInfo) {
+ return validateUser(vals);
+}
+
+export { validateSignin }
\ No newline at end of file
diff --git a/week8/m1/tsconfig.json b/week8/m1/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/week8/m1/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/week8/m1/vite-env.d.ts b/week8/m1/vite-env.d.ts
new file mode 100644
index 00000000..f56ab64d
--- /dev/null
+++ b/week8/m1/vite-env.d.ts
@@ -0,0 +1,16 @@
+///
+
+interface ViteTypeOptions {
+ // 아래 라인을 추가하면, ImportMetaEnv 타입을 엄격하게 설정해
+ // 알 수 없는 키를 허용하지 않게 할 수 있습니다.
+ // strictImportMetaEnv: unknown
+}
+
+interface ImportMetaEnv {
+ readonly VITE_SERVER_API_URL: string
+ // 다른 환경 변수들에 대한 타입 정의...
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
\ No newline at end of file
diff --git a/week8/m1/vite.config.ts b/week8/m1/vite.config.ts
new file mode 100644
index 00000000..004a47f7
--- /dev/null
+++ b/week8/m1/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import tailwindcss from '@tailwindcss/vite';
+
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+});
\ No newline at end of file