From 52cc6dbc30a1b80ff5f6aedcfeb195d71c213816 Mon Sep 17 00:00:00 2001 From: goeun Date: Sun, 22 Mar 2026 17:01:27 +0900 Subject: [PATCH 1/7] =?UTF-8?q?mission:=20[week1/mission]=20TypeScript?= =?UTF-8?q?=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=98=EC=97=AC=20ToDoList=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- week1/dist/index.js | 69 +++++++++++++++++++++++++ week1/index.html | 37 ++++++++++++++ week1/src/index.ts | 95 ++++++++++++++++++++++++++++++++++ week1/style.css | 121 ++++++++++++++++++++++++++++++++++++++++++++ week1/tsconfig.json | 19 +++++++ 5 files changed, 341 insertions(+) create mode 100644 week1/dist/index.js create mode 100644 week1/index.html create mode 100644 week1/src/index.ts create mode 100644 week1/style.css create mode 100644 week1/tsconfig.json diff --git a/week1/dist/index.js b/week1/dist/index.js new file mode 100644 index 00000000..5beb347a --- /dev/null +++ b/week1/dist/index.js @@ -0,0 +1,69 @@ +"use strict"; +const todoInput = document.getElementById('todo-input'); +const todoForm = document.getElementById('todo-form'); +const todoList = document.getElementById('todo-list'); +const doneList = document.getElementById('done-list'); +let todos = []; +let doneTasks = []; +const renderTasks = () => { + todoList.innerHTML = ''; + doneList.innerHTML = ''; + todos.forEach((todo) => { + const li = createTodoElement(todo, false); + todoList.appendChild(li); + }); + doneTasks.forEach((todo) => { + const li = createTodoElement(todo, true); + doneList.appendChild(li); + }); +}; +const getTodoText = () => { + return todoInput.value.trim(); +}; +const addTodo = (text) => { + todos.push({ id: Date.now(), text }); + todoInput.value = ''; + renderTasks(); +}; +const compleTodo = (todo) => { + todos = todos.filter((t) => t.id !== todo.id); + doneTasks.push(todo); + renderTasks(); +}; +const deleteTodo = (todo) => { + doneTasks = doneTasks.filter((t) => t.id !== todo.id); + renderTasks(); +}; +const createTodoElement = (todo, isDone) => { + const li = document.createElement('li'); + li.classList.add('render-container__item'); + li.textContent = todo.text; + const button = document.createElement('button'); + button.classList.add('render-container__item-button'); + if (isDone) { + button.textContent = '삭제'; + button.style.backgroundColor = '#dc3545'; + } + else { + button.textContent = '완료'; + button.style.backgroundColor = '#28a745'; + } + button.addEventListener('click', () => { + if (isDone) { + deleteTodo(todo); + } + else { + compleTodo(todo); + } + }); + li.appendChild(button); + return li; +}; +todoForm.addEventListener('submit', (event) => { + event.preventDefault(); + const text = getTodoText(); + if (text) { + addTodo(text); + } +}); +renderTasks(); diff --git a/week1/index.html b/week1/index.html new file mode 100644 index 00000000..766bcfba --- /dev/null +++ b/week1/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + UMC TODO + + +
+

🍿POPCORN TODO🍿

+
+ + +
+
+
+

할 일

+
    +
    +
    +

    완료

    +
      +
      +
      +
      + + \ No newline at end of file diff --git a/week1/src/index.ts b/week1/src/index.ts new file mode 100644 index 00000000..852d12b9 --- /dev/null +++ b/week1/src/index.ts @@ -0,0 +1,95 @@ +// 1. HTML 요소 선택(핸드북 자바 스크립트 편 참고) +const todoInput = document.getElementById('todo-input') as HTMLInputElement; +const todoForm = document.getElementById('todo-form') as HTMLFormElement; +const todoList = document.getElementById('todo-list') as HTMLUListElement; +const doneList = document.getElementById('done-list') as HTMLUListElement; + +//2. 할 일이 어떻게 생긴애인지 Type을 정의 +type Todo = { + id: number; + text: string; +}; + +let todos: Todo[] = []; +let doneTasks: Todo[] = []; + +//- 할 일 목록 렌더링 하는 함수를 정의 +const renderTasks = (): void => { + todoList.innerHTML = ''; + doneList.innerHTML = ''; + + todos.forEach((todo): void => { + const li = createTodoElement(todo, false); + todoList.appendChild(li); + }); + + doneTasks.forEach((todo): void => { + const li = createTodoElement(todo, true); + doneList.appendChild(li); + }) +}; + +//3. 할 일 텍스트 입력 처리 함수(공백 잘라줌) +const getTodoText = (): string => { + return todoInput.value.trim(); +}; + +//4. 할 일 추가 처리 함수 +const addTodo = (text: string) : void => { + todos.push({id: Date.now(), text}); + todoInput.value = ''; + renderTasks(); +}; + +//5. 할 일 상태 변경(완료로 이동) +const compleTodo = (todo: Todo): void => { + todos = todos.filter((t): boolean => t.id !== todo.id); + doneTasks.push(todo); + renderTasks(); +}; + +//6. 완료된 할 일 삭제 함수 +const deleteTodo = (todo: Todo): void => { + doneTasks = doneTasks.filter((t): boolean => t.id !== todo.id); + renderTasks(); +} + +//7. 할 일 아이템 생성 함수(완료 여부에 따라 버튼 텍스트나 색상 설정) +const createTodoElement = (todo: Todo, isDone: boolean): HTMLLIElement =>{ + const li = document.createElement('li'); + li.classList.add('render-container__item'); + li.textContent = todo.text; + + const button = document.createElement('button'); + button.classList.add('render-container__item-button'); + + if(isDone){ + button.textContent = '삭제'; + button.style.backgroundColor = '#dc3545'; + } else{ + button.textContent = '완료'; + button.style.backgroundColor = '#28a745'; + } + + button.addEventListener('click', ():void =>{ + if(isDone){ + deleteTodo(todo); + } else { + compleTodo(todo); + } + }); + + li.appendChild(button); + return li; +}; + +//8. 폼 제출 이벤트 리스너 +todoForm.addEventListener('submit', (event: Event): void =>{ + event.preventDefault(); + const text = getTodoText(); + if(text){ + addTodo(text); + } +}); + +renderTasks(); \ No newline at end of file diff --git a/week1/style.css b/week1/style.css new file mode 100644 index 00000000..00c6fdb6 --- /dev/null +++ b/week1/style.css @@ -0,0 +1,121 @@ +*{ + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body{ + font-family: 'Roboto', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #f1f1f1; +} + +.todo-container{ + background: white; + padding: 20px; + border-radius: 12px; + box-shadow: 0 4px 10px rgb(0, 0, 0, 0.1); + width: 350px; + text-align: center +} + +.todo-container__header{ + font-size: 24px; + margin-bottom: 16px; +} + +.todo-container__form{ + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.todo-container__input{ + flex: 1; + padding: 8px; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 14px; +} + +.todo-container__button{ + background-color: #28a745; + color: white; + border: none; + padding: 8px 12px; + cursor: pointer; + border-radius: 6px; + transition: background-color 0.3s ease; +} + +.todo-container__button:hover{ + background-color: #218838; +} + +.render-container{ + display: flex; + justify-content: space-between; + gap: 20px; +} + +.render-container__title{ + font-size: 18px; + margin-bottom: 10px; + + display: flex; + justify-content: center; +} + +.render-container__section{ + width: 100%; + text-align: left; +} + +.render-container__title{ + font-size: 18px; + margin-bottom: 10px; + + display: flex; + justify-content: center; +} + +.render-container__list{ + list-style: none; + padding: 0; + margin: 0; +} + +.render-container__item{ + display: flex; + justify-content: space-between; + align-items: center; + + padding: 8px; + border-bottom: 1px solid #ddd; + background-color: #f9f9f9; + border-radius: 6px; + margin-bottom: 6px; + width: 100%; +} + +.render-container__item-text{ + flex: 1; +} + +.render-container__item-button{ + background-color: #dc3545; + color: white; + border: none; + padding: 6px 10px; + cursor: pointer; + border-radius: 6px; + font-size: 12px; + transition: background-color 0.3s ease; +} + +.render-container__item-button:hover { + background-color: #c82333; +} \ No newline at end of file diff --git a/week1/tsconfig.json b/week1/tsconfig.json new file mode 100644 index 00000000..f537001c --- /dev/null +++ b/week1/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2016", // ECMAScript 2016으로 컴파일 + "module": "ES2015", // ES2015 모듈 시스템 사용 + "rootDir": "./src", // 소스 파일의 루트 디렉토리 + "outDir": "./dist", // 컴파일된 파일이 저장될 디렉토리 + "esModuleInterop": true, // ES 모듈 호환성 설정 + "forceConsistentCasingInFileNames": true, // 파일 이름의 대소문자 일관성 강제 + "strict": true, // 엄격한 타입 검사 + "skipLibCheck": true, // 라이브러리 파일 검사 건너뜀 + "removeComments": true, // 컴파일된 코드에서 주석 제거 + "noEmitOnError": false, // 컴파일 에러 발생 시 파일 생성 안 함 + "noUnusedLocals": true, // 사용하지 않는 지역 변수에 대해 에러 발생 + "noUnusedParameters": true, // 사용하지 않는 매개변수에 대해 에러 발생 + "noImplicitReturns": true, // 함수에서 명시적으로 값을 반환하지 않는 경우 에러 발생 + "noFallthroughCasesInSwitch": true, // switch 문에서 fallthrough 방지 + "noUncheckedIndexedAccess": true // 인덱스 접근 시 체크되지 않은 경우 에러 발생 + } +} \ No newline at end of file From df69c8807a0fdde172e68f27df64b02df7bfd43f Mon Sep 17 00:00:00 2001 From: goeun Date: Sat, 9 May 2026 17:25:12 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[chapter6/mission1]=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?1.=20useQuery=EB=A1=9C=20LP=20=EB=AA=A9=EB=A1=9D/=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EB=A7=8C=EB=93=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UMC-10th-mission-FE/.env | 2 + UMC-10th-mission-FE/.gitignore | 24 + UMC-10th-mission-FE/README.md | 73 + UMC-10th-mission-FE/eslint.config.js | 22 + UMC-10th-mission-FE/index.html | 13 + UMC-10th-mission-FE/package.json | 45 + UMC-10th-mission-FE/pnpm-lock.yaml | 2365 +++++++++++++++++ UMC-10th-mission-FE/public/favicon.svg | 1 + UMC-10th-mission-FE/public/icons.svg | 24 + UMC-10th-mission-FE/src/App.css | 60 + UMC-10th-mission-FE/src/App.tsx | 79 + UMC-10th-mission-FE/src/apis/auth.ts | 35 + UMC-10th-mission-FE/src/apis/axios.ts | 73 + UMC-10th-mission-FE/src/apis/lp.ts | 17 + UMC-10th-mission-FE/src/assets/hero.png | Bin 0 -> 13057 bytes UMC-10th-mission-FE/src/assets/react.svg | 1 + UMC-10th-mission-FE/src/assets/vite.svg | 1 + UMC-10th-mission-FE/src/constants/key.ts | 4 + .../src/context/AuthContext.tsx | 72 + UMC-10th-mission-FE/src/hooks/useForm.ts | 43 + .../src/hooks/useGetLPDetail.ts | 13 + UMC-10th-mission-FE/src/hooks/useGetLpList.ts | 11 + UMC-10th-mission-FE/src/hooks/useGetMyInfo.ts | 12 + .../src/hooks/useLocalStorage.ts | 36 + UMC-10th-mission-FE/src/imgs/google.png | Bin 0 -> 6624 bytes UMC-10th-mission-FE/src/index.css | 36 + .../src/layouts/HomeLayout.tsx | 91 + .../src/layouts/PrivateLayout.tsx | 14 + .../src/layouts/ProtectedLayout.tsx | 13 + UMC-10th-mission-FE/src/main.tsx | 10 + .../src/pages/GoogleLoginRedirectPage.tsx | 38 + UMC-10th-mission-FE/src/pages/HomePage.tsx | 28 + .../src/pages/LPDetailPage.tsx | 112 + UMC-10th-mission-FE/src/pages/LPListPage.tsx | 104 + UMC-10th-mission-FE/src/pages/LoginPage.tsx | 127 + UMC-10th-mission-FE/src/pages/MyPage.tsx | 97 + .../src/pages/NotFoundPage.tsx | 20 + UMC-10th-mission-FE/src/pages/SignupPage.tsx | 262 ++ UMC-10th-mission-FE/src/pages/WritePage.tsx | 8 + UMC-10th-mission-FE/src/types/auth.ts | 41 + UMC-10th-mission-FE/src/types/common.ts | 5 + UMC-10th-mission-FE/src/types/lp.ts | 19 + UMC-10th-mission-FE/src/utils/validate.ts | 29 + UMC-10th-mission-FE/src/vite-env.d.ts | 7 + UMC-10th-mission-FE/tsconfig.app.json | 25 + UMC-10th-mission-FE/tsconfig.json | 7 + UMC-10th-mission-FE/tsconfig.node.json | 24 + UMC-10th-mission-FE/vite.config.ts | 8 + 48 files changed, 4151 insertions(+) create mode 100644 UMC-10th-mission-FE/.env create mode 100644 UMC-10th-mission-FE/.gitignore create mode 100644 UMC-10th-mission-FE/README.md create mode 100644 UMC-10th-mission-FE/eslint.config.js create mode 100644 UMC-10th-mission-FE/index.html create mode 100644 UMC-10th-mission-FE/package.json create mode 100644 UMC-10th-mission-FE/pnpm-lock.yaml create mode 100644 UMC-10th-mission-FE/public/favicon.svg create mode 100644 UMC-10th-mission-FE/public/icons.svg create mode 100644 UMC-10th-mission-FE/src/App.css create mode 100644 UMC-10th-mission-FE/src/App.tsx create mode 100644 UMC-10th-mission-FE/src/apis/auth.ts create mode 100644 UMC-10th-mission-FE/src/apis/axios.ts create mode 100644 UMC-10th-mission-FE/src/apis/lp.ts create mode 100644 UMC-10th-mission-FE/src/assets/hero.png create mode 100644 UMC-10th-mission-FE/src/assets/react.svg create mode 100644 UMC-10th-mission-FE/src/assets/vite.svg create mode 100644 UMC-10th-mission-FE/src/constants/key.ts create mode 100644 UMC-10th-mission-FE/src/context/AuthContext.tsx create mode 100644 UMC-10th-mission-FE/src/hooks/useForm.ts create mode 100644 UMC-10th-mission-FE/src/hooks/useGetLPDetail.ts create mode 100644 UMC-10th-mission-FE/src/hooks/useGetLpList.ts create mode 100644 UMC-10th-mission-FE/src/hooks/useGetMyInfo.ts create mode 100644 UMC-10th-mission-FE/src/hooks/useLocalStorage.ts create mode 100644 UMC-10th-mission-FE/src/imgs/google.png create mode 100644 UMC-10th-mission-FE/src/index.css create mode 100644 UMC-10th-mission-FE/src/layouts/HomeLayout.tsx create mode 100644 UMC-10th-mission-FE/src/layouts/PrivateLayout.tsx create mode 100644 UMC-10th-mission-FE/src/layouts/ProtectedLayout.tsx create mode 100644 UMC-10th-mission-FE/src/main.tsx create mode 100644 UMC-10th-mission-FE/src/pages/GoogleLoginRedirectPage.tsx create mode 100644 UMC-10th-mission-FE/src/pages/HomePage.tsx create mode 100644 UMC-10th-mission-FE/src/pages/LPDetailPage.tsx create mode 100644 UMC-10th-mission-FE/src/pages/LPListPage.tsx create mode 100644 UMC-10th-mission-FE/src/pages/LoginPage.tsx create mode 100644 UMC-10th-mission-FE/src/pages/MyPage.tsx create mode 100644 UMC-10th-mission-FE/src/pages/NotFoundPage.tsx create mode 100644 UMC-10th-mission-FE/src/pages/SignupPage.tsx create mode 100644 UMC-10th-mission-FE/src/pages/WritePage.tsx create mode 100644 UMC-10th-mission-FE/src/types/auth.ts create mode 100644 UMC-10th-mission-FE/src/types/common.ts create mode 100644 UMC-10th-mission-FE/src/types/lp.ts create mode 100644 UMC-10th-mission-FE/src/utils/validate.ts create mode 100644 UMC-10th-mission-FE/src/vite-env.d.ts create mode 100644 UMC-10th-mission-FE/tsconfig.app.json create mode 100644 UMC-10th-mission-FE/tsconfig.json create mode 100644 UMC-10th-mission-FE/tsconfig.node.json create mode 100644 UMC-10th-mission-FE/vite.config.ts diff --git a/UMC-10th-mission-FE/.env b/UMC-10th-mission-FE/.env new file mode 100644 index 00000000..286cd470 --- /dev/null +++ b/UMC-10th-mission-FE/.env @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://localhost:8000 + diff --git a/UMC-10th-mission-FE/.gitignore b/UMC-10th-mission-FE/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/UMC-10th-mission-FE/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/UMC-10th-mission-FE/README.md b/UMC-10th-mission-FE/README.md new file mode 100644 index 00000000..7dbf7ebf --- /dev/null +++ b/UMC-10th-mission-FE/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/UMC-10th-mission-FE/eslint.config.js b/UMC-10th-mission-FE/eslint.config.js new file mode 100644 index 00000000..ef614d25 --- /dev/null +++ b/UMC-10th-mission-FE/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/UMC-10th-mission-FE/index.html b/UMC-10th-mission-FE/index.html new file mode 100644 index 00000000..2bce0e63 --- /dev/null +++ b/UMC-10th-mission-FE/index.html @@ -0,0 +1,13 @@ + + + + + + + umc + + +
      + + + diff --git a/UMC-10th-mission-FE/package.json b/UMC-10th-mission-FE/package.json new file mode 100644 index 00000000..49292894 --- /dev/null +++ b/UMC-10th-mission-FE/package.json @@ -0,0 +1,45 @@ +{ + "name": "umc", + "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", + "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-router": "^7.15.0", + "react-router-dom": "^7.15.0", + "styled-components": "^6.4.1", + "zod": "^4.4.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tanstack/react-query-devtools": "^5.100.9", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/styled-components": "^5.1.36", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "postcss": "^8.5.14", + "tailwindcss": "^4.2.4", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10" + } +} diff --git a/UMC-10th-mission-FE/pnpm-lock.yaml b/UMC-10th-mission-FE/pnpm-lock.yaml new file mode 100644 index 00000000..2e6cd75e --- /dev/null +++ b/UMC-10th-mission-FE/pnpm-lock.yaml @@ -0,0 +1,2365 @@ +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.6)) + '@tailwindcss/vite': + specifier: ^4.2.4 + version: 4.2.4(vite@8.0.11(@types/node@24.12.3)(jiti@2.7.0)) + '@tanstack/react-query': + specifier: ^5.100.9 + version: 5.100.9(react@19.2.6) + axios: + specifier: ^1.16.0 + version: 1.16.0 + lucide-react: + specifier: ^1.14.0 + version: 1.14.0(react@19.2.6) + react: + specifier: ^19.2.5 + version: 19.2.6 + react-dom: + specifier: ^19.2.5 + version: 19.2.6(react@19.2.6) + react-hook-form: + specifier: ^7.75.0 + version: 7.75.0(react@19.2.6) + react-router: + specifier: ^7.15.0 + version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router-dom: + specifier: ^7.15.0 + version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + styled-components: + specifier: ^6.4.1 + version: 6.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + 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)) + '@tanstack/react-query-devtools': + specifier: ^5.100.9 + version: 5.100.9(@tanstack/react-query@5.100.9(react@19.2.6))(react@19.2.6) + '@types/node': + specifier: ^24.12.2 + version: 24.12.3 + '@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) + '@types/styled-components': + specifier: ^5.1.36 + version: 5.1.36 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.11(@types/node@24.12.3)(jiti@2.7.0)) + autoprefixer: + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.14) + 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 + postcss: + specifier: ^8.5.14 + version: 8.5.14 + tailwindcss: + specifier: ^4.2.4 + version: 4.2.4 + 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.11(@types/node@24.12.3)(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==} + + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@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.128.0': + resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + + '@rolldown/binding-android-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.18': + resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.18': + resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} + + '@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.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.12.3': + resolution: {integrity: sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ==} + + '@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==} + + '@types/styled-components@5.1.36': + resolution: {integrity: sha512-pGMRNY5G2rNDKEv2DOiFYa7Ft1r0jrhmgBwHhOMzPTgCjO76bCot0/4uEfqj7K0Jf1KdQmDtAuaDk9EAs9foSw==} + + '@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==} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + 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.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + 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.352: + resolution: {integrity: sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==} + + enhanced-resolve@5.21.1: + resolution: {integrity: sha512-8p7DUVq6XJnZEz9W4oSwiwycxBIjHjRzYb3Je3zVN+geKTRQKzAkR/K4PBExlS0090d9nshak6phMUxr3PDjmQ==} + 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'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + 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==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + 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-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + 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.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + 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-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + 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.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + rolldown@1.0.0-rc.18: + resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} + 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'} + + styled-components@6.4.1: + resolution: {integrity: sha512-ADu2dF53esUzzM4I0ewxhxFtsDd6v4V6dNkg3vG0iFKhnt06sJneTZnRvujAosZwW0XD58IKgGMQoqri4wHRqg==} + engines: {node: '>= 16'} + peerDependencies: + css-to-react-native: '>= 3.2.0' + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + react-native: '>= 0.68.0' + peerDependenciesMeta: + css-to-react-native: + optional: true + react-dom: + optional: true + react-native: + optional: true + + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + 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.11: + resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + 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 + + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@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.6))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.75.0(react@19.2.6) + + '@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.128.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + 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.18': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.18': {} + + '@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.1 + 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.11(@types/node@24.12.3)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.2.4 + '@tailwindcss/oxide': 4.2.4 + tailwindcss: 4.2.4 + vite: 8.0.11(@types/node@24.12.3)(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.6))(react@19.2.6)': + dependencies: + '@tanstack/query-devtools': 5.100.9 + '@tanstack/react-query': 5.100.9(react@19.2.6) + react: 19.2.6 + + '@tanstack/react-query@5.100.9(react@19.2.6)': + dependencies: + '@tanstack/query-core': 5.100.9 + react: 19.2.6 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + hoist-non-react-statics: 3.3.2 + + '@types/json-schema@7.0.15': {} + + '@types/node@24.12.3': + 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 + + '@types/styled-components@5.1.36': + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.14) + '@types/react': 19.2.14 + 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.11(@types/node@24.12.3)(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.11(@types/node@24.12.3)(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: {} + + autoprefixer@10.5.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001792 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-value-parser: 4.2.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.6: + 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.352 + 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.352: {} + + enhanced-resolve@5.21.1: + 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.9 + 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.9 + 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 + + fraction.js@5.3.4: {} + + 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 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.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.6): + dependencies: + react: 19.2.6 + + 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.6 + + 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-value-parser@4.2.0: {} + + 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.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react-hook-form@7.75.0(react@19.2.6): + dependencies: + react: 19.2.6 + + react-is@16.13.1: {} + + react-router-dom@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-router: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + + react-router@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + cookie: 1.1.1 + react: 19.2.6 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + + react@19.2.6: {} + + rolldown@1.0.0-rc.18: + dependencies: + '@oxc-project/types': 0.128.0 + '@rolldown/pluginutils': 1.0.0-rc.18 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.18 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.18 + '@rolldown/binding-darwin-x64': 1.0.0-rc.18 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.18 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + + 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: {} + + styled-components@6.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@emotion/is-prop-valid': 1.4.0 + csstype: 3.2.3 + react: 19.2.6 + stylis: 4.3.6 + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + + stylis@4.3.6: {} + + 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.11(@types/node@24.12.3)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.0-rc.18 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.3 + 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/UMC-10th-mission-FE/public/favicon.svg b/UMC-10th-mission-FE/public/favicon.svg new file mode 100644 index 00000000..6893eb13 --- /dev/null +++ b/UMC-10th-mission-FE/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UMC-10th-mission-FE/public/icons.svg b/UMC-10th-mission-FE/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/UMC-10th-mission-FE/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UMC-10th-mission-FE/src/App.css b/UMC-10th-mission-FE/src/App.css new file mode 100644 index 00000000..af499fbc --- /dev/null +++ b/UMC-10th-mission-FE/src/App.css @@ -0,0 +1,60 @@ +.login-container { + width: 100%; + max-width: 400px; + padding: 20px; + display: flex; + flex-direction: column; +} + +.login-header { + display: flex; + align-items: center; + margin-bottom: 30px; +} + +.back-button { + background: none; + border: none; + color: white; + font-size: 24px; + cursor: pointer; + margin-right: 10px; +} + +input { + width: 100%; + padding: 15px; + margin-bottom: 10px; + border-radius: 8px; + border: 1px solid #333; + background-color: white; + box-sizing: border-box; +} + +.error-input { + border: 2px solid #ff4d4d; +} + +.error-msg { + color: #ff4d4d; + font-size: 12px; + margin-top: -5px; + margin-bottom: 10px; +} + +.login-submit-btn { + width: 100%; + padding: 15px; + border-radius: 8px; + border: none; + background-color: #444; /* 비활성화 상태 */ + color: white; + font-weight: bold; + cursor: not-allowed; +} + +/* 유효성 검사 통과 시 활성화 (이미지 속 핑크색) */ +.login-submit-btn.active { + background-color: #ff2d78; + cursor: pointer; +} \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/App.tsx b/UMC-10th-mission-FE/src/App.tsx new file mode 100644 index 00000000..bb80ed3e --- /dev/null +++ b/UMC-10th-mission-FE/src/App.tsx @@ -0,0 +1,79 @@ +// src/App.tsx +import "./App.css"; +import { + createBrowserRouter, + RouterProvider, + type RouteObject, + Outlet, +} from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; // 👈 추가! +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // 👈 추가! + +import Home from "./pages/HomePage"; +import NotFoundPage from "./pages/NotFoundPage"; +import LoginPage from "./pages/LoginPage"; +import HomeLayout from "./layouts/HomeLayout"; +import SignUpPage from "./pages/SignupPage"; +import MyPage from "./pages/MyPage"; +import { AuthProvider } from "./context/AuthContext"; +import PrivateLayout from "./layouts/PrivateLayout"; +import GoogleLoginRedirectPage from "./pages/GoogleLoginRedirectPage"; +import LPListPage from "./pages/LPListPage"; // 👈 목록 페이지 임포트 +import LPDetailPage from "./pages/LPDetailPage"; // 👈 (나중에 만들) 상세 페이지 임포트 +import WritePage from "./pages/WritePage"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + }, + }, +}); + +const AppRoot = () => { + return ( + + + + + {import.meta.env.DEV && } + + ); +}; + +const routes: RouteObject[] = [ + { + path: "/", + element: , + errorElement: , + children: [ + { + path: "/", + element: , + children: [ + { index: true, element: }, + { path: "login", element: }, + { path: "signup", element: }, + { path: "v1/auth/google/callback", element: }, + { path: "lps", element: }, + { path: "lp/:lpid", element: }, + { + element: , + children: [ + { path: "mypage", element: }, + { path: "write", element: }, + ], + }, + ], + }, + ], + }, +]; + +const router = createBrowserRouter(routes); + +function App() { + return ; +} + +export default App; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/apis/auth.ts b/UMC-10th-mission-FE/src/apis/auth.ts new file mode 100644 index 00000000..31587a41 --- /dev/null +++ b/UMC-10th-mission-FE/src/apis/auth.ts @@ -0,0 +1,35 @@ +import axios from "axios"; +import type { + ReqSignInDto, + ReqSignUpDto, + ResMyInfoDto, + ResSignInDto, + ResSignUpDto, +} from "../types/auth"; +import { axiosInstance } from "./axios"; + +const BASE_URL = import.meta.env.VITE_API_BASE_URL; + +// 회원가입 (토큰 불필요 → 기본 axios) +export const postSignup = async (body: ReqSignUpDto): Promise => { + const { data } = await axios.post(`${BASE_URL}/v1/auth/signup`, body); + return data; +}; + +// 로그인 (토큰 불필요 → 기본 axios) +export const postSignin = async (body: ReqSignInDto): Promise => { + const { data } = await axios.post(`${BASE_URL}/v1/auth/signin`, body); + return data; +}; + +// 내 정보 조회 (토큰 필요 → axiosInstance 사용, Hook 규칙 위반 제거) +export const getMyInfo = async (): Promise => { + const { data } = await axiosInstance.get("/v1/users/me"); + return data; +}; + +// 로그아웃 (서버에 알림, 토큰 필요할 수 있음 → axiosInstance) +export const postLogout = async () => { + const { data } = await axiosInstance.post("/v1/auth/signout"); + return data; +}; diff --git a/UMC-10th-mission-FE/src/apis/axios.ts b/UMC-10th-mission-FE/src/apis/axios.ts new file mode 100644 index 00000000..876bbce5 --- /dev/null +++ b/UMC-10th-mission-FE/src/apis/axios.ts @@ -0,0 +1,73 @@ +import axios, { type InternalAxiosRequestConfig } from "axios"; +import { LOCAL_STORAGE_KEY } from "../constants/key"; + +interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { + _retry?: boolean; +} + +let refreshPromise: Promise | null = null; + +// 환경변수를 하나로 통일 (VITE_API_BASE_URL) +const BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export const axiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || "http://localhost:8080", +}); + +// 요청 인터셉터: 모든 요청에 액세스 토큰 자동 첨부 +axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (err) => Promise.reject(err) +); + +// 응답 인터셉터: 401 발생 시 토큰 자동 갱신 +axiosInstance.interceptors.response.use( + (res) => res, + async (err) => { + const originalRequest = err.config as CustomAxiosRequestConfig; + + if ( + err.response?.status === 401 && + !originalRequest._retry && + !originalRequest.url?.includes("/v1/auth/refresh") + ) { + originalRequest._retry = true; + + if (!refreshPromise) { + refreshPromise = axios + .post(`${BASE_URL}/v1/auth/refresh`, { + refreshToken: localStorage.getItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN), + }) + .then(({ data }) => { + const { accessToken: newAccess, refreshToken: newRefresh } = + data.data; + localStorage.setItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN, newAccess); + localStorage.setItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN, newRefresh); + return newAccess; + }) + .catch((refreshErr) => { + localStorage.removeItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN); + localStorage.removeItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN); + window.location.href = "/login"; + return Promise.reject(refreshErr); + }) + .finally(() => { + refreshPromise = null; + }); + } + + return refreshPromise.then((newAccessToken) => { + originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; + return axiosInstance.request(originalRequest); + }); + } + + return Promise.reject(err); + } +); diff --git a/UMC-10th-mission-FE/src/apis/lp.ts b/UMC-10th-mission-FE/src/apis/lp.ts new file mode 100644 index 00000000..83332ad9 --- /dev/null +++ b/UMC-10th-mission-FE/src/apis/lp.ts @@ -0,0 +1,17 @@ +import { axiosInstance } from "./axios"; +import type { GetLpsResponse } from "../types/lp"; + +// 이름을 getLps로 통일 (query hook과 일치) +export const getLps = async ( + sort: "latest" | "oldest" = "latest" +): Promise => { + const { data } = await axiosInstance.get("/v1/lps", { + params: { sort }, + }); + return data; +}; + +export const getLpDetail = async (id: number) => { + const { data } = await axiosInstance.get(`/v1/lps/${id}`); + return data; +}; diff --git a/UMC-10th-mission-FE/src/assets/hero.png b/UMC-10th-mission-FE/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/UMC-10th-mission-FE/src/assets/react.svg b/UMC-10th-mission-FE/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/UMC-10th-mission-FE/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/assets/vite.svg b/UMC-10th-mission-FE/src/assets/vite.svg new file mode 100644 index 00000000..5101b674 --- /dev/null +++ b/UMC-10th-mission-FE/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/UMC-10th-mission-FE/src/constants/key.ts b/UMC-10th-mission-FE/src/constants/key.ts new file mode 100644 index 00000000..93fadd58 --- /dev/null +++ b/UMC-10th-mission-FE/src/constants/key.ts @@ -0,0 +1,4 @@ +export const LOCAL_STORAGE_KEY = { + ACCESS_TOKEN: "accessToken", + REFRESH_TOKEN: "refreshToken", +} as const; diff --git a/UMC-10th-mission-FE/src/context/AuthContext.tsx b/UMC-10th-mission-FE/src/context/AuthContext.tsx new file mode 100644 index 00000000..560f616b --- /dev/null +++ b/UMC-10th-mission-FE/src/context/AuthContext.tsx @@ -0,0 +1,72 @@ +import { + createContext, + useState, + useContext, + type PropsWithChildren, +} from "react"; +import { postSignin, postLogout } from "../apis/auth"; +import { LOCAL_STORAGE_KEY } from "../constants/key"; +import { useLocalStorage } from "../hooks/useLocalStorage"; +import type { ReqSignInDto } from "../types/auth"; + +interface AuthContextType { + accessToken: string | null; + login: (signInData: ReqSignInDto) => Promise; + logout: () => Promise; +} + +const AuthContext = createContext(null); + +export const AuthProvider = ({ children }: PropsWithChildren) => { + const { + getItem: getAccessToken, + setItem: setAccessTokenStorage, + removeItem: removeAccessToken, + } = useLocalStorage(LOCAL_STORAGE_KEY.ACCESS_TOKEN); + + const { + setItem: setRefreshTokenStorage, + removeItem: removeRefreshToken, + } = useLocalStorage(LOCAL_STORAGE_KEY.REFRESH_TOKEN); + + const [accessToken, setAccessToken] = useState( + getAccessToken() + ); + + const login = async (signInData: ReqSignInDto) => { + // 에러를 호출부로 전파하여 UI에서 처리할 수 있도록 try/catch 제거 + const response = await postSignin(signInData); + const { accessToken: newAccess, refreshToken: newRefresh } = response.data; + + setAccessTokenStorage(newAccess); + setRefreshTokenStorage(newRefresh); + setAccessToken(newAccess); + }; + + const logout = async () => { + try { + await postLogout(); + } catch (err) { + // 서버 로그아웃 실패해도 클라이언트는 정리 + console.error("서버 로그아웃 실패:", err); + } finally { + removeAccessToken(); + removeRefreshToken(); + setAccessToken(null); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth는 AuthProvider 안에서만 사용할 수 있습니다."); + } + return context; +}; diff --git a/UMC-10th-mission-FE/src/hooks/useForm.ts b/UMC-10th-mission-FE/src/hooks/useForm.ts new file mode 100644 index 00000000..56eda62a --- /dev/null +++ b/UMC-10th-mission-FE/src/hooks/useForm.ts @@ -0,0 +1,43 @@ +import { useEffect, useState, type ChangeEvent } from "react"; + +interface UseFormProps { + init_val: T; + validate: (values: T) => Record; +} + +function useForm>({ + init_val, + validate, +}: UseFormProps) { + const [values, setValues] = useState(init_val); + const [touched, setTouched] = useState>({}); + const [errors, setErrors] = useState>({}); + + const handleChange = (name: keyof T, text: string) => { + setValues((prev) => ({ ...prev, [name]: text })); + }; + + const handleBlur = (name: keyof T) => { + setTouched((prev) => ({ ...prev, [name]: true })); + }; + + const getInputProps = (name: keyof T) => { + return { + value: values[name] as string, + onChange: (e: ChangeEvent) => { + handleChange(name, e.target.value); + }, + onBlur: () => { + handleBlur(name); + }, + }; + }; + + useEffect(() => { + setErrors(validate(values)); + }, [values]); // validate를 deps에서 제거 - 매 렌더마다 새 함수 참조로 무한루프 방지 + + return { values, errors, touched, getInputProps }; +} + +export default useForm; diff --git a/UMC-10th-mission-FE/src/hooks/useGetLPDetail.ts b/UMC-10th-mission-FE/src/hooks/useGetLPDetail.ts new file mode 100644 index 00000000..a534e33f --- /dev/null +++ b/UMC-10th-mission-FE/src/hooks/useGetLPDetail.ts @@ -0,0 +1,13 @@ +// src/hooks/useGetLpDetail.ts +import { useQuery } from "@tanstack/react-query"; +import { getLpDetail } from "../apis/lp"; // 👈 경로가 맞는지 확인해줘! + +export const useGetLpDetail = (lpid: string | undefined) => { + return useQuery({ + // 💡 퀄리 키에 lpid를 넣어야 "1번 LP"랑 "2번 LP"를 헷갈리지 않아! + queryKey: ["lp", lpid], + queryFn: () => getLpDetail(Number(lpid)), + enabled: !!lpid, // lpid가 있을 때만 비서가 출발하도록 설정! + staleTime: 1000 * 60 * 5, // 5분 동안은 신선하게 유지 + }); +}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/hooks/useGetLpList.ts b/UMC-10th-mission-FE/src/hooks/useGetLpList.ts new file mode 100644 index 00000000..fa1523a7 --- /dev/null +++ b/UMC-10th-mission-FE/src/hooks/useGetLpList.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import { getLps } from "../apis/lp"; + +export const useGetLpList = (sort: "latest" | "oldest" = "latest") => { + return useQuery({ + queryKey: ["lps", sort], + queryFn: () => getLps(sort), + staleTime: 1000 * 60, + gcTime: 1000 * 60 * 5, + }); +}; diff --git a/UMC-10th-mission-FE/src/hooks/useGetMyInfo.ts b/UMC-10th-mission-FE/src/hooks/useGetMyInfo.ts new file mode 100644 index 00000000..4f2b4275 --- /dev/null +++ b/UMC-10th-mission-FE/src/hooks/useGetMyInfo.ts @@ -0,0 +1,12 @@ +// src/hooks/useGetMyInfo.ts +import { useQuery } from "@tanstack/react-query"; +import { getMyInfo } from "../apis/auth"; + +export const useGetMyInfo = (accessToken: string | null) => { + return useQuery({ + queryKey: ["user", "me"], + queryFn: getMyInfo, + enabled: !!accessToken, + staleTime: 1000 * 60 * 60, + }); +}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/hooks/useLocalStorage.ts b/UMC-10th-mission-FE/src/hooks/useLocalStorage.ts new file mode 100644 index 00000000..b7edc4aa --- /dev/null +++ b/UMC-10th-mission-FE/src/hooks/useLocalStorage.ts @@ -0,0 +1,36 @@ +export const useLocalStorage = (key: string) => { + const setItem = (value: unknown) => { + try { + const valueToStore = + typeof value === "string" ? value : JSON.stringify(value); + localStorage.setItem(key, valueToStore); + } catch (err) { + console.error(err); + } + }; + + const getItem = (): string | null => { + try { + const item = localStorage.getItem(key); + if (!item) return null; + try { + return JSON.parse(item); + } catch { + return item; + } + } catch (err) { + console.error(err); + return null; + } + }; + + const removeItem = () => { + try { + localStorage.removeItem(key); + } catch (err) { + console.error(err); + } + }; + + return { setItem, getItem, removeItem }; +}; diff --git a/UMC-10th-mission-FE/src/imgs/google.png b/UMC-10th-mission-FE/src/imgs/google.png new file mode 100644 index 0000000000000000000000000000000000000000..f1afa00e3a939d4442f2aefd825e6c3c01c10cc9 GIT binary patch literal 6624 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 qAi% { + await logout(); + navigate("/"); + }; + + return ( +
      + + {/* --- 상단 네비게이션 헤더 --- */} + + +
      + {/* --- 사이드바 (기존 로직 그대로 유지!) --- */} + {isSidebarOpen && ( +
      setIsSidebarOpen(false)} /> + )} + + + {/* --- 메인 콘텐츠 --- */} +
      + +
      +
      + + {/* --- 우측 하단 플로팅 버튼 (+) --- */} + + +
      + ); +} \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/layouts/PrivateLayout.tsx b/UMC-10th-mission-FE/src/layouts/PrivateLayout.tsx new file mode 100644 index 00000000..5d44360e --- /dev/null +++ b/UMC-10th-mission-FE/src/layouts/PrivateLayout.tsx @@ -0,0 +1,14 @@ +import { Navigate, Outlet } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; + +const PrivateLayout = () => { + const { accessToken } = useAuth(); + + if (!accessToken) { + return ; + } + + return ; +}; + +export default PrivateLayout; diff --git a/UMC-10th-mission-FE/src/layouts/ProtectedLayout.tsx b/UMC-10th-mission-FE/src/layouts/ProtectedLayout.tsx new file mode 100644 index 00000000..dfd03564 --- /dev/null +++ b/UMC-10th-mission-FE/src/layouts/ProtectedLayout.tsx @@ -0,0 +1,13 @@ +import { useAuth } from "../context/AuthContext"; +import { Navigate, Outlet } from "react-router-dom"; + +export const ProtectedLayout = () => { + const { accessToken } = useAuth(); // AuthContext에서 토큰 상태 구독 [cite: 116] + + if (!accessToken) { + alert("로그인이 필요한 서비스입니다."); // 사용자 경험 고려 [cite: 120] + return ; // 리다이렉트 처리 [cite: 110, 119] + } + + return ; // 인증된 경우 하위 경로(LPListPage 등) 렌더링 +}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/main.tsx b/UMC-10th-mission-FE/src/main.tsx new file mode 100644 index 00000000..3d4bdea4 --- /dev/null +++ b/UMC-10th-mission-FE/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( + + + , +) \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/pages/GoogleLoginRedirectPage.tsx b/UMC-10th-mission-FE/src/pages/GoogleLoginRedirectPage.tsx new file mode 100644 index 00000000..94407e7a --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/GoogleLoginRedirectPage.tsx @@ -0,0 +1,38 @@ +import { useEffect } from "react"; +import { useLocalStorage } from "../hooks/useLocalStorage"; +import { LOCAL_STORAGE_KEY } from "../constants/key"; +import { useNavigate } from "react-router-dom"; + +const GoogleLoginRedirectPage = () => { + const { setItem: setAccessToken } = useLocalStorage( + LOCAL_STORAGE_KEY.ACCESS_TOKEN + ); + const { setItem: setRefreshToken } = useLocalStorage( + LOCAL_STORAGE_KEY.REFRESH_TOKEN + ); + const navigate = useNavigate(); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const accessToken = params.get("accessToken"); + const refreshToken = params.get("refreshToken"); + + if (accessToken) { + setAccessToken(accessToken); + if (refreshToken) setRefreshToken(refreshToken); + // window.location.href 대신 navigate 사용 (SPA 방식 유지) + navigate("/mypage", { replace: true }); + } else { + // 토큰이 없으면 로그인 페이지로 + navigate("/login", { replace: true }); + } + }, []); + + return ( +
      +

      로그인 처리 중...

      +
      + ); +}; + +export default GoogleLoginRedirectPage; diff --git a/UMC-10th-mission-FE/src/pages/HomePage.tsx b/UMC-10th-mission-FE/src/pages/HomePage.tsx new file mode 100644 index 00000000..e16165c4 --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/HomePage.tsx @@ -0,0 +1,28 @@ +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; + +const HomePage = () => { + const navigate = useNavigate(); + const { accessToken } = useAuth(); + + return ( +
      +

      + 돌려돌려LP판 +

      +

      당신의 LP 컬렉션을 관리하세요

      + {!accessToken && ( +
      + +
      + )} +
      + ); +}; + +export default HomePage; diff --git a/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx b/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx new file mode 100644 index 00000000..53297dc7 --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx @@ -0,0 +1,112 @@ +// src/pages/LPDetailPage.tsx +import { useEffect } from "react"; +import { useParams, useNavigate, useLocation } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { useGetLpDetail } from "../hooks/useGetLPDetail"; + +const LPDetailPage = () => { + const { lpid } = useParams(); // URL에서 lpid 뽑아오기 + const navigate = useNavigate(); + const location = useLocation(); + const { accessToken } = useAuth(); + + // 🛡️ [체크리스트] 비로그인 사용자 차단! + useEffect(() => { + if (!accessToken) { + // 1. 브라우저 기본 경고창(모달 역할) 띄우기 + alert("로그인이 필요한 서비스입니다. 로그인 페이지로 이동합니다! 🚨"); + + // 2. 로그인 창으로 보내면서, state라는 '비밀 쪽지'에 원래 주소(from)를 적어서 보냄! + navigate("/login", { + state: { from: location.pathname }, // 👈 핵심 포인트! + replace: true + }); + } + }, [accessToken, navigate, location]); // deps에 location 추가 + + // 비서(useQuery) 출동! + const { data: response, isPending, isError, refetch } = useGetLpDetail(lpid); + + // 토큰이 없어서 쫓겨나는 중이면 에러나 빈화면 방지 + if (!accessToken) return null; + + // 1️⃣ 에러 났을 때 + if (isError) { + return ( +
      +

      데이터를 불러오는데 실패했습니다 😭

      + +
      + ); + } + + // 2️⃣ 로딩 중일 때 (Skeleton UI) + if (isPending) { + return ( +
      +
      +
      +
      +
      +
      +
      +
      + ); + } + + // 3️⃣ 데이터 도착! (성공) + const lp = response?.data; // 서버 데이터 구조에 따라 수정될 수 있음! + + return ( +
      +
      + + {/* 썸네일 이미지 */} + {lp?.thumbnail ? ( + {lp.title} + ) : ( +
      + 이미지가 없습니다 +
      + )} + + {/* 메타 정보 (제목, 업로드일) */} +
      +

      {lp?.title || "제목 없음"}

      +
      + 📅 {lp?.createdAt ? new Date(lp.createdAt).toLocaleDateString() : "업로드일 모름"} + ❤️ 좋아요 {lp?.likes || 0}개 + {lp?.artist && 🎤 아티스트: {lp.artist}} +
      +
      + +
      + + {/* 본문 (Content) */} +
      + {lp?.content || "본문 내용이 없습니다."} +
      + + {/* 액션 버튼들 (좋아요 / 수정 / 삭제) */} +
      + + + +
      + +
      +
      + ); +}; + +export default LPDetailPage; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/pages/LPListPage.tsx b/UMC-10th-mission-FE/src/pages/LPListPage.tsx new file mode 100644 index 00000000..263cf12a --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/LPListPage.tsx @@ -0,0 +1,104 @@ +// src/pages/LPListPage.tsx +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useGetLpList } from "../hooks/useGetLpList"; // 👈 비서 훅 임포트 +import type { Lp } from "../types/lp"; // 👈 [수정] 팝콘이 만든 진짜 타입 불러오기! + +interface LpItem { + id: string; + thumbnail?: string; + title: string; +} + +const LPListPage = () => { + const navigate = useNavigate(); + // 정렬 상태 관리 ('latest' 또는 'oldest') + const [sort, setSort] = useState<"latest" | "oldest">("latest"); + + // 비서 호출! (데이터, 로딩상태, 에러상태, 다시불러오기 함수 받기) + const { data: response, isPending, isError, refetch } = useGetLpList(sort); + + const lpList: Lp[] = response?.data?.data ?? []; + + // 1. 에러 났을 때 보여줄 UI + if (isError) { + return ( +
      +

      데이터를 불러오는데 실패했습니다 😭

      + +
      + ); + } + + return ( +
      +
      +

      나의 LP 보관함

      + {/* 정렬 토글 버튼 */} + +
      + + {/* 2. 로딩 중일 때 보여줄 Skeleton UI */} + {isPending ? ( +
      + {[1, 2, 3, 4].map((n) => ( +
      + ))} +
      + ) : lpList.length === 0 ? ( +

      보관된 LP가 없습니다.

      + ) : ( +
      + {lpList.map((lp) => ( + // 1️⃣ 라우팅 연결 (onClick) & 카드 확대 (hover:scale-105) + // 💡 Tailwind의 'group' 클래스를 넣어야 자식 요소가 마우스를 인식해! +
      navigate(`/lp/${lp.id}`)} + className="group border border-[#FF1493] rounded-xl p-4 bg-[#111] flex flex-col cursor-pointer transition-transform duration-300 hover:scale-105" + > + {/* 썸네일 영역 (여기에 오버레이를 덮을 거야) */} +
      + {lp.thumbnail ? ( + {lp.title} + ) : ( +
      + No Image +
      + )} + + {/* 2️⃣ 호버 오버레이 (메타 정보 노출) */} + {/* 💡 기본은 투명(opacity-0)인데, 부모(group)에 마우스가 올라가면 짠!(group-hover:opacity-100) */} +
      +

      + {lp.title} +

      +

      + 📅 {lp.createdAt ? new Date(lp.createdAt).toLocaleDateString() : "날짜 모름"} +

      +

      + ❤️ 좋아요 {lp.likes || 0}개 +

      +
      +
      + + {/* 카드 하단에 항상 보이는 기본 제목 */} +

      {lp.title}

      +
      + ))} +
      + )} +
      + ); +}; + +export default LPListPage; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/pages/LoginPage.tsx b/UMC-10th-mission-FE/src/pages/LoginPage.tsx new file mode 100644 index 00000000..760a5fc4 --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/LoginPage.tsx @@ -0,0 +1,127 @@ +import { useNavigate, useLocation } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import useForm from "../hooks/useForm"; +import { validateSignin } from "../utils/validate"; +import { ChevronLeft } from "lucide-react"; +import { useState } from "react"; + +const LoginPage = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { login } = useAuth(); + const [serverError, setServerError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const { values, errors, touched, getInputProps } = useForm({ + init_val: { email: "", password: "" }, + validate: validateSignin, + }); + + const isFormValid = + !errors.email && + !errors.password && + values.email !== "" && + values.password !== ""; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!isFormValid || isLoading) return; + + setIsLoading(true); + setServerError(""); + + try { + await login(values); + const from = location.state?.from || "/mypage"; + navigate(from, { replace: true }); + } catch (err: any) { + const msg = + err.response?.data?.message || "로그인 중 오류가 발생했습니다."; + setServerError(msg); + } finally { + setIsLoading(false); + } + }; + + return ( +
      + {/* 헤더 */} +
      + +

      로그인

      +
      + +
      + {/* 이메일 */} +
      + + {touched.email && errors.email && ( + {errors.email} + )} +
      + + {/* 비밀번호 */} +
      + + {touched.password && errors.password && ( + + {errors.password} + + )} +
      + + {/* 서버 에러 */} + {serverError && ( +

      {serverError}

      + )} + + + +

      + 계정이 없으신가요?{" "} + navigate("/signup")} + className="text-[#FF1493] cursor-pointer hover:underline" + > + 회원가입 + +

      +
      +
      + ); +}; + +export default LoginPage; diff --git a/UMC-10th-mission-FE/src/pages/MyPage.tsx b/UMC-10th-mission-FE/src/pages/MyPage.tsx new file mode 100644 index 00000000..7d0f59b8 --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/MyPage.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react"; +import { axiosInstance } from "../apis/axios"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import type { ResMyInfoDto } from "../types/auth"; + +const MyPage = () => { + const [userInfo, setUserInfo] = useState(null); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + const { logout } = useAuth(); + + useEffect(() => { + const fetchUserInfo = async () => { + try { + const response = await axiosInstance.get("/v1/users/me"); + setUserInfo(response.data.data); + } catch (error) { + console.error("유저 정보를 가져오는데 실패했습니다.", error); + } finally { + setLoading(false); + } + }; + fetchUserInfo(); + }, []); + + const handleLogout = async () => { + await logout(); + navigate("/", { replace: true }); + }; + + if (loading) { + return ( +
      +
      + 사용자 정보 확인 중... +
      +
      + ); + } + + return ( +
      +

      마이페이지

      + + {userInfo ? ( +
      + {/* 아바타 */} +
      + {userInfo.avatar ? ( + avatar + ) : ( +
      + {userInfo.name?.charAt(0).toUpperCase()} +
      + )} +
      +

      {userInfo.name}

      +

      {userInfo.email}

      +
      +
      + + {userInfo.bio && ( +

      + {userInfo.bio} +

      + )} + + +
      + ) : ( +
      +

      + 사용자 정보를 불러올 수 없습니다. +

      + +
      + )} +
      + ); +}; + +export default MyPage; diff --git a/UMC-10th-mission-FE/src/pages/NotFoundPage.tsx b/UMC-10th-mission-FE/src/pages/NotFoundPage.tsx new file mode 100644 index 00000000..39f8c70e --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/NotFoundPage.tsx @@ -0,0 +1,20 @@ +import { useNavigate } from "react-router-dom"; + +const NotFoundPage = () => { + const navigate = useNavigate(); + + return ( +
      +

      404

      +

      페이지를 찾을 수 없습니다.

      + +
      + ); +}; + +export default NotFoundPage; diff --git a/UMC-10th-mission-FE/src/pages/SignupPage.tsx b/UMC-10th-mission-FE/src/pages/SignupPage.tsx new file mode 100644 index 00000000..28a87c3d --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/SignupPage.tsx @@ -0,0 +1,262 @@ +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 { useNavigate } from "react-router-dom"; +import { Eye, EyeOff, ChevronLeft, User } from "lucide-react"; + +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 valid = await trigger(fields); + if (valid) setStep(nextStep); + }; + + const onSubmit: SubmitHandler = async (data) => { + try { + const { passwordCheck: _, ...rest } = data; + await postSignup(rest); + alert("회원가입이 완료되었습니다!"); + navigate("/login"); + } catch (error: any) { + const msg = + error.response?.data?.message || "회원가입 중 오류가 발생했습니다."; + alert(msg); + } + }; + + const stepBack = () => { + if (step === 1) navigate(-1); + else setStep((prev) => (prev - 1) as 1 | 2 | 3); + }; + + return ( +
      + {/* 헤더 */} +
      + +

      회원가입

      + {/* 스텝 인디케이터 */} +
      + {[1, 2, 3].map((s) => ( +
      + ))} +
      +
      + +
      + {/* Step 1: 이메일 */} + {step === 1 && ( +
      + + + + + + + + 구글로 시작하기 + + +
      +
      + OR +
      +
      + +
      + + {errors.email && ( +

      + {errors.email.message} +

      + )} +
      + + +
      + )} + + {/* Step 2: 비밀번호 */} + {step === 2 && ( +
      +
      +

      이메일

      +

      {emailValue}

      +
      + +
      +
      + + +
      + {errors.password && ( +

      + {errors.password.message} +

      + )} +
      + +
      +
      + + +
      + {errors.passwordCheck && ( +

      + {errors.passwordCheck.message} +

      + )} +
      + + +
      + )} + + {/* Step 3: 닉네임 */} + {step === 3 && ( +
      +
      +

      + 거의 다 왔어요! +

      +

      + 사용하실 닉네임을 설정해주세요. +

      +
      + + {/* 아바타 자리 */} +
      + +
      + +
      + + {errors.name && ( +

      + {errors.name.message} +

      + )} +
      + + +
      + )} +
      +
      + ); +} diff --git a/UMC-10th-mission-FE/src/pages/WritePage.tsx b/UMC-10th-mission-FE/src/pages/WritePage.tsx new file mode 100644 index 00000000..697bfcb3 --- /dev/null +++ b/UMC-10th-mission-FE/src/pages/WritePage.tsx @@ -0,0 +1,8 @@ +const WritePage = () => { + return ( +
      +

      새로운 LP 등록 페이지 (준비 중!)

      +
      + ); +}; +export default WritePage; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/types/auth.ts b/UMC-10th-mission-FE/src/types/auth.ts new file mode 100644 index 00000000..3578d059 --- /dev/null +++ b/UMC-10th-mission-FE/src/types/auth.ts @@ -0,0 +1,41 @@ +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; +}>; diff --git a/UMC-10th-mission-FE/src/types/common.ts b/UMC-10th-mission-FE/src/types/common.ts new file mode 100644 index 00000000..5e306ecd --- /dev/null +++ b/UMC-10th-mission-FE/src/types/common.ts @@ -0,0 +1,5 @@ +export type CommonRes = { + status: boolean; + message: string; + data: T; +}; diff --git a/UMC-10th-mission-FE/src/types/lp.ts b/UMC-10th-mission-FE/src/types/lp.ts new file mode 100644 index 00000000..fc8530a8 --- /dev/null +++ b/UMC-10th-mission-FE/src/types/lp.ts @@ -0,0 +1,19 @@ +// src/types/lp.ts +import type { CommonRes } from "./common"; + +export type Lp = { + id: number; + title: string; // title 속성 추가 + content?: string; + thumbnail?: string; // 또는 imageUrl? 형태 + artist?: string; // LP에 아티스트 정보가 포함될 경우 + createdAt?: string; + updatedAt?: string; + likes?: number; +}; + +export type GetLpsResponse = CommonRes<{ + data: Lp[]; + cursor: number | null; + hasNext: boolean; +}>; diff --git a/UMC-10th-mission-FE/src/utils/validate.ts b/UMC-10th-mission-FE/src/utils/validate.ts new file mode 100644 index 00000000..f1d574ca --- /dev/null +++ b/UMC-10th-mission-FE/src/utils/validate.ts @@ -0,0 +1,29 @@ +export type UserSigninInfo = { + email: string; + password: string; +}; + +const EMAIL_REGEX = + /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i; + +function validateUser(vals: UserSigninInfo) { + const errors: Record = { + email: "", + password: "", + }; + + if (!EMAIL_REGEX.test(vals.email)) { + errors.email = "올바른 이메일 형식이 아닙니다!"; + } + + // 수정: 빈 문자열(length=0)도 잡히도록 >= 8 조건만으로 충분하지 않았음 + if (vals.password.length < 8 || vals.password.length >= 20) { + errors.password = "비밀번호는 8 ~ 20자 사이로 입력해주세요!"; + } + + return errors; +} + +export function validateSignin(vals: UserSigninInfo) { + return validateUser(vals); +} diff --git a/UMC-10th-mission-FE/src/vite-env.d.ts b/UMC-10th-mission-FE/src/vite-env.d.ts new file mode 100644 index 00000000..f59599ab --- /dev/null +++ b/UMC-10th-mission-FE/src/vite-env.d.ts @@ -0,0 +1,7 @@ +interface ImportMetaEnv { + readonly VITE_SERVER_API_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/UMC-10th-mission-FE/tsconfig.app.json b/UMC-10th-mission-FE/tsconfig.app.json new file mode 100644 index 00000000..7f42e5f7 --- /dev/null +++ b/UMC-10th-mission-FE/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/UMC-10th-mission-FE/tsconfig.json b/UMC-10th-mission-FE/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/UMC-10th-mission-FE/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/UMC-10th-mission-FE/tsconfig.node.json b/UMC-10th-mission-FE/tsconfig.node.json new file mode 100644 index 00000000..d3c52ea6 --- /dev/null +++ b/UMC-10th-mission-FE/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/UMC-10th-mission-FE/vite.config.ts b/UMC-10th-mission-FE/vite.config.ts new file mode 100644 index 00000000..c4069b77 --- /dev/null +++ b/UMC-10th-mission-FE/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], +}) From 9b5d8d1401dad1737f11a27c5a2de8b0925cd6b0 Mon Sep 17 00:00:00 2001 From: goeun Date: Sat, 9 May 2026 23:31:14 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[week6/mission2]=20=EB=AC=B4=ED=95=9C?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=B6=80=EB=B6=84=20useInfiniteQ?= =?UTF-8?q?uery=EB=A1=9C=20=EB=AC=B4=ED=95=9C=EC=8A=A4=ED=81=AC=EB=A1=A4?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=ED=95=B4=EB=B3=B4=EA=B8=B0=20+=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UMC-10th-mission-FE/package.json | 1 + UMC-10th-mission-FE/pnpm-lock.yaml | 18 +++ UMC-10th-mission-FE/src/App.tsx | 2 +- UMC-10th-mission-FE/src/apis/lp.ts | 16 +- .../src/components/LpCardSkeleton.tsx | 11 ++ .../src/hooks/useGetLpComments.ts | 17 ++ UMC-10th-mission-FE/src/hooks/useGetLpList.ts | 15 +- .../src/pages/LPDetailPage.tsx | 146 ++++++++++++++---- UMC-10th-mission-FE/src/pages/LPListPage.tsx | 90 +++++------ 9 files changed, 233 insertions(+), 83 deletions(-) create mode 100644 UMC-10th-mission-FE/src/components/LpCardSkeleton.tsx create mode 100644 UMC-10th-mission-FE/src/hooks/useGetLpComments.ts diff --git a/UMC-10th-mission-FE/package.json b/UMC-10th-mission-FE/package.json index 49292894..d73f5d9a 100644 --- a/UMC-10th-mission-FE/package.json +++ b/UMC-10th-mission-FE/package.json @@ -18,6 +18,7 @@ "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", "styled-components": "^6.4.1", diff --git a/UMC-10th-mission-FE/pnpm-lock.yaml b/UMC-10th-mission-FE/pnpm-lock.yaml index 2e6cd75e..8a23b496 100644 --- a/UMC-10th-mission-FE/pnpm-lock.yaml +++ b/UMC-10th-mission-FE/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: react-hook-form: specifier: ^7.75.0 version: 7.75.0(react@19.2.6) + react-intersection-observer: + specifier: ^10.0.3 + version: 10.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-router: specifier: ^7.15.0 version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -1102,6 +1105,15 @@ packages: 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-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2225,6 +2237,12 @@ snapshots: dependencies: react: 19.2.6 + react-intersection-observer@10.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + react-is@16.13.1: {} react-router-dom@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): diff --git a/UMC-10th-mission-FE/src/App.tsx b/UMC-10th-mission-FE/src/App.tsx index bb80ed3e..01a9ec82 100644 --- a/UMC-10th-mission-FE/src/App.tsx +++ b/UMC-10th-mission-FE/src/App.tsx @@ -56,7 +56,7 @@ const routes: RouteObject[] = [ { path: "signup", element: }, { path: "v1/auth/google/callback", element: }, { path: "lps", element: }, - { path: "lp/:lpid", element: }, + { path: "lps/:lpid", element: }, { element: , children: [ diff --git a/UMC-10th-mission-FE/src/apis/lp.ts b/UMC-10th-mission-FE/src/apis/lp.ts index 83332ad9..608044fa 100644 --- a/UMC-10th-mission-FE/src/apis/lp.ts +++ b/UMC-10th-mission-FE/src/apis/lp.ts @@ -3,10 +3,11 @@ import type { GetLpsResponse } from "../types/lp"; // 이름을 getLps로 통일 (query hook과 일치) export const getLps = async ( - sort: "latest" | "oldest" = "latest" + sort: "latest" | "oldest" = "latest", + cursor: number = 0 ): Promise => { const { data } = await axiosInstance.get("/v1/lps", { - params: { sort }, + params: { sort, cursor, limit: 10 }, }); return data; }; @@ -15,3 +16,14 @@ export const getLpDetail = async (id: number) => { const { data } = await axiosInstance.get(`/v1/lps/${id}`); return data; }; + +export const getLpComments = async ( + lpId: number, + order: "latest" | "oldest" = "latest", + cursor: number = 0 +) => { + const { data } = await axiosInstance.get(`/v1/lps/${lpId}/comments`, { + params: { order, cursor, limit: 10 }, + }); + return data; +}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/components/LpCardSkeleton.tsx b/UMC-10th-mission-FE/src/components/LpCardSkeleton.tsx new file mode 100644 index 00000000..7dc2e594 --- /dev/null +++ b/UMC-10th-mission-FE/src/components/LpCardSkeleton.tsx @@ -0,0 +1,11 @@ +// src/components/LpCardSkeleton.tsx +export const LpCardSkeleton = () => { + return ( +
      + {/* 썸네일 뼈대 */} +
      + {/* 제목 뼈대 */} +
      +
      + ); +}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/hooks/useGetLpComments.ts b/UMC-10th-mission-FE/src/hooks/useGetLpComments.ts new file mode 100644 index 00000000..cd56895f --- /dev/null +++ b/UMC-10th-mission-FE/src/hooks/useGetLpComments.ts @@ -0,0 +1,17 @@ +// src/hooks/useGetLpComments.ts +import { useInfiniteQuery } from "@tanstack/react-query"; +import { getLpComments } from "../apis/lp"; + +export const useGetLpComments = (lpId: string | undefined, order: "latest" | "oldest") => { + return useInfiniteQuery({ + // 💡 [체크리스트 달성] queryKey에 lpId와 order 포함! (정렬 바뀌면 알아서 리패치) + queryKey: ['lpComments', lpId, order], + queryFn: ({ pageParam = 0 }) => getLpComments(Number(lpId), order, pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + // 서버에서 다음 댓글이 있다고 하면 다음 커서를 반환! + return lastPage?.data?.hasNext ? lastPage.data.cursor : undefined; + }, + enabled: !!lpId, // lpId가 있을 때만 작동! + }); +}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/hooks/useGetLpList.ts b/UMC-10th-mission-FE/src/hooks/useGetLpList.ts index fa1523a7..f7e2e970 100644 --- a/UMC-10th-mission-FE/src/hooks/useGetLpList.ts +++ b/UMC-10th-mission-FE/src/hooks/useGetLpList.ts @@ -1,11 +1,16 @@ -import { useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { getLps } from "../apis/lp"; export const useGetLpList = (sort: "latest" | "oldest" = "latest") => { - return useQuery({ - queryKey: ["lps", sort], - queryFn: () => getLps(sort), + return useInfiniteQuery({ + queryKey: ["lps", sort], // 정렬 기준이 바뀌면 새로 캐싱 + queryFn: ({ pageParam = 0 }) => getLps(sort, pageParam), // pageParam이 바로 cursor 역할! + initialPageParam: 0, // 첫 시작 커서는 0번 + getNextPageParam: (lastPage) => { + // 💡 서버에서 "다음 데이터 있어!(hasNext)"라고 하면 다음 커서 번호를 주고, 없으면 undefined! + return lastPage.data.hasNext ? lastPage.data.cursor : undefined; + }, staleTime: 1000 * 60, gcTime: 1000 * 60 * 5, }); -}; +}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx b/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx index 53297dc7..9930bab1 100644 --- a/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx +++ b/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx @@ -1,11 +1,25 @@ // src/pages/LPDetailPage.tsx -import { useEffect } from "react"; +import { useEffect, useState } from "react"; // 👈 useState 추가 import { useParams, useNavigate, useLocation } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; import { useGetLpDetail } from "../hooks/useGetLPDetail"; +import { useInView } from "react-intersection-observer"; +import { useGetLpComments } from "../hooks/useGetLpComments"; + +// 🦴 댓글용 스켈레톤 컴포넌트 (초기 로딩 & 추가 로딩 때 사용) +const CommentSkeleton = () => ( +
      +
      +
      +
      +
      +
      +
      +
      +); const LPDetailPage = () => { - const { lpid } = useParams(); // URL에서 lpid 뽑아오기 + const { lpid } = useParams(); const navigate = useNavigate(); const location = useLocation(); const { accessToken } = useAuth(); @@ -13,24 +27,42 @@ const LPDetailPage = () => { // 🛡️ [체크리스트] 비로그인 사용자 차단! useEffect(() => { if (!accessToken) { - // 1. 브라우저 기본 경고창(모달 역할) 띄우기 alert("로그인이 필요한 서비스입니다. 로그인 페이지로 이동합니다! 🚨"); - - // 2. 로그인 창으로 보내면서, state라는 '비밀 쪽지'에 원래 주소(from)를 적어서 보냄! navigate("/login", { - state: { from: location.pathname }, // 👈 핵심 포인트! + state: { from: location.pathname }, replace: true }); } - }, [accessToken, navigate, location]); // deps에 location 추가 + }, [accessToken, navigate, location]); - // 비서(useQuery) 출동! + // 1️⃣ 상세 정보 비서 출동! const { data: response, isPending, isError, refetch } = useGetLpDetail(lpid); - // 토큰이 없어서 쫓겨나는 중이면 에러나 빈화면 방지 + // 2️⃣ 💬 댓글 무한스크롤 비서 & 상태 세팅! + const [order, setOrder] = useState<"latest" | "oldest">("latest"); + const { ref, inView } = useInView(); + + const { + data: commentsData, + isPending: isCommentsPending, // 첫 댓글 로딩 상태 + fetchNextPage, + hasNextPage, + isFetchingNextPage // 추가 댓글 로딩 상태 + } = useGetLpComments(lpid, order); + + // 📸 화면 바닥(ref)에 닿으면 다음 댓글 가져오기! + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + // 받아온 댓글 페이지들 납작하게 펴기 + const commentsList = commentsData?.pages.flatMap((page) => page.data.data) || []; + if (!accessToken) return null; - // 1️⃣ 에러 났을 때 + // 에러 UI if (isError) { return (
      @@ -40,7 +72,7 @@ const LPDetailPage = () => { ); } - // 2️⃣ 로딩 중일 때 (Skeleton UI) + // 상세 페이지 로딩 UI if (isPending) { return (
      @@ -54,27 +86,21 @@ const LPDetailPage = () => { ); } - // 3️⃣ 데이터 도착! (성공) - const lp = response?.data; // 서버 데이터 구조에 따라 수정될 수 있음! + const lp = response?.data; return (
      - {/* 썸네일 이미지 */} + {/* --- 💿 기존 LP 상세 정보 영역 --- */} {lp?.thumbnail ? ( - {lp.title} + {lp.title} ) : (
      이미지가 없습니다
      )} - {/* 메타 정보 (제목, 업로드일) */}

      {lp?.title || "제목 없음"}

      @@ -86,24 +112,82 @@ const LPDetailPage = () => {
      - {/* 본문 (Content) */}
      {lp?.content || "본문 내용이 없습니다."}
      - {/* 액션 버튼들 (좋아요 / 수정 / 삭제) */}
      - - - + + +
      + {/* --- 💬 여기서부터 댓글 영역 시작! --- */} +
      + +
      +
      +

      댓글 ({commentsList.length})

      + {/* 정렬 토글 버튼 */} + +
      + + {/* 댓글 작성란 (UI만 구현) */} +
      + +
      + ※ 타인을 비방하는 댓글은 삭제될 수 있습니다. + +
      +
      + + {/* 1. 최초 로딩 시 (상단 스켈레톤) */} + {isCommentsPending ? ( +
      + + + +
      + ) : commentsList.length === 0 ? ( +

      아직 작성된 댓글이 없습니다. 첫 댓글의 주인공이 되어보세요!

      + ) : ( +
      + {/* 실제 댓글 데이터 렌더링 */} + {commentsList.map((comment: any) => ( +
      +
      + {comment.author?.name || "익명"} + + {comment.createdAt ? new Date(comment.createdAt).toLocaleDateString() : ""} + +
      +

      {comment.content}

      +
      + ))} +
      + )} + + {/* 2. 추가 로딩 시 (하단 스켈레톤) */} + {isFetchingNextPage && ( +
      + + +
      + )} + + {/* 📸 댓글 무한 스크롤 트리거 관찰용 div */} +
      +
      ); diff --git a/UMC-10th-mission-FE/src/pages/LPListPage.tsx b/UMC-10th-mission-FE/src/pages/LPListPage.tsx index 263cf12a..c4167588 100644 --- a/UMC-10th-mission-FE/src/pages/LPListPage.tsx +++ b/UMC-10th-mission-FE/src/pages/LPListPage.tsx @@ -1,30 +1,42 @@ // src/pages/LPListPage.tsx -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import { useGetLpList } from "../hooks/useGetLpList"; // 👈 비서 훅 임포트 -import type { Lp } from "../types/lp"; // 👈 [수정] 팝콘이 만든 진짜 타입 불러오기! - -interface LpItem { - id: string; - thumbnail?: string; - title: string; -} +import { useInView } from "react-intersection-observer"; // 👈 마법의 관찰 카메라 +import { useGetLpList } from "../hooks/useGetLpList"; +import { LpCardSkeleton } from "../components/LpCardSkeleton"; // 👈 스켈레톤 가져오기 const LPListPage = () => { const navigate = useNavigate(); - // 정렬 상태 관리 ('latest' 또는 'oldest') const [sort, setSort] = useState<"latest" | "oldest">("latest"); - // 비서 호출! (데이터, 로딩상태, 에러상태, 다시불러오기 함수 받기) - const { data: response, isPending, isError, refetch } = useGetLpList(sort); + // 📸 관찰 카메라 달기 (맨 밑에 닿으면 inView가 true가 됨!) + const { ref, inView } = useInView(); + + // 🏃‍♂️ 무한 배달 비서 호출! + const { + data, + isPending, // 첫 로딩 중인지 + isError, + fetchNextPage, // 다음 10개 가져와! + hasNextPage, // 더 가져올 게 남았어? + isFetchingNextPage, // 지금 다음 페이지 가져오는 중이야? + refetch + } = useGetLpList(sort); + + // 💡 [핵심] 카메라에 맨 밑 요소가 보이고, 더 가져올 게 있고, 현재 로딩 중이 아니라면? -> 다음 페이지 호출! + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); - const lpList: Lp[] = response?.data?.data ?? []; + // 페이지 데이터들을 하나로 쭉 합쳐주기 (플랫하게 펴기!) + const lpList = data?.pages.flatMap((page) => page.data.data) || []; - // 1. 에러 났을 때 보여줄 UI if (isError) { return ( -
      -

      데이터를 불러오는데 실패했습니다 😭

      +
      +

      데이터를 불러오는데 실패했습니다 😭

      ); @@ -34,7 +46,6 @@ const LPListPage = () => {

      나의 LP 보관함

      - {/* 정렬 토글 버튼 */}
      - {/* 2. 로딩 중일 때 보여줄 Skeleton UI */} + {/* 1. 최초 로딩 시 (상단 스켈레톤) */} {isPending ? (
      - {[1, 2, 3, 4].map((n) => ( -
      - ))} + {[1, 2, 3, 4, 5, 6].map((n) => )}
      ) : lpList.length === 0 ? (

      보관된 LP가 없습니다.

      ) : (
      + {/* 실제 데이터 렌더링 (카드 오버레이는 유지!) */} {lpList.map((lp) => ( - // 1️⃣ 라우팅 연결 (onClick) & 카드 확대 (hover:scale-105) - // 💡 Tailwind의 'group' 클래스를 넣어야 자식 요소가 마우스를 인식해!
      navigate(`/lp/${lp.id}`)} + onClick={() => navigate(`/lps/${lp.id}`)} className="group border border-[#FF1493] rounded-xl p-4 bg-[#111] flex flex-col cursor-pointer transition-transform duration-300 hover:scale-105" > - {/* 썸네일 영역 (여기에 오버레이를 덮을 거야) */}
      {lp.thumbnail ? ( - {lp.title} + {lp.title} ) : ( -
      - No Image -
      +
      No Image
      )} - - {/* 2️⃣ 호버 오버레이 (메타 정보 노출) */} - {/* 💡 기본은 투명(opacity-0)인데, 부모(group)에 마우스가 올라가면 짠!(group-hover:opacity-100) */}
      -

      - {lp.title} -

      +

      {lp.title}

      📅 {lp.createdAt ? new Date(lp.createdAt).toLocaleDateString() : "날짜 모름"}

      -

      - ❤️ 좋아요 {lp.likes || 0}개 -

      +

      ❤️ 좋아요 {lp.likes || 0}개

      - - {/* 카드 하단에 항상 보이는 기본 제목 */}

      {lp.title}

      ))}
      )} + + {/* 2. 추가 로딩 시 (하단 스켈레톤) */} + {isFetchingNextPage && ( +
      + {[1, 2].map((n) => )} +
      + )} + + {/* 📸 관찰용 투명 div (여기에 스크롤이 닿으면 다음 페이지 호출) */} +
      ); }; From d2d92f433b96cf1e9a80746e915f908e722a644a Mon Sep 17 00:00:00 2001 From: goeun Date: Sun, 17 May 2026 21:53:10 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[week7/mission1]=20useMutation=EC=9D=84=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=86=90=EC=89=BD=EA=B2=8C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 7 + .claudeignore | 0 CLAUDE.md | 90 ++++++ UMC-10th-mission-FE/.gitignore | 2 + UMC-10th-mission-FE/src/apis/auth.ts | 24 ++ UMC-10th-mission-FE/src/apis/lp.ts | 73 ++++- .../src/components/CommentSection.tsx | 258 ++++++++++++++++++ .../src/components/ConfirmModal.tsx | 71 +++++ .../src/components/EditProfileModal.tsx | 169 ++++++++++++ .../src/components/LpWriteModal.tsx | 241 ++++++++++++++++ .../src/context/AuthContext.tsx | 9 +- .../src/layouts/HomeLayout.tsx | 121 ++++++-- .../src/pages/LPDetailPage.tsx | 242 ++++++++-------- UMC-10th-mission-FE/src/pages/LoginPage.tsx | 56 ++-- UMC-10th-mission-FE/src/pages/MyPage.tsx | 68 +++-- UMC-10th-mission-FE/src/types/auth.ts | 6 + UMC-10th-mission-FE/src/types/lp.ts | 33 ++- 17 files changed, 1260 insertions(+), 210 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .claudeignore create mode 100644 CLAUDE.md create mode 100644 UMC-10th-mission-FE/src/components/CommentSection.tsx create mode 100644 UMC-10th-mission-FE/src/components/ConfirmModal.tsx create mode 100644 UMC-10th-mission-FE/src/components/EditProfileModal.tsx create mode 100644 UMC-10th-mission-FE/src/components/LpWriteModal.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..9c17112d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm build *)" + ] + } +} diff --git a/.claudeignore b/.claudeignore new file mode 100644 index 00000000..e69de29b diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..c30c4e8a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Location + +All source code lives under `UMC-10th-mission-FE/`. Run all commands from that directory. + +## Commands + +```bash +# Install dependencies (pnpm) +pnpm install + +# Start dev server +pnpm dev + +# Type-check and build +pnpm build + +# Lint +pnpm lint +``` + +No test suite is configured. Use `pnpm build` to catch type errors. + +## Environment + +Create `UMC-10th-mission-FE/.env.local` with: + +``` +VITE_API_BASE_URL=http://localhost:8080 +``` + +## Architecture + +**Stack:** React 19 + TypeScript + Vite, TailwindCSS v4, TanStack Query v5, React Router v7, Axios, Zod + React Hook Form, Styled Components. + +**Entry point:** `src/main.tsx` → `src/App.tsx` + +### Routing (`src/App.tsx`) + +React Router v7 with a nested layout structure: + +``` +AppRoot (QueryClientProvider + AuthProvider) +└── HomeLayout (nav + sidebar + floating button) + ├── / (HomePage) + ├── /login, /signup, /v1/auth/google/callback + ├── /lps (LPListPage) + ├── /lps/:lpid (LPDetailPage) + └── PrivateLayout (redirects to /login if no accessToken) + ├── /mypage + └── /write +``` + +### Auth flow + +- `AuthContext` (`src/context/AuthContext.tsx`) holds `accessToken` in React state, initialized from `localStorage`. +- Tokens stored under keys in `src/constants/key.ts` (`accessToken`, `refreshToken`). +- `src/apis/axios.ts` — single `axiosInstance` with two interceptors: attaches Bearer token on every request, auto-refreshes on 401 (deduped with a shared `refreshPromise`), clears storage and redirects to `/login` on refresh failure. +- `PrivateLayout` reads `accessToken` from context and redirects unauthenticated users. + +### API layer (`src/apis/`) + +- `axios.ts` — configured `axiosInstance` (base URL from `VITE_API_BASE_URL`) +- `auth.ts` — sign-in, sign-up, logout, Google OAuth, my-info endpoints +- `lp.ts` — LP list (cursor-based), LP detail, LP comments + +### Data fetching hooks (`src/hooks/`) + +All hooks wrap TanStack Query: +- `useGetLpList` — `useInfiniteQuery` with cursor pagination; `queryKey: ["lps", sort]` +- `useGetLPDetail` — `useQuery` for a single LP +- `useGetLpComments` — comments for an LP +- `useGetMyInfo` — accepts `accessToken | null`; skips the query when null + +### Infinite scroll pattern + +`LPListPage` combines `useInfiniteQuery` + `react-intersection-observer`: a sentinel `
      ` at the bottom triggers `fetchNextPage()` via `useEffect` when `inView && hasNextPage && !isFetchingNextPage`. Initial load shows a grid of `` components; subsequent pages append more skeletons below the list while fetching. + +### Types (`src/types/`) + +- `CommonRes` — wrapper `{ status, message, data: T }` for all API responses +- `lp.ts` — `Lp`, `GetLpsResponse` +- `auth.ts` — auth DTOs + +### Styling + +Tailwind v4 utility classes throughout. Color palette: `#0f1014` backgrounds, `#FF1493` accent/primary, `#1a1a1a` / `#333` surfaces. `LpCardSkeleton` uses `animate-pulse` for loading states. diff --git a/UMC-10th-mission-FE/.gitignore b/UMC-10th-mission-FE/.gitignore index a547bf36..00ee0aac 100644 --- a/UMC-10th-mission-FE/.gitignore +++ b/UMC-10th-mission-FE/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +CLAUDE.md \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/apis/auth.ts b/UMC-10th-mission-FE/src/apis/auth.ts index 31587a41..7ae0504c 100644 --- a/UMC-10th-mission-FE/src/apis/auth.ts +++ b/UMC-10th-mission-FE/src/apis/auth.ts @@ -2,6 +2,7 @@ import axios from "axios"; import type { ReqSignInDto, ReqSignUpDto, + ReqUpdateProfileDto, ResMyInfoDto, ResSignInDto, ResSignUpDto, @@ -33,3 +34,26 @@ export const postLogout = async () => { const { data } = await axiosInstance.post("/v1/auth/signout"); return data; }; + +// 회원 탈퇴 +export const deleteMyAccount = async () => { + const { data } = await axiosInstance.delete("/v1/users/me"); + return data; +}; + +// 프로필 수정 +// - name, bio는 항상 전송 (bio 빈 문자열 → 서버에서 초기화) +// - avatar는 새 파일을 선택했을 때만 포함 (없으면 기존 이미지 유지) +export const patchMyProfile = async ( + body: ReqUpdateProfileDto +): Promise => { + const formData = new FormData(); + formData.append("name", body.name); + formData.append("bio", body.bio); + if (body.avatar) formData.append("avatar", body.avatar); + + const { data } = await axiosInstance.patch("/v1/users/me", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return data; +}; diff --git a/UMC-10th-mission-FE/src/apis/lp.ts b/UMC-10th-mission-FE/src/apis/lp.ts index 608044fa..e003f2a3 100644 --- a/UMC-10th-mission-FE/src/apis/lp.ts +++ b/UMC-10th-mission-FE/src/apis/lp.ts @@ -1,5 +1,12 @@ import { axiosInstance } from "./axios"; -import type { GetLpsResponse } from "../types/lp"; +import type { + CreateLpResponse, + GetLpsResponse, + ReqCreateCommentDto, + ReqCreateLpDto, + ReqUpdateCommentDto, + ReqUpdateLpDto, +} from "../types/lp"; // 이름을 getLps로 통일 (query hook과 일치) export const getLps = async ( @@ -18,12 +25,72 @@ export const getLpDetail = async (id: number) => { }; export const getLpComments = async ( - lpId: number, - order: "latest" | "oldest" = "latest", + lpId: number, + order: "latest" | "oldest" = "latest", cursor: number = 0 ) => { const { data } = await axiosInstance.get(`/v1/lps/${lpId}/comments`, { params: { order, cursor, limit: 10 }, }); return data; +}; + +export const postComment = async (lpId: number, body: ReqCreateCommentDto) => { + const { data } = await axiosInstance.post(`/v1/lps/${lpId}/comments`, body); + return data; +}; + +export const patchComment = async ( + lpId: number, + commentId: number, + body: ReqUpdateCommentDto +) => { + const { data } = await axiosInstance.patch( + `/v1/lps/${lpId}/comments/${commentId}`, + body + ); + return data; +}; + +export const deleteComment = async (lpId: number, commentId: number) => { + const { data } = await axiosInstance.delete( + `/v1/lps/${lpId}/comments/${commentId}` + ); + return data; +}; + +const buildLpFormData = (body: ReqCreateLpDto | ReqUpdateLpDto): FormData => { + const formData = new FormData(); + formData.append("title", body.title); + formData.append("content", body.content); + if (body.thumbnail) formData.append("thumbnail", body.thumbnail); + body.tags.forEach((tag) => formData.append("tags", tag)); + return formData; +}; + +export const postLp = async (body: ReqCreateLpDto): Promise => { + const { data } = await axiosInstance.post("/v1/lps", buildLpFormData(body), { + headers: { "Content-Type": "multipart/form-data" }, + }); + return data; +}; + +export const patchLp = async ( + id: number, + body: ReqUpdateLpDto +): Promise => { + const { data } = await axiosInstance.patch(`/v1/lps/${id}`, buildLpFormData(body), { + headers: { "Content-Type": "multipart/form-data" }, + }); + return data; +}; + +export const deleteLp = async (id: number) => { + const { data } = await axiosInstance.delete(`/v1/lps/${id}`); + return data; +}; + +export const postLpLike = async (id: number) => { + const { data } = await axiosInstance.post(`/v1/lps/${id}/likes`); + return data; }; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/components/CommentSection.tsx b/UMC-10th-mission-FE/src/components/CommentSection.tsx new file mode 100644 index 00000000..bfc4415c --- /dev/null +++ b/UMC-10th-mission-FE/src/components/CommentSection.tsx @@ -0,0 +1,258 @@ +import { useState, useEffect } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useInView } from "react-intersection-observer"; +import { MoreVertical } from "lucide-react"; +import { useGetLpComments } from "../hooks/useGetLpComments"; +import { useGetMyInfo } from "../hooks/useGetMyInfo"; +import { useAuth } from "../context/AuthContext"; +import { postComment, patchComment, deleteComment } from "../apis/lp"; +import type { Comment } from "../types/lp"; + +const CommentSkeleton = () => ( +
      +
      +
      +
      +
      +
      +
      +
      +); + +interface CommentSectionProps { + lpId: string; +} + +const CommentSection = ({ lpId }: CommentSectionProps) => { + const queryClient = useQueryClient(); + const { accessToken } = useAuth(); + const { data: myInfo } = useGetMyInfo(accessToken); + const myId = myInfo?.data?.id; + + const [order, setOrder] = useState<"latest" | "oldest">("latest"); + const [commentText, setCommentText] = useState(""); + const [openMenuId, setOpenMenuId] = useState(null); + const [editingId, setEditingId] = useState(null); + const [editText, setEditText] = useState(""); + + const { ref, inView } = useInView(); + + const { + data: commentsData, + isPending: isCommentsPending, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useGetLpComments(lpId, order); + + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) fetchNextPage(); + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + // 외부 클릭 시 메뉴 닫기 + useEffect(() => { + const handler = () => setOpenMenuId(null); + document.addEventListener("click", handler); + return () => document.removeEventListener("click", handler); + }, []); + + const commentsList: Comment[] = + commentsData?.pages.flatMap((page) => page.data.data) ?? []; + + // 댓글 작성 + const { mutate: createComment, isPending: isCreating } = useMutation({ + mutationFn: (content: string) => postComment(Number(lpId), { content }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] }); + setCommentText(""); + }, + }); + + // 댓글 수정 + const { mutate: updateComment, isPending: isUpdating } = useMutation({ + mutationFn: ({ commentId, content }: { commentId: number; content: string }) => + patchComment(Number(lpId), commentId, { content }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] }); + setEditingId(null); + }, + }); + + // 댓글 삭제 + const { mutate: removeComment } = useMutation({ + mutationFn: (commentId: number) => deleteComment(Number(lpId), commentId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] }); + }, + }); + + return ( +
      +
      +

      + 댓글 ({commentsList.length}) +

      + +
      + + {/* 댓글 작성란 */} +
      + -
      - ※ 타인을 비방하는 댓글은 삭제될 수 있습니다. - -
      -
      - - {/* 1. 최초 로딩 시 (상단 스켈레톤) */} - {isCommentsPending ? ( -
      - - - -
      - ) : commentsList.length === 0 ? ( -

      아직 작성된 댓글이 없습니다. 첫 댓글의 주인공이 되어보세요!

      - ) : ( -
      - {/* 실제 댓글 데이터 렌더링 */} - {commentsList.map((comment: any) => ( -
      -
      - {comment.author?.name || "익명"} - - {comment.createdAt ? new Date(comment.createdAt).toLocaleDateString() : ""} - -
      -

      {comment.content}

      -
      - ))} -
      - )} - - {/* 2. 추가 로딩 시 (하단 스켈레톤) */} - {isFetchingNextPage && ( -
      - - -
      - )} - - {/* 📸 댓글 무한 스크롤 트리거 관찰용 div */} -
      -
      + + {/* LP 수정 모달 — LpWriteModal을 edit 모드로 재사용 */} + {isEditModalOpen && lp && ( + setIsEditModalOpen(false)} + /> + )} + + {/* 삭제 확인 모달 */} + {isDeleteModalOpen && ( + handleDelete()} + onCancel={() => setIsDeleteModalOpen(false)} + /> + )}
      ); }; -export default LPDetailPage; \ No newline at end of file +export default LPDetailPage; diff --git a/UMC-10th-mission-FE/src/pages/LoginPage.tsx b/UMC-10th-mission-FE/src/pages/LoginPage.tsx index 760a5fc4..b9e1e3fd 100644 --- a/UMC-10th-mission-FE/src/pages/LoginPage.tsx +++ b/UMC-10th-mission-FE/src/pages/LoginPage.tsx @@ -1,16 +1,15 @@ import { useNavigate, useLocation } from "react-router-dom"; +import { useMutation } from "@tanstack/react-query"; import { useAuth } from "../context/AuthContext"; import useForm from "../hooks/useForm"; import { validateSignin } from "../utils/validate"; import { ChevronLeft } from "lucide-react"; -import { useState } from "react"; +import type { AxiosError } from "axios"; const LoginPage = () => { const navigate = useNavigate(); const location = useLocation(); const { login } = useAuth(); - const [serverError, setServerError] = useState(""); - const [isLoading, setIsLoading] = useState(false); const { values, errors, touched, getInputProps } = useForm({ init_val: { email: "", password: "" }, @@ -18,29 +17,25 @@ const LoginPage = () => { }); const isFormValid = - !errors.email && - !errors.password && - values.email !== "" && - values.password !== ""; + !errors.email && !errors.password && !!values.email && !!values.password; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!isFormValid || isLoading) return; + const { mutate: handleLogin, isPending, error } = useMutation({ + mutationFn: login, + onSuccess: () => { + const from = (location.state as { from?: string })?.from ?? "/"; + navigate(from, { replace: true }); + }, + }); - setIsLoading(true); - setServerError(""); + const serverError = error + ? ((error as AxiosError<{ message: string }>).response?.data?.message ?? + "로그인 중 오류가 발생했습니다.") + : null; - try { - await login(values); - const from = location.state?.from || "/mypage"; - navigate(from, { replace: true }); - } catch (err: any) { - const msg = - err.response?.data?.message || "로그인 중 오류가 발생했습니다."; - setServerError(msg); - } finally { - setIsLoading(false); - } + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!isFormValid || isPending) return; + handleLogin(values); }; return ( @@ -66,9 +61,7 @@ const LoginPage = () => { type="email" placeholder="이메일을 입력해주세요" className={`bg-[#1a1a1a] border ${ - touched.email && errors.email - ? "border-red-500" - : "border-[#333]" + touched.email && errors.email ? "border-red-500" : "border-[#333]" } rounded-lg p-4 text-white placeholder-gray-500 focus:border-[#FF1493] outline-none transition-colors`} {...getInputProps("email")} /> @@ -90,9 +83,7 @@ const LoginPage = () => { {...getInputProps("password")} /> {touched.password && errors.password && ( - - {errors.password} - + {errors.password} )}
      @@ -103,11 +94,10 @@ const LoginPage = () => {

      diff --git a/UMC-10th-mission-FE/src/pages/MyPage.tsx b/UMC-10th-mission-FE/src/pages/MyPage.tsx index 7d0f59b8..a812a7ff 100644 --- a/UMC-10th-mission-FE/src/pages/MyPage.tsx +++ b/UMC-10th-mission-FE/src/pages/MyPage.tsx @@ -1,35 +1,24 @@ -import { useEffect, useState } from "react"; -import { axiosInstance } from "../apis/axios"; +import { useState } from "react"; import { useNavigate } from "react-router-dom"; +import { Settings } from "lucide-react"; import { useAuth } from "../context/AuthContext"; -import type { ResMyInfoDto } from "../types/auth"; +import { useGetMyInfo } from "../hooks/useGetMyInfo"; +import EditProfileModal from "../components/EditProfileModal"; const MyPage = () => { - const [userInfo, setUserInfo] = useState(null); - const [loading, setLoading] = useState(true); const navigate = useNavigate(); - const { logout } = useAuth(); + const { accessToken, logout } = useAuth(); + const { data: response, isPending } = useGetMyInfo(accessToken); + const userInfo = response?.data ?? null; - useEffect(() => { - const fetchUserInfo = async () => { - try { - const response = await axiosInstance.get("/v1/users/me"); - setUserInfo(response.data.data); - } catch (error) { - console.error("유저 정보를 가져오는데 실패했습니다.", error); - } finally { - setLoading(false); - } - }; - fetchUserInfo(); - }, []); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); const handleLogout = async () => { await logout(); navigate("/", { replace: true }); }; - if (loading) { + if (isPending) { return (

      @@ -45,29 +34,46 @@ const MyPage = () => { {userInfo ? (
      - {/* 아바타 */} + {/* 아바타 + 이름/이메일 + 설정 버튼 */}
      {userInfo.avatar ? ( avatar ) : ( -
      +
      {userInfo.name?.charAt(0).toUpperCase()}
      )} -
      -

      {userInfo.name}

      -

      {userInfo.email}

      + +
      +

      + {userInfo.name} +

      +

      {userInfo.email}

      + + {/* 설정 버튼 */} +
      - {userInfo.bio && ( + {/* Bio */} + {userInfo.bio ? (

      {userInfo.bio}

      + ) : ( +

      + 아직 자기소개가 없습니다. +

      )}
      )} + + {/* 프로필 수정 모달 */} + {isEditModalOpen && userInfo && ( + setIsEditModalOpen(false)} + /> + )}
      ); }; diff --git a/UMC-10th-mission-FE/src/types/auth.ts b/UMC-10th-mission-FE/src/types/auth.ts index 3578d059..3439501e 100644 --- a/UMC-10th-mission-FE/src/types/auth.ts +++ b/UMC-10th-mission-FE/src/types/auth.ts @@ -39,3 +39,9 @@ export type ResMyInfoDto = CommonRes<{ createdAt: Date; updatedAt: Date; }>; + +export type ReqUpdateProfileDto = { + name: string; + bio: string; // 빈 문자열로 보내면 서버에서 bio 초기화 + avatar?: File; // 새 파일을 선택했을 때만 포함 +}; diff --git a/UMC-10th-mission-FE/src/types/lp.ts b/UMC-10th-mission-FE/src/types/lp.ts index fc8530a8..9e810ef9 100644 --- a/UMC-10th-mission-FE/src/types/lp.ts +++ b/UMC-10th-mission-FE/src/types/lp.ts @@ -3,13 +3,15 @@ import type { CommonRes } from "./common"; export type Lp = { id: number; - title: string; // title 속성 추가 + title: string; content?: string; - thumbnail?: string; // 또는 imageUrl? 형태 - artist?: string; // LP에 아티스트 정보가 포함될 경우 + thumbnail?: string; + artist?: string; createdAt?: string; updatedAt?: string; likes?: number; + tags?: string[]; + author?: { id: number; name: string }; }; export type GetLpsResponse = CommonRes<{ @@ -17,3 +19,28 @@ export type GetLpsResponse = CommonRes<{ cursor: number | null; hasNext: boolean; }>; + +export type ReqCreateLpDto = { + title: string; + content: string; + thumbnail?: File | null; + tags: string[]; +}; + +export type ReqUpdateLpDto = ReqCreateLpDto; + +export type CreateLpResponse = CommonRes; + +export type Comment = { + id: number; + content: string; + createdAt: string; + updatedAt?: string; + author: { + id: number; + name: string; + }; +}; + +export type ReqCreateCommentDto = { content: string }; +export type ReqUpdateCommentDto = { content: string }; From 4420435a23a9f2970d4935d1f5ac125a4d4d4bb6 Mon Sep 17 00:00:00 2001 From: goeun Date: Sun, 17 May 2026 23:20:12 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[week7/mission2]=EB=82=99=EA=B4=80=EC=A0=81?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8(OptimisticUpdate)=20?= =?UTF-8?q?=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=B4=EC=84=9C=20=EB=B9=9B?= =?UTF-8?q?=EB=B3=B4=EB=8B=A4=20=EB=B9=A0=EB=A5=B4=EA=B2=8C=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UMC-10th-mission-FE/src/apis/lp.ts | 3 +- .../src/components/EditProfileModal.tsx | 51 +++++++++++++++---- .../src/components/EditProfileModel.tsx | 31 +++++++++++ UMC-10th-mission-FE/src/hooks/useGetLpList.ts | 2 +- .../src/pages/LPDetailPage.tsx | 49 ++++++++++++++++-- UMC-10th-mission-FE/src/pages/MyPage.tsx | 8 +-- UMC-10th-mission-FE/src/types/lp.ts | 3 ++ 7 files changed, 126 insertions(+), 21 deletions(-) create mode 100644 UMC-10th-mission-FE/src/components/EditProfileModel.tsx diff --git a/UMC-10th-mission-FE/src/apis/lp.ts b/UMC-10th-mission-FE/src/apis/lp.ts index e003f2a3..22b099ed 100644 --- a/UMC-10th-mission-FE/src/apis/lp.ts +++ b/UMC-10th-mission-FE/src/apis/lp.ts @@ -2,6 +2,7 @@ import { axiosInstance } from "./axios"; import type { CreateLpResponse, GetLpsResponse, + LpDetailResponse, ReqCreateCommentDto, ReqCreateLpDto, ReqUpdateCommentDto, @@ -19,7 +20,7 @@ export const getLps = async ( return data; }; -export const getLpDetail = async (id: number) => { +export const getLpDetail = async (id: number): Promise => { const { data } = await axiosInstance.get(`/v1/lps/${id}`); return data; }; diff --git a/UMC-10th-mission-FE/src/components/EditProfileModal.tsx b/UMC-10th-mission-FE/src/components/EditProfileModal.tsx index 2012b0df..8db34a88 100644 --- a/UMC-10th-mission-FE/src/components/EditProfileModal.tsx +++ b/UMC-10th-mission-FE/src/components/EditProfileModal.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, type ChangeEvent } from "react"; +import { useState, useEffect, type ChangeEvent } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { X } from "lucide-react"; import { patchMyProfile } from "../apis/auth"; @@ -19,8 +19,6 @@ const EditProfileModal = ({ userInfo, onClose }: EditProfileModalProps) => { userInfo.avatar ?? null ); - const backdropRef = useRef(null); - useEffect(() => { const handler = (e: globalThis.KeyboardEvent) => { if (e.key === "Escape") onClose(); @@ -29,10 +27,6 @@ const EditProfileModal = ({ userInfo, onClose }: EditProfileModalProps) => { return () => window.removeEventListener("keydown", handler); }, [onClose]); - const handleBackdropClick = (e: React.MouseEvent) => { - if (e.target === backdropRef.current) onClose(); - }; - const handleAvatarChange = (e: ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -44,10 +38,41 @@ const EditProfileModal = ({ userInfo, onClose }: EditProfileModalProps) => { const { mutate: updateProfile, isPending } = useMutation({ mutationFn: patchMyProfile, + onMutate: async (variables) => { + // 진행 중인 내 정보 쿼리 취소 (race condition 방지) + await queryClient.cancelQueries({ queryKey: ["user", "me"] }); + + // 롤백용 스냅샷 저장 + const previousData = queryClient.getQueryData(["user", "me"]); + + // 서버 응답 전에 UI 즉시 반영 + queryClient.setQueryData(["user", "me"], (old) => { + if (!old) return old; + return { + ...old, + data: { + ...old.data, + name: variables.name, + bio: variables.bio || null, + }, + }; + }); + + return { previousData }; + }, + onError: (_error, _variables, context) => { + // 요청 실패 시 스냅샷으로 롤백 + if (context?.previousData) { + queryClient.setQueryData(["user", "me"], context.previousData); + } + }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["user", "me"] }); onClose(); }, + onSettled: () => { + // 성공·실패 무관하게 서버 최신 데이터로 동기화 + queryClient.invalidateQueries({ queryKey: ["user", "me"] }); + }, }); const handleSubmit = () => { @@ -61,11 +86,13 @@ const EditProfileModal = ({ userInfo, onClose }: EditProfileModalProps) => { return (
      -
      +
      e.stopPropagation()} + > {/* Header */}

      프로필 수정

      @@ -148,12 +175,14 @@ const EditProfileModal = ({ userInfo, onClose }: EditProfileModalProps) => { {/* Buttons */}
      + + ); +}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/hooks/useGetLpList.ts b/UMC-10th-mission-FE/src/hooks/useGetLpList.ts index f7e2e970..789ffb7f 100644 --- a/UMC-10th-mission-FE/src/hooks/useGetLpList.ts +++ b/UMC-10th-mission-FE/src/hooks/useGetLpList.ts @@ -8,7 +8,7 @@ export const useGetLpList = (sort: "latest" | "oldest" = "latest") => { initialPageParam: 0, // 첫 시작 커서는 0번 getNextPageParam: (lastPage) => { // 💡 서버에서 "다음 데이터 있어!(hasNext)"라고 하면 다음 커서 번호를 주고, 없으면 undefined! - return lastPage.data.hasNext ? lastPage.data.cursor : undefined; + return lastPage.data.hasNext ? (lastPage.data.cursor ?? undefined) : undefined; }, staleTime: 1000 * 60, gcTime: 1000 * 60 * 5, diff --git a/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx b/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx index 9d9917f7..3d5ae1e1 100644 --- a/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx +++ b/UMC-10th-mission-FE/src/pages/LPDetailPage.tsx @@ -6,6 +6,7 @@ import { Heart } from "lucide-react"; import { useAuth } from "../context/AuthContext"; import { useGetLpDetail } from "../hooks/useGetLPDetail"; import { deleteLp, postLpLike } from "../apis/lp"; +import type { LpDetailResponse } from "../types/lp"; import CommentSection from "../components/CommentSection"; import LpWriteModal from "../components/LpWriteModal"; import ConfirmModal from "../components/ConfirmModal"; @@ -28,7 +29,10 @@ const LPDetailPage = () => { replace: true, }); } - }, [accessToken, navigate, location]); + // location은 navigate 시 현재 pathname을 state에 담기 위한 값이므로 + // 의존성 배열에서 제외해 쿼리 상태 변화로 인한 불필요한 effect 재실행을 방지 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accessToken]); const { data: response, isPending, isError, refetch } = useGetLpDetail(lpid); @@ -41,10 +45,41 @@ const LPDetailPage = () => { }, }); - // 좋아요 토글 + // 좋아요 토글 (낙관적 업데이트) const { mutate: handleLike, isPending: isLiking } = useMutation({ mutationFn: () => postLpLike(Number(lpid)), + onMutate: async () => { + // 진행 중인 상세 조회 쿼리 취소 (race condition 방지) + await queryClient.cancelQueries({ queryKey: ["lp", lpid] }); + + // 롤백용 스냅샷 저장 + const previousData = queryClient.getQueryData(["lp", lpid]); + + // 좋아요 상태 즉시 토글 + queryClient.setQueryData(["lp", lpid], (old) => { + if (!old) return old; + const liked = old.data.isLiked ?? false; + return { + ...old, + data: { + ...old.data, + isLiked: !liked, + likes: (old.data.likes ?? 0) + (liked ? -1 : 1), + }, + }; + }); + + return { previousData }; + }, + onError: (_error, _variables, context) => { + // 요청 실패 시 스냅샷으로 롤백 (에러 시 재요청 없이 롤백만 수행) + if (context?.previousData) { + queryClient.setQueryData(["lp", lpid], context.previousData); + } + }, onSuccess: () => { + // 성공 시에만 서버 최신 데이터로 동기화 + // (실패 시 재요청을 막아 LP 상세 오류 화면으로 튕기는 현상 방지) queryClient.invalidateQueries({ queryKey: ["lp", lpid] }); }, }); @@ -136,16 +171,21 @@ const LPDetailPage = () => {
      {/* 좋아요 */} {/* 수정 */}
      - {/* Bio */} {userInfo.bio ? (

      {userInfo.bio} @@ -77,6 +76,7 @@ const MyPage = () => { )}

      )} - {/* 프로필 수정 모달 */} {isEditModalOpen && userInfo && ( ; + export type GetLpsResponse = CommonRes<{ data: Lp[]; cursor: number | null; From 01d4e38154a47f2fb0fff8b715027472335dc45a Mon Sep 17 00:00:00 2001 From: goeun Date: Sat, 13 Jun 2026 19:02:47 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[week10/mission1]=20=EC=98=81=ED=99=94=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 11 +- .env.example | 2 + .gitattributes | 12 + UMC-10th-mission-FE/.gitignore => .gitignore | 28 +- MOVIE_SEARCH_README.md | 86 + OPTIMIZATION_NOTES.md | 135 + UMC-10th-mission-FE/.env | 2 - UMC-10th-mission-FE/README.md | 73 - UMC-10th-mission-FE/eslint.config.js | 22 - UMC-10th-mission-FE/package.json | 46 - UMC-10th-mission-FE/pnpm-lock.yaml | 2383 ----------------- UMC-10th-mission-FE/public/favicon.svg | 1 - UMC-10th-mission-FE/public/icons.svg | 24 - UMC-10th-mission-FE/src/App.css | 60 - UMC-10th-mission-FE/src/App.tsx | 79 - UMC-10th-mission-FE/src/apis/auth.ts | 59 - UMC-10th-mission-FE/src/apis/axios.ts | 73 - UMC-10th-mission-FE/src/apis/lp.ts | 97 - UMC-10th-mission-FE/src/assets/hero.png | Bin 13057 -> 0 bytes UMC-10th-mission-FE/src/assets/react.svg | 1 - UMC-10th-mission-FE/src/assets/vite.svg | 1 - .../src/components/CommentSection.tsx | 258 -- .../src/components/ConfirmModal.tsx | 71 - .../src/components/EditProfileModal.tsx | 198 -- .../src/components/EditProfileModel.tsx | 31 - .../src/components/LpCardSkeleton.tsx | 11 - .../src/components/LpWriteModal.tsx | 241 -- UMC-10th-mission-FE/src/constants/key.ts | 4 - .../src/context/AuthContext.tsx | 79 - UMC-10th-mission-FE/src/hooks/useForm.ts | 43 - .../src/hooks/useGetLPDetail.ts | 13 - .../src/hooks/useGetLpComments.ts | 17 - UMC-10th-mission-FE/src/hooks/useGetLpList.ts | 16 - UMC-10th-mission-FE/src/hooks/useGetMyInfo.ts | 12 - .../src/hooks/useLocalStorage.ts | 36 - UMC-10th-mission-FE/src/imgs/google.png | Bin 6624 -> 0 bytes UMC-10th-mission-FE/src/index.css | 36 - .../src/layouts/HomeLayout.tsx | 164 -- .../src/layouts/PrivateLayout.tsx | 14 - .../src/layouts/ProtectedLayout.tsx | 13 - UMC-10th-mission-FE/src/main.tsx | 10 - .../src/pages/GoogleLoginRedirectPage.tsx | 38 - UMC-10th-mission-FE/src/pages/HomePage.tsx | 28 - .../src/pages/LPDetailPage.tsx | 241 -- UMC-10th-mission-FE/src/pages/LPListPage.tsx | 106 - UMC-10th-mission-FE/src/pages/LoginPage.tsx | 117 - UMC-10th-mission-FE/src/pages/MyPage.tsx | 111 - .../src/pages/NotFoundPage.tsx | 20 - UMC-10th-mission-FE/src/pages/SignupPage.tsx | 262 -- UMC-10th-mission-FE/src/pages/WritePage.tsx | 8 - UMC-10th-mission-FE/src/types/auth.ts | 47 - UMC-10th-mission-FE/src/types/common.ts | 5 - UMC-10th-mission-FE/src/types/lp.ts | 49 - UMC-10th-mission-FE/src/utils/validate.ts | 29 - UMC-10th-mission-FE/src/vite-env.d.ts | 7 - UMC-10th-mission-FE/tsconfig.json | 7 - UMC-10th-mission-FE/tsconfig.node.json | 24 - UMC-10th-mission-FE/vite.config.ts | 8 - UMC-10th-mission-FE/index.html => index.html | 6 +- package-lock.json | 1823 +++++++++++++ package.json | 23 + src/App.tsx | 7 + src/apis/tmdb.ts | 44 + src/components/MovieCard.tsx | 45 + src/components/MovieModal.tsx | 100 + src/components/MovieSearch.tsx | 132 + src/index.css | 295 ++ src/main.tsx | 10 + src/types/movie.ts | 19 + src/vite-env.d.ts | 10 + .../tsconfig.app.json => tsconfig.json | 15 +- vite.config.ts | 7 + week1/dist/index.js | 69 - week1/index.html | 37 - week1/src/index.ts | 95 - week1/style.css | 121 - week1/tsconfig.json | 19 - 77 files changed, 2787 insertions(+), 5659 deletions(-) create mode 100644 .env.example create mode 100644 .gitattributes rename UMC-10th-mission-FE/.gitignore => .gitignore (55%) create mode 100644 MOVIE_SEARCH_README.md create mode 100644 OPTIMIZATION_NOTES.md delete mode 100644 UMC-10th-mission-FE/.env delete mode 100644 UMC-10th-mission-FE/README.md delete mode 100644 UMC-10th-mission-FE/eslint.config.js delete mode 100644 UMC-10th-mission-FE/package.json delete mode 100644 UMC-10th-mission-FE/pnpm-lock.yaml delete mode 100644 UMC-10th-mission-FE/public/favicon.svg delete mode 100644 UMC-10th-mission-FE/public/icons.svg delete mode 100644 UMC-10th-mission-FE/src/App.css delete mode 100644 UMC-10th-mission-FE/src/App.tsx delete mode 100644 UMC-10th-mission-FE/src/apis/auth.ts delete mode 100644 UMC-10th-mission-FE/src/apis/axios.ts delete mode 100644 UMC-10th-mission-FE/src/apis/lp.ts delete mode 100644 UMC-10th-mission-FE/src/assets/hero.png delete mode 100644 UMC-10th-mission-FE/src/assets/react.svg delete mode 100644 UMC-10th-mission-FE/src/assets/vite.svg delete mode 100644 UMC-10th-mission-FE/src/components/CommentSection.tsx delete mode 100644 UMC-10th-mission-FE/src/components/ConfirmModal.tsx delete mode 100644 UMC-10th-mission-FE/src/components/EditProfileModal.tsx delete mode 100644 UMC-10th-mission-FE/src/components/EditProfileModel.tsx delete mode 100644 UMC-10th-mission-FE/src/components/LpCardSkeleton.tsx delete mode 100644 UMC-10th-mission-FE/src/components/LpWriteModal.tsx delete mode 100644 UMC-10th-mission-FE/src/constants/key.ts delete mode 100644 UMC-10th-mission-FE/src/context/AuthContext.tsx delete mode 100644 UMC-10th-mission-FE/src/hooks/useForm.ts delete mode 100644 UMC-10th-mission-FE/src/hooks/useGetLPDetail.ts delete mode 100644 UMC-10th-mission-FE/src/hooks/useGetLpComments.ts delete mode 100644 UMC-10th-mission-FE/src/hooks/useGetLpList.ts delete mode 100644 UMC-10th-mission-FE/src/hooks/useGetMyInfo.ts delete mode 100644 UMC-10th-mission-FE/src/hooks/useLocalStorage.ts delete mode 100644 UMC-10th-mission-FE/src/imgs/google.png delete mode 100644 UMC-10th-mission-FE/src/index.css delete mode 100644 UMC-10th-mission-FE/src/layouts/HomeLayout.tsx delete mode 100644 UMC-10th-mission-FE/src/layouts/PrivateLayout.tsx delete mode 100644 UMC-10th-mission-FE/src/layouts/ProtectedLayout.tsx delete mode 100644 UMC-10th-mission-FE/src/main.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/GoogleLoginRedirectPage.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/HomePage.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/LPDetailPage.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/LPListPage.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/LoginPage.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/MyPage.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/NotFoundPage.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/SignupPage.tsx delete mode 100644 UMC-10th-mission-FE/src/pages/WritePage.tsx delete mode 100644 UMC-10th-mission-FE/src/types/auth.ts delete mode 100644 UMC-10th-mission-FE/src/types/common.ts delete mode 100644 UMC-10th-mission-FE/src/types/lp.ts delete mode 100644 UMC-10th-mission-FE/src/utils/validate.ts delete mode 100644 UMC-10th-mission-FE/src/vite-env.d.ts delete mode 100644 UMC-10th-mission-FE/tsconfig.json delete mode 100644 UMC-10th-mission-FE/tsconfig.node.json delete mode 100644 UMC-10th-mission-FE/vite.config.ts rename UMC-10th-mission-FE/index.html => index.html (68%) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/apis/tmdb.ts create mode 100644 src/components/MovieCard.tsx create mode 100644 src/components/MovieModal.tsx create mode 100644 src/components/MovieSearch.tsx create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/types/movie.ts create mode 100644 src/vite-env.d.ts rename UMC-10th-mission-FE/tsconfig.app.json => tsconfig.json (53%) create mode 100644 vite.config.ts delete mode 100644 week1/dist/index.js delete mode 100644 week1/index.html delete mode 100644 week1/src/index.ts delete mode 100644 week1/style.css delete mode 100644 week1/tsconfig.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9c17112d..fe030ed4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,16 @@ { "permissions": { "allow": [ - "Bash(pnpm build *)" + "Bash(pnpm build *)", + "Bash(npm install *)", + "Bash(npx tsc *)", + "Bash(echo \"EXIT: $?\")", + "Bash(npm run *)", + "Read(//tmp/**)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:5173/)", + "Bash(kill %1)", + "Bash(wait)", + "Bash(curl -s \"https://api.themoviedb.org/3/search/movie?api_key=ca324ae23606391798bdf45057a9bf4f&query=inception&language=ko-KR&include_adult=false\")" ] } } diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..6d9eb9c8 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# 이 파일을 복사해 .env 로 만들고 본인 키를 채우세요. (.env 는 git에 커밋되지 않습니다) +VITE_TMDB_API_KEY=your_tmdb_api_key_here diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..ce760737 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# 모든 텍스트 파일의 줄바꿈을 저장소·작업트리 모두 LF로 통일한다. +# (Windows의 core.autocrlf 설정과 무관하게 CRLF 변환 경고가 더 이상 뜨지 않음) +* text=auto eol=lf + +# 바이너리로 다뤄 변환하지 않을 파일들 +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary diff --git a/UMC-10th-mission-FE/.gitignore b/.gitignore similarity index 55% rename from UMC-10th-mission-FE/.gitignore rename to .gitignore index 00ee0aac..0e2fe2c0 100644 --- a/UMC-10th-mission-FE/.gitignore +++ b/.gitignore @@ -1,18 +1,28 @@ -# Logs +# dependencies +node_modules +.pnp +.pnp.js + +# build output +dist +dist-ssr +*.local +*.tsbuildinfo + +# env files (절대 커밋 금지 - TMDB API 키 보호) +.env +.env.* +!.env.example + +# logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* -lerna-debug.log* -node_modules -dist -dist-ssr -*.local - -# Editor directories and files +# editor / os .vscode/* !.vscode/extensions.json .idea @@ -22,5 +32,3 @@ dist-ssr *.njsproj *.sln *.sw? - -CLAUDE.md \ No newline at end of file diff --git a/MOVIE_SEARCH_README.md b/MOVIE_SEARCH_README.md new file mode 100644 index 00000000..d2f736c5 --- /dev/null +++ b/MOVIE_SEARCH_README.md @@ -0,0 +1,86 @@ +# 🎥 TMDB 영화 검색 + 렌더링 최적화 미션 + +Vite + React + TypeScript 로 만든 TMDB 영화 검색 사이트입니다. +`memo` / `useCallback` / `useMemo` 로 불필요한 리렌더를 제거하는 과정을 콘솔 로그로 확인할 수 있습니다. + +## 실행 방법 + +```bash +# 1. 의존성 설치 +npm install + +# 2. 환경변수 설정 (루트 .env) +# .env.example 을 복사해 .env 로 만들고 본인 TMDB 키를 넣으세요. +# VITE_TMDB_API_KEY=발급받은_키 +# ※ .env 는 .gitignore 에 포함되어 커밋되지 않습니다. + +# 3. 개발 서버 실행 +npm run dev # http://localhost:5173 + +# (선택) 타입체크 + 프로덕션 빌드 +npm run build +``` + +## 폴더 구조 (이번 미션에서 만든 파일) + +``` +. +├─ .env # VITE_TMDB_API_KEY (git 미추적) +├─ .env.example # 키 자리표시자 템플릿 +├─ .gitignore # .env 포함 +├─ index.html +├─ package.json +├─ tsconfig.json +├─ vite.config.ts +└─ src/ + ├─ main.tsx + ├─ App.tsx # MovieSearch 렌더 + ├─ index.css + ├─ vite-env.d.ts # VITE_TMDB_API_KEY 타입 선언 + ├─ apis/ + │ └─ tmdb.ts # search/movie 호출 (키는 import.meta.env 로만 사용) + ├─ types/ + │ └─ movie.ts # Movie / 응답 / Language 타입 + └─ components/ + ├─ MovieSearch.tsx # 부모: 검색 폼 + 상태 + 최적화 훅 + └─ MovieCard.tsx # 자식: memo 적용 카드 +``` + +## 단계별 구현 내용 + +### 1단계 — 기본 검색 기능 (`MovieSearch.tsx`) +- 상단 검색 영역을 `
      ` 으로 감싸 **엔터만으로도 검색** (`onSubmit` + `e.preventDefault()`). +- 영화 제목 `text input` — `placeholder="영화 제목을 입력하세요"`, 값은 `title` state. +- 성인 콘텐츠 `checkbox` — `includeAdult` boolean state → API `include_adult` 파라미터. +- 언어 `select` — 한국어(`ko-KR`)·영어(`en-US`)·일본어(`ja-JP`), `language` state → API `language` 파라미터. +- TMDB `search/movie` 호출 후 포스터·제목·평점·개요를 리스트로 렌더. +- **로딩 상태** 표시, **빈 검색어**(`trim()` 후 빈 값)는 호출하지 않음. + +### 2단계 — 리렌더 추적 로그 +- 부모(`MovieSearch`)와 자식(`MovieCard`)에 각각 `console.log` 삽입. +- 검색어를 타이핑하면 `title` state가 바뀌어 부모가 리렌더되고, + 최적화 전에는 **목록이 그대로인데도 모든 카드가 다시 렌더**되는 걸 콘솔에서 볼 수 있음. + +### 3단계 — 최적화 적용 +- **`memo`** (`MovieCard.tsx`): 카드 컴포넌트를 메모이제이션 → `movie`, `onSelect` props가 같으면 리렌더 건너뜀. +- **`useCallback`** (`MovieSearch.tsx`의 `handleSelect`): memo된 카드에 넘기는 핸들러의 **참조를 고정**. + 인라인 함수로 넘기면 매 렌더마다 새 함수가 되어 memo가 무력화되므로 필수. +- **`useMemo`** (`sortedMovies`): 평점순 정렬을 `movies`가 바뀔 때만 재계산. +- 반대로 **`handleSubmit`** 은 form에서만 쓰고 자식에게 넘기지 않으므로 `useCallback`으로 감싸지 않음(효과 없음). + +## 최적화 전 / 후 콘솔 로그 차이 + +검색 결과가 떠 있는 상태에서 **검색창에 글자 한 개를 타이핑**할 때: + +| 구분 | 부모 로그 | 카드 로그 | +| --- | --- | --- | +| **최적화 전** (memo·useCallback 없음) | `👪 [MovieSearch] render` 1회 | `🎬 [MovieCard] render` **카드 수만큼** (예: 20회) ❌ | +| **최적화 후** (memo + useCallback) | `👪 [MovieSearch] render` 1회 | `🎬 [MovieCard] render` **0회** ✅ | + +- 부모는 `title` state가 바뀌므로 어느 경우든 리렌더된다(정상). +- 핵심은 **목록이 바뀌지 않았는데 자식 카드가 다시 그려지지 않는 것**. + `movie` prop은 `useMemo`로 안정적인 배열에서 오고, `onSelect`는 `useCallback`으로 참조가 고정되어 + `memo`의 얕은 비교를 통과 → 카드 렌더가 생략된다. +- 새로 **검색을 실행**하면 `movies`가 바뀌어 `useMemo`가 재계산되고 카드들이 정상적으로 렌더된다. + +> 참고: `main.tsx`가 `StrictMode`라 개발 모드에서는 초기 렌더 로그가 의도적으로 2번씩 찍힐 수 있습니다(프로덕션 빌드에는 영향 없음). 타이핑 시 카드 로그가 사라지는지로 최적화 효과를 확인하세요. diff --git a/OPTIMIZATION_NOTES.md b/OPTIMIZATION_NOTES.md new file mode 100644 index 00000000..c39f59ea --- /dev/null +++ b/OPTIMIZATION_NOTES.md @@ -0,0 +1,135 @@ +# 🚀 성능 최적화 정리 (React.memo / useCallback / useMemo) + +> React DevTools **Profiler 탭**으로 리렌더를 관찰하고, 불필요한 상위/하위 리렌더를 +> `memo` · `useCallback` · `useMemo`로 제거한 과정을 정리한 문서입니다. + +## 0. Profiler로 무엇을 보나 + +1. Chrome 웹스토어에서 **React Developer Tools** 설치 → 개발 모드(`npm run dev`)로 앱 실행. +2. DevTools → **⚛️ Profiler** 탭 → 좌상단 **● Record** 클릭. +3. 영화 검색 / 필터링(검색어 타이핑, 언어 변경 등)을 수행 → **Stop**. +4. **Flamegraph / Ranked** 차트에서 *회색(렌더 안 됨)* vs *색칠(리렌더됨)* 을 확인. + - 설정 ⚙️ → **"Highlight updates when components render"** 를 켜면 화면에서도 리렌더된 컴포넌트에 테두리가 깜빡인다. + +관찰 포인트: **목록 데이터가 안 바뀌었는데도 영화 카드들이 리렌더되는가?** + +--- + +## 1. TMDB 영화 검색 — 최적화 전 / 후 + +### 문제 (최적화 전) + +검색창에 글자 하나를 타이핑 → 부모 `MovieSearch`의 `title` state 변경 → 부모 리렌더. +이때 아래 두 가지 때문에 **목록이 그대로인데도 모든 `MovieCard`가 같이 리렌더**된다. + +- `MovieCard`가 일반 컴포넌트라 부모가 렌더되면 무조건 자식도 렌더. +- 카드에 넘기는 `onSelect`가 **인라인 화살표 함수**라 매 렌더마다 새 참조. +- 정렬된 배열을 **렌더마다 새로 `sort()`** → 새 배열 참조. + +### 해결 (최적화 후) + +| 훅 | 위치 | 역할 | +| --- | --- | --- | +| `memo` | `MovieCard.tsx` | `movie`·`onSelect` props가 얕은 비교로 같으면 리렌더 건너뜀 | +| `useCallback` | `MovieSearch.tsx` `handleSelect`, `handleCloseModal` (deps `[]`) | memo된 카드에 넘기는 핸들러 참조 고정 → memo 유지 | +| `useMemo` | `MovieSearch.tsx` `sortedMovies` (deps `[movies]`) | 평점순 정렬을 `movies` 변경 시에만 재계산 + 안정적 배열 참조 | + +> `handleSubmit`은 form에서만 쓰고 자식에게 안 넘기므로 일부러 `useCallback`을 쓰지 않았다(효과 없음). + +### 콘솔 로그로 본 차이 (Profiler의 대용 증거) + +검색 결과 20개가 떠 있는 상태에서 **검색창에 글자 1개 입력** 시: + +| 구분 | `👪 MovieSearch` | `🎬 MovieCard` | +| --- | --- | --- | +| 최적화 전 | 1회 | **20회** ❌ | +| 최적화 후 | 1회 | **0회** ✅ | + +- 부모는 입력 state가 바뀌므로 어느 쪽이든 1회 렌더(정상). +- 핵심은 **목록 미변경 시 카드 리렌더가 0회**가 된 것. Profiler Flamegraph에서도 카드들이 회색(렌더 안 됨)으로 표시된다. +- 새로 **검색을 실행**하면 `movies`가 바뀌어 `useMemo` 재계산 + 카드 정상 렌더 → 의도대로 동작. + +**결론:** 현재 TMDB 앱은 상위 전체가 불필요하게 리렌더되는 구간이 없다(이미 최적화 완료 상태). + +--- + +## 2. LP 사이트 성능 개선 포인트 (3개 이상) + +> 현재 LP 프로젝트(`UMC-10th-mission-FE`)는 새 미션을 위해 working tree에서 제거된 상태라, +> 아래는 git 기록의 실제 코드(`LPListPage.tsx`, `CommentSection.tsx`)를 기준으로 한 개선안이다. +> 프로젝트를 복구(`git restore`)하면 그대로 적용 가능. + +### ① LP 카드를 별도 컴포넌트로 분리 + `React.memo` + +**문제:** `LPListPage`에서 카드를 인라인 `
      `로 `.map()` 렌더 중. 정렬 토글(`setSort`)이나 +다음 페이지 로드(`isFetchingNextPage`) 등으로 페이지가 리렌더되면 **기존 카드 전부가 다시 렌더**된다. + +```tsx +// components/LpCard.tsx (신규) +import { memo } from "react"; +import type { Lp } from "../types/lp"; + +interface LpCardProps { + lp: Lp; + onClick: (id: number) => void; // 인라인 navigate 대신 id만 받는 안정적 핸들러 +} + +function LpCardBase({ lp, onClick }: LpCardProps) { + return ( +
      onClick(lp.id)} className="..."> + {/* 기존 카드 마크업 */} +
      + ); +} +export default memo(LpCardBase); // props 같으면 리렌더 skip +``` + +### ② 카드 클릭 핸들러를 `useCallback`으로 고정 + +**문제:** 기존엔 카드마다 `onClick={() => navigate(`/lps/${lp.id}`)}` 인라인 함수 → memo를 무력화. + +```tsx +// LPListPage.tsx +const handleCardClick = useCallback( + (id: number) => navigate(`/lps/${id}`), + [navigate] +); +// ... +{lpList.map((lp) => )} +``` + +→ ①+② 조합으로, 정렬 토글/추가 로딩 시 **새로 들어온 카드만 렌더**되고 기존 카드는 skip. + +### ③ `flatMap` 결과를 `useMemo`로 메모이제이션 + +**문제:** `const lpList = data?.pages.flatMap(...) || []` 가 **렌더마다 새 배열** 생성 → +하위에 넘기면 참조가 매번 달라져 memo가 깨진다. + +```tsx +const lpList = useMemo( + () => data?.pages.flatMap((page) => page.data.data) ?? [], + [data] +); +``` + +### ④ (보너스) CommentSection 댓글 항목 분리 + memo + +**문제:** `CommentSection`은 `commentText`(입력창), `openMenuId`, `editingId` 등 state가 많아 +**댓글을 한 글자 타이핑할 때마다 댓글 리스트 전체가 리렌더**된다. + +```tsx +// 댓글 한 개를 CommentItem으로 분리하고 memo로 감싼다. +const CommentItem = memo(function CommentItem({ comment, myId, onEdit, onDelete }) { ... }); +// 부모에서 onEdit/onDelete는 useCallback으로 고정. +``` + +→ 입력창 타이핑 시 입력 영역만 리렌더되고, 기존 댓글들은 Profiler에서 회색(skip)으로 유지. + +--- + +## 적용한 최적화 요약 (한 줄) + +- **TMDB**: `MovieCard`(memo) + `handleSelect/handleCloseModal`(useCallback) + `sortedMovies`(useMemo) + → 타이핑 시 카드 리렌더 20회 → 0회. +- **LP**: 카드/댓글 컴포넌트 분리 후 `memo`, 클릭·수정·삭제 핸들러 `useCallback`, 목록 배열 `useMemo` + → 정렬 토글·추가 로딩·댓글 입력 시 기존 항목 리렌더 제거. diff --git a/UMC-10th-mission-FE/.env b/UMC-10th-mission-FE/.env deleted file mode 100644 index 286cd470..00000000 --- a/UMC-10th-mission-FE/.env +++ /dev/null @@ -1,2 +0,0 @@ -VITE_API_BASE_URL=http://localhost:8000 - diff --git a/UMC-10th-mission-FE/README.md b/UMC-10th-mission-FE/README.md deleted file mode 100644 index 7dbf7ebf..00000000 --- a/UMC-10th-mission-FE/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# 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/UMC-10th-mission-FE/eslint.config.js b/UMC-10th-mission-FE/eslint.config.js deleted file mode 100644 index ef614d25..00000000 --- a/UMC-10th-mission-FE/eslint.config.js +++ /dev/null @@ -1,22 +0,0 @@ -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/UMC-10th-mission-FE/package.json b/UMC-10th-mission-FE/package.json deleted file mode 100644 index d73f5d9a..00000000 --- a/UMC-10th-mission-FE/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "umc", - "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", - "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", - "styled-components": "^6.4.1", - "zod": "^4.4.3" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@tanstack/react-query-devtools": "^5.100.9", - "@types/node": "^24.12.2", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@types/styled-components": "^5.1.36", - "@vitejs/plugin-react": "^6.0.1", - "autoprefixer": "^10.5.0", - "eslint": "^10.2.1", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.5.0", - "postcss": "^8.5.14", - "tailwindcss": "^4.2.4", - "typescript": "~6.0.2", - "typescript-eslint": "^8.58.2", - "vite": "^8.0.10" - } -} diff --git a/UMC-10th-mission-FE/pnpm-lock.yaml b/UMC-10th-mission-FE/pnpm-lock.yaml deleted file mode 100644 index 8a23b496..00000000 --- a/UMC-10th-mission-FE/pnpm-lock.yaml +++ /dev/null @@ -1,2383 +0,0 @@ -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.6)) - '@tailwindcss/vite': - specifier: ^4.2.4 - version: 4.2.4(vite@8.0.11(@types/node@24.12.3)(jiti@2.7.0)) - '@tanstack/react-query': - specifier: ^5.100.9 - version: 5.100.9(react@19.2.6) - axios: - specifier: ^1.16.0 - version: 1.16.0 - lucide-react: - specifier: ^1.14.0 - version: 1.14.0(react@19.2.6) - react: - specifier: ^19.2.5 - version: 19.2.6 - react-dom: - specifier: ^19.2.5 - version: 19.2.6(react@19.2.6) - react-hook-form: - specifier: ^7.75.0 - version: 7.75.0(react@19.2.6) - react-intersection-observer: - specifier: ^10.0.3 - version: 10.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-router: - specifier: ^7.15.0 - version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-router-dom: - specifier: ^7.15.0 - version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - styled-components: - specifier: ^6.4.1 - version: 6.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - 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)) - '@tanstack/react-query-devtools': - specifier: ^5.100.9 - version: 5.100.9(@tanstack/react-query@5.100.9(react@19.2.6))(react@19.2.6) - '@types/node': - specifier: ^24.12.2 - version: 24.12.3 - '@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) - '@types/styled-components': - specifier: ^5.1.36 - version: 5.1.36 - '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.11(@types/node@24.12.3)(jiti@2.7.0)) - autoprefixer: - specifier: ^10.5.0 - version: 10.5.0(postcss@8.5.14) - 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 - postcss: - specifier: ^8.5.14 - version: 8.5.14 - tailwindcss: - specifier: ^4.2.4 - version: 4.2.4 - 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.11(@types/node@24.12.3)(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==} - - '@emotion/is-prop-valid@1.4.0': - resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} - - '@emotion/memoize@0.9.0': - resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} - - '@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.128.0': - resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} - - '@rolldown/binding-android-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0-rc.18': - resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.0-rc.18': - resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': - resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': - resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': - resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': - resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': - resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': - resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.0-rc.18': - resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} - - '@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.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/hoist-non-react-statics@3.3.7': - resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} - peerDependencies: - '@types/react': '*' - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/node@24.12.3': - resolution: {integrity: sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ==} - - '@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==} - - '@types/styled-components@5.1.36': - resolution: {integrity: sha512-pGMRNY5G2rNDKEv2DOiFYa7Ft1r0jrhmgBwHhOMzPTgCjO76bCot0/4uEfqj7K0Jf1KdQmDtAuaDk9EAs9foSw==} - - '@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==} - - autoprefixer@10.5.0: - resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - - 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.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} - 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.352: - resolution: {integrity: sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==} - - enhanced-resolve@5.21.1: - resolution: {integrity: sha512-8p7DUVq6XJnZEz9W4oSwiwycxBIjHjRzYb3Je3zVN+geKTRQKzAkR/K4PBExlS0090d9nshak6phMUxr3PDjmQ==} - 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'} - - fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - - 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==} - - hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - - 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-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - 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.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} - peerDependencies: - react: ^19.2.6 - - 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-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - 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.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} - engines: {node: '>=0.10.0'} - - rolldown@1.0.0-rc.18: - resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} - 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'} - - styled-components@6.4.1: - resolution: {integrity: sha512-ADu2dF53esUzzM4I0ewxhxFtsDd6v4V6dNkg3vG0iFKhnt06sJneTZnRvujAosZwW0XD58IKgGMQoqri4wHRqg==} - engines: {node: '>= 16'} - peerDependencies: - css-to-react-native: '>= 3.2.0' - react: '>= 16.8.0' - react-dom: '>= 16.8.0' - react-native: '>= 0.68.0' - peerDependenciesMeta: - css-to-react-native: - optional: true - react-dom: - optional: true - react-native: - optional: true - - stylis@4.3.6: - resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - - 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.11: - resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - 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 - - '@emotion/is-prop-valid@1.4.0': - dependencies: - '@emotion/memoize': 0.9.0 - - '@emotion/memoize@0.9.0': {} - - '@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.6))': - dependencies: - '@standard-schema/utils': 0.3.0 - react-hook-form: 7.75.0(react@19.2.6) - - '@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.128.0': {} - - '@rolldown/binding-android-arm64@1.0.0-rc.18': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.0-rc.18': - optional: true - - '@rolldown/binding-darwin-x64@1.0.0-rc.18': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.0-rc.18': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': - 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.18': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.18': {} - - '@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.1 - 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.11(@types/node@24.12.3)(jiti@2.7.0))': - dependencies: - '@tailwindcss/node': 4.2.4 - '@tailwindcss/oxide': 4.2.4 - tailwindcss: 4.2.4 - vite: 8.0.11(@types/node@24.12.3)(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.6))(react@19.2.6)': - dependencies: - '@tanstack/query-devtools': 5.100.9 - '@tanstack/react-query': 5.100.9(react@19.2.6) - react: 19.2.6 - - '@tanstack/react-query@5.100.9(react@19.2.6)': - dependencies: - '@tanstack/query-core': 5.100.9 - react: 19.2.6 - - '@tybys/wasm-util@0.10.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/esrecurse@4.3.1': {} - - '@types/estree@1.0.9': {} - - '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14)': - dependencies: - '@types/react': 19.2.14 - hoist-non-react-statics: 3.3.2 - - '@types/json-schema@7.0.15': {} - - '@types/node@24.12.3': - 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 - - '@types/styled-components@5.1.36': - dependencies: - '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.14) - '@types/react': 19.2.14 - 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.11(@types/node@24.12.3)(jiti@2.7.0))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.11(@types/node@24.12.3)(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: {} - - autoprefixer@10.5.0(postcss@8.5.14): - dependencies: - browserslist: 4.28.2 - caniuse-lite: 1.0.30001792 - fraction.js: 5.3.4 - picocolors: 1.1.1 - postcss: 8.5.14 - postcss-value-parser: 4.2.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.6: - 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.352 - 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.352: {} - - enhanced-resolve@5.21.1: - 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.9 - 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.9 - 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 - - fraction.js@5.3.4: {} - - 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 - - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.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.6): - dependencies: - react: 19.2.6 - - 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.6 - - 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-value-parser@4.2.0: {} - - 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.6(react@19.2.6): - dependencies: - react: 19.2.6 - scheduler: 0.27.0 - - react-hook-form@7.75.0(react@19.2.6): - dependencies: - react: 19.2.6 - - react-intersection-observer@10.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6): - dependencies: - react: 19.2.6 - optionalDependencies: - react-dom: 19.2.6(react@19.2.6) - - react-is@16.13.1: {} - - react-router-dom@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): - dependencies: - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-router: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - - react-router@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): - dependencies: - cookie: 1.1.1 - react: 19.2.6 - set-cookie-parser: 2.7.2 - optionalDependencies: - react-dom: 19.2.6(react@19.2.6) - - react@19.2.6: {} - - rolldown@1.0.0-rc.18: - dependencies: - '@oxc-project/types': 0.128.0 - '@rolldown/pluginutils': 1.0.0-rc.18 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.18 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.18 - '@rolldown/binding-darwin-x64': 1.0.0-rc.18 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.18 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 - - 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: {} - - styled-components@6.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): - dependencies: - '@emotion/is-prop-valid': 1.4.0 - csstype: 3.2.3 - react: 19.2.6 - stylis: 4.3.6 - optionalDependencies: - react-dom: 19.2.6(react@19.2.6) - - stylis@4.3.6: {} - - 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.11(@types/node@24.12.3)(jiti@2.7.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.14 - rolldown: 1.0.0-rc.18 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 24.12.3 - 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/UMC-10th-mission-FE/public/favicon.svg b/UMC-10th-mission-FE/public/favicon.svg deleted file mode 100644 index 6893eb13..00000000 --- a/UMC-10th-mission-FE/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/UMC-10th-mission-FE/public/icons.svg b/UMC-10th-mission-FE/public/icons.svg deleted file mode 100644 index e9522193..00000000 --- a/UMC-10th-mission-FE/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/UMC-10th-mission-FE/src/App.css b/UMC-10th-mission-FE/src/App.css deleted file mode 100644 index af499fbc..00000000 --- a/UMC-10th-mission-FE/src/App.css +++ /dev/null @@ -1,60 +0,0 @@ -.login-container { - width: 100%; - max-width: 400px; - padding: 20px; - display: flex; - flex-direction: column; -} - -.login-header { - display: flex; - align-items: center; - margin-bottom: 30px; -} - -.back-button { - background: none; - border: none; - color: white; - font-size: 24px; - cursor: pointer; - margin-right: 10px; -} - -input { - width: 100%; - padding: 15px; - margin-bottom: 10px; - border-radius: 8px; - border: 1px solid #333; - background-color: white; - box-sizing: border-box; -} - -.error-input { - border: 2px solid #ff4d4d; -} - -.error-msg { - color: #ff4d4d; - font-size: 12px; - margin-top: -5px; - margin-bottom: 10px; -} - -.login-submit-btn { - width: 100%; - padding: 15px; - border-radius: 8px; - border: none; - background-color: #444; /* 비활성화 상태 */ - color: white; - font-weight: bold; - cursor: not-allowed; -} - -/* 유효성 검사 통과 시 활성화 (이미지 속 핑크색) */ -.login-submit-btn.active { - background-color: #ff2d78; - cursor: pointer; -} \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/App.tsx b/UMC-10th-mission-FE/src/App.tsx deleted file mode 100644 index 01a9ec82..00000000 --- a/UMC-10th-mission-FE/src/App.tsx +++ /dev/null @@ -1,79 +0,0 @@ -// src/App.tsx -import "./App.css"; -import { - createBrowserRouter, - RouterProvider, - type RouteObject, - Outlet, -} from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; // 👈 추가! -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // 👈 추가! - -import Home from "./pages/HomePage"; -import NotFoundPage from "./pages/NotFoundPage"; -import LoginPage from "./pages/LoginPage"; -import HomeLayout from "./layouts/HomeLayout"; -import SignUpPage from "./pages/SignupPage"; -import MyPage from "./pages/MyPage"; -import { AuthProvider } from "./context/AuthContext"; -import PrivateLayout from "./layouts/PrivateLayout"; -import GoogleLoginRedirectPage from "./pages/GoogleLoginRedirectPage"; -import LPListPage from "./pages/LPListPage"; // 👈 목록 페이지 임포트 -import LPDetailPage from "./pages/LPDetailPage"; // 👈 (나중에 만들) 상세 페이지 임포트 -import WritePage from "./pages/WritePage"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 1, - }, - }, -}); - -const AppRoot = () => { - return ( - - - - - {import.meta.env.DEV && } - - ); -}; - -const routes: RouteObject[] = [ - { - path: "/", - element: , - errorElement: , - children: [ - { - path: "/", - element: , - children: [ - { index: true, element: }, - { path: "login", element: }, - { path: "signup", element: }, - { path: "v1/auth/google/callback", element: }, - { path: "lps", element: }, - { path: "lps/:lpid", element: }, - { - element: , - children: [ - { path: "mypage", element: }, - { path: "write", element: }, - ], - }, - ], - }, - ], - }, -]; - -const router = createBrowserRouter(routes); - -function App() { - return ; -} - -export default App; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/apis/auth.ts b/UMC-10th-mission-FE/src/apis/auth.ts deleted file mode 100644 index 7ae0504c..00000000 --- a/UMC-10th-mission-FE/src/apis/auth.ts +++ /dev/null @@ -1,59 +0,0 @@ -import axios from "axios"; -import type { - ReqSignInDto, - ReqSignUpDto, - ReqUpdateProfileDto, - ResMyInfoDto, - ResSignInDto, - ResSignUpDto, -} from "../types/auth"; -import { axiosInstance } from "./axios"; - -const BASE_URL = import.meta.env.VITE_API_BASE_URL; - -// 회원가입 (토큰 불필요 → 기본 axios) -export const postSignup = async (body: ReqSignUpDto): Promise => { - const { data } = await axios.post(`${BASE_URL}/v1/auth/signup`, body); - return data; -}; - -// 로그인 (토큰 불필요 → 기본 axios) -export const postSignin = async (body: ReqSignInDto): Promise => { - const { data } = await axios.post(`${BASE_URL}/v1/auth/signin`, body); - return data; -}; - -// 내 정보 조회 (토큰 필요 → axiosInstance 사용, Hook 규칙 위반 제거) -export const getMyInfo = async (): Promise => { - const { data } = await axiosInstance.get("/v1/users/me"); - return data; -}; - -// 로그아웃 (서버에 알림, 토큰 필요할 수 있음 → axiosInstance) -export const postLogout = async () => { - const { data } = await axiosInstance.post("/v1/auth/signout"); - return data; -}; - -// 회원 탈퇴 -export const deleteMyAccount = async () => { - const { data } = await axiosInstance.delete("/v1/users/me"); - return data; -}; - -// 프로필 수정 -// - name, bio는 항상 전송 (bio 빈 문자열 → 서버에서 초기화) -// - avatar는 새 파일을 선택했을 때만 포함 (없으면 기존 이미지 유지) -export const patchMyProfile = async ( - body: ReqUpdateProfileDto -): Promise => { - const formData = new FormData(); - formData.append("name", body.name); - formData.append("bio", body.bio); - if (body.avatar) formData.append("avatar", body.avatar); - - const { data } = await axiosInstance.patch("/v1/users/me", formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); - return data; -}; diff --git a/UMC-10th-mission-FE/src/apis/axios.ts b/UMC-10th-mission-FE/src/apis/axios.ts deleted file mode 100644 index 876bbce5..00000000 --- a/UMC-10th-mission-FE/src/apis/axios.ts +++ /dev/null @@ -1,73 +0,0 @@ -import axios, { type InternalAxiosRequestConfig } from "axios"; -import { LOCAL_STORAGE_KEY } from "../constants/key"; - -interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { - _retry?: boolean; -} - -let refreshPromise: Promise | null = null; - -// 환경변수를 하나로 통일 (VITE_API_BASE_URL) -const BASE_URL = import.meta.env.VITE_API_BASE_URL; - -export const axiosInstance = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL || "http://localhost:8080", -}); - -// 요청 인터셉터: 모든 요청에 액세스 토큰 자동 첨부 -axiosInstance.interceptors.request.use( - (config) => { - const token = localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (err) => Promise.reject(err) -); - -// 응답 인터셉터: 401 발생 시 토큰 자동 갱신 -axiosInstance.interceptors.response.use( - (res) => res, - async (err) => { - const originalRequest = err.config as CustomAxiosRequestConfig; - - if ( - err.response?.status === 401 && - !originalRequest._retry && - !originalRequest.url?.includes("/v1/auth/refresh") - ) { - originalRequest._retry = true; - - if (!refreshPromise) { - refreshPromise = axios - .post(`${BASE_URL}/v1/auth/refresh`, { - refreshToken: localStorage.getItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN), - }) - .then(({ data }) => { - const { accessToken: newAccess, refreshToken: newRefresh } = - data.data; - localStorage.setItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN, newAccess); - localStorage.setItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN, newRefresh); - return newAccess; - }) - .catch((refreshErr) => { - localStorage.removeItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN); - localStorage.removeItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN); - window.location.href = "/login"; - return Promise.reject(refreshErr); - }) - .finally(() => { - refreshPromise = null; - }); - } - - return refreshPromise.then((newAccessToken) => { - originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; - return axiosInstance.request(originalRequest); - }); - } - - return Promise.reject(err); - } -); diff --git a/UMC-10th-mission-FE/src/apis/lp.ts b/UMC-10th-mission-FE/src/apis/lp.ts deleted file mode 100644 index 22b099ed..00000000 --- a/UMC-10th-mission-FE/src/apis/lp.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { axiosInstance } from "./axios"; -import type { - CreateLpResponse, - GetLpsResponse, - LpDetailResponse, - ReqCreateCommentDto, - ReqCreateLpDto, - ReqUpdateCommentDto, - ReqUpdateLpDto, -} from "../types/lp"; - -// 이름을 getLps로 통일 (query hook과 일치) -export const getLps = async ( - sort: "latest" | "oldest" = "latest", - cursor: number = 0 -): Promise => { - const { data } = await axiosInstance.get("/v1/lps", { - params: { sort, cursor, limit: 10 }, - }); - return data; -}; - -export const getLpDetail = async (id: number): Promise => { - const { data } = await axiosInstance.get(`/v1/lps/${id}`); - return data; -}; - -export const getLpComments = async ( - lpId: number, - order: "latest" | "oldest" = "latest", - cursor: number = 0 -) => { - const { data } = await axiosInstance.get(`/v1/lps/${lpId}/comments`, { - params: { order, cursor, limit: 10 }, - }); - return data; -}; - -export const postComment = async (lpId: number, body: ReqCreateCommentDto) => { - const { data } = await axiosInstance.post(`/v1/lps/${lpId}/comments`, body); - return data; -}; - -export const patchComment = async ( - lpId: number, - commentId: number, - body: ReqUpdateCommentDto -) => { - const { data } = await axiosInstance.patch( - `/v1/lps/${lpId}/comments/${commentId}`, - body - ); - return data; -}; - -export const deleteComment = async (lpId: number, commentId: number) => { - const { data } = await axiosInstance.delete( - `/v1/lps/${lpId}/comments/${commentId}` - ); - return data; -}; - -const buildLpFormData = (body: ReqCreateLpDto | ReqUpdateLpDto): FormData => { - const formData = new FormData(); - formData.append("title", body.title); - formData.append("content", body.content); - if (body.thumbnail) formData.append("thumbnail", body.thumbnail); - body.tags.forEach((tag) => formData.append("tags", tag)); - return formData; -}; - -export const postLp = async (body: ReqCreateLpDto): Promise => { - const { data } = await axiosInstance.post("/v1/lps", buildLpFormData(body), { - headers: { "Content-Type": "multipart/form-data" }, - }); - return data; -}; - -export const patchLp = async ( - id: number, - body: ReqUpdateLpDto -): Promise => { - const { data } = await axiosInstance.patch(`/v1/lps/${id}`, buildLpFormData(body), { - headers: { "Content-Type": "multipart/form-data" }, - }); - return data; -}; - -export const deleteLp = async (id: number) => { - const { data } = await axiosInstance.delete(`/v1/lps/${id}`); - return data; -}; - -export const postLpLike = async (id: number) => { - const { data } = await axiosInstance.post(`/v1/lps/${id}/likes`); - return data; -}; \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/assets/hero.png b/UMC-10th-mission-FE/src/assets/hero.png deleted file mode 100644 index 02251f4b956c55af2d76fd0788124d7eee2b45eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13057 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 P00000NkvXXu0mjft=yBf diff --git a/UMC-10th-mission-FE/src/assets/react.svg b/UMC-10th-mission-FE/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/UMC-10th-mission-FE/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/UMC-10th-mission-FE/src/assets/vite.svg b/UMC-10th-mission-FE/src/assets/vite.svg deleted file mode 100644 index 5101b674..00000000 --- a/UMC-10th-mission-FE/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/UMC-10th-mission-FE/src/components/CommentSection.tsx b/UMC-10th-mission-FE/src/components/CommentSection.tsx deleted file mode 100644 index bfc4415c..00000000 --- a/UMC-10th-mission-FE/src/components/CommentSection.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useState, useEffect } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useInView } from "react-intersection-observer"; -import { MoreVertical } from "lucide-react"; -import { useGetLpComments } from "../hooks/useGetLpComments"; -import { useGetMyInfo } from "../hooks/useGetMyInfo"; -import { useAuth } from "../context/AuthContext"; -import { postComment, patchComment, deleteComment } from "../apis/lp"; -import type { Comment } from "../types/lp"; - -const CommentSkeleton = () => ( -
      -
      -
      -
      -
      -
      -
      -
      -); - -interface CommentSectionProps { - lpId: string; -} - -const CommentSection = ({ lpId }: CommentSectionProps) => { - const queryClient = useQueryClient(); - const { accessToken } = useAuth(); - const { data: myInfo } = useGetMyInfo(accessToken); - const myId = myInfo?.data?.id; - - const [order, setOrder] = useState<"latest" | "oldest">("latest"); - const [commentText, setCommentText] = useState(""); - const [openMenuId, setOpenMenuId] = useState(null); - const [editingId, setEditingId] = useState(null); - const [editText, setEditText] = useState(""); - - const { ref, inView } = useInView(); - - const { - data: commentsData, - isPending: isCommentsPending, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useGetLpComments(lpId, order); - - useEffect(() => { - if (inView && hasNextPage && !isFetchingNextPage) fetchNextPage(); - }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); - - // 외부 클릭 시 메뉴 닫기 - useEffect(() => { - const handler = () => setOpenMenuId(null); - document.addEventListener("click", handler); - return () => document.removeEventListener("click", handler); - }, []); - - const commentsList: Comment[] = - commentsData?.pages.flatMap((page) => page.data.data) ?? []; - - // 댓글 작성 - const { mutate: createComment, isPending: isCreating } = useMutation({ - mutationFn: (content: string) => postComment(Number(lpId), { content }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] }); - setCommentText(""); - }, - }); - - // 댓글 수정 - const { mutate: updateComment, isPending: isUpdating } = useMutation({ - mutationFn: ({ commentId, content }: { commentId: number; content: string }) => - patchComment(Number(lpId), commentId, { content }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] }); - setEditingId(null); - }, - }); - - // 댓글 삭제 - const { mutate: removeComment } = useMutation({ - mutationFn: (commentId: number) => deleteComment(Number(lpId), commentId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["lpComments", lpId] }); - }, - }); - - return ( -
      -
      -

      - 댓글 ({commentsList.length}) -

      - -
      - - {/* 댓글 작성란 */} -
      -