diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md deleted file mode 100644 index d664bb1..0000000 --- a/.github/ISSUE_TEMPLATE/issue.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: "Issue Template" -about: "공통 이슈 템플릿" -title: "" -labels: "" -assignees: "" ---- - -## 📃 이슈 내용 -- - -## 🗃️ 범위(페이지, 기능) -- ex. SearchPage, LoginPage, 공통 컴포넌트 - -## 📌 작업목록 -- [ ] UI 구성 -- [ ] 라우팅 연결 -- [ ] API 연동 (또는 mock) -- [ ] 예외처리(로딩/에러/빈화면) diff --git "a/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" "b/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" new file mode 100644 index 0000000..e2479f8 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\342\231\273\357\270\217-refactor.md" @@ -0,0 +1,21 @@ +--- +name: "♻️ Refactor" +about: "Code refactoring Template " +title: "♻️ [Refactor] " +labels: ♻️ Refactor +assignees: "" +--- + +## 🔨 Issue Description + +[//]: # 해당 이슈에 대한 설명을 작성해주세요 + +## 🗯️ Cause and Effect + +[//]: # 코드 리팩토링을 하는 이유와 예상결과를 작성해주세요 + +## 📝 Check List + +[//]: # 진행할 업무 체크리스트를 작성해주세요 + +- [ ] Task 1 diff --git "a/.github/ISSUE_TEMPLATE/\342\232\231\357\270\217-setting.md" "b/.github/ISSUE_TEMPLATE/\342\232\231\357\270\217-setting.md" new file mode 100644 index 0000000..964686b --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\342\232\231\357\270\217-setting.md" @@ -0,0 +1,21 @@ +--- +name: "⚙️ Setting" +about: "Development environment setup Template " +title: "⚙️ [Setting] " +labels: ⚙️ Setting +assignees: "" +--- + +## 🔨 Issue Description + +[//]: # 해당 이슈에 대한 설명을 작성해주세요 + +## 🗯️ Setting Environment + +[//]: # 세팅한 환경이 무엇인지, 어떤 방법으로 세팅 예정인지 작성해주세요 + +## 📝 Check List + +[//]: # 진행할 업무 체크리스트를 작성해주세요 + +- [ ] Task 1 diff --git "a/.github/ISSUE_TEMPLATE/\342\234\250-feature.md" "b/.github/ISSUE_TEMPLATE/\342\234\250-feature.md" new file mode 100644 index 0000000..68fba34 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\342\234\250-feature.md" @@ -0,0 +1,21 @@ +--- +name: "✨ Feature" +about: "Feature Template " +title: "✨ [Feature] " +labels: ✨ Feature +assignees: "" +--- + +## 🔨 Issue Description + +[//]: # 해당 이슈에 대한 설명을 작성해주세요 + +## 🗯️ Feature role + +[//]: # 기능이 가지는 역할과 영향을 설명해주세요 + +## 📝 Check List + +[//]: # 진행할 업무 체크리스트를 작성해주세요 + +- [ ] Task 1 diff --git "a/.github/ISSUE_TEMPLATE/\360\237\220\236-bugfix.md" "b/.github/ISSUE_TEMPLATE/\360\237\220\236-bugfix.md" new file mode 100644 index 0000000..1a2c127 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\220\236-bugfix.md" @@ -0,0 +1,21 @@ +--- +name: "🐞 BugFix" +about: "Something isn't working Template " +title: "🐞 [BugFix] " +labels: 🐞 BugFix +assignees: "" +--- + +## 🔨 Issue Description + +[//]: # 해당 이슈에 대한 설명을 작성해주세요 + +## 🗯️ Bug Description + +[//]: # 버그의 문제와 원인을 적어주세요 + +## 📝 Check List + +[//]: # 진행할 업무 체크리스트를 작성해주세요 + +- [ ] Task 1 diff --git "a/.github/ISSUE_TEMPLATE/\360\237\223\203-docs.md" "b/.github/ISSUE_TEMPLATE/\360\237\223\203-docs.md" new file mode 100644 index 0000000..673f8e1 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\223\203-docs.md" @@ -0,0 +1,21 @@ +--- +name: "📃 Docs" +about: "Documentation writing and editing (README.md, etc.) Template " +title: "📃 [Docs] " +labels: 📃 Docs +assignees: "" +--- + +## 🔨 Issue Description + +[//]: # 해당 이슈에 대한 설명을 작성해주세요 + +## 🗯️ Documents and objectives + +[//]: # 어떤 종류의 문서인지 작성해주세요 + +## 📝 Check List + +[//]: # 진행할 업무 체크리스트를 작성해주세요 + +- [ ] Task 1 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5c933ed..1990e39 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,27 +1,47 @@ -## 💡 개요 - + ## 🔢 관련 이슈 링크 + - Closes # +## 📌 변경사항PR + +- [ ] ✨Feature: 새로운 기능 추가 +- [ ] 🐞Bugfix: 버그/오류 수정 +- [ ] 📃Docs: 문서 수정(README 등) +- [ ] 🔨Refactor: 코드 리팩토링 (기능 변경 없음) +- [ ] 🧪Test: 테스트 코드 추가/수정 +- [ ] 🎨UI/UX: 디자인 및 사용성 수정 +- [ ] ⚙️Setting: 기본 세팅 작업 + ## 💻 작업내용 -- -- -## 📌 변경사항PR -- [ ] FEAT: 새로운 기능 추가 -- [ ] FIX: 버그/오류 수정 -- [ ] CHORE: 코드/내부 파일/설정 수정 -- [ ] DOCS: 문서 수정(README 등) -- [ ] REFACTOR: 코드 리팩토링 (기능 변경 없음) -- [ ] TEST: 테스트 코드 추가/수정 -- [ ] STYLE: 스타일 변경(포맷, 세미콜론 등) + -## 🤔 추가 논의하고 싶은 내용 -- +- 작업 내용 + +## 🪧 미완성 작업 + + + +- [ ] Task 1 + +## 🤔 논의 사항 및 참고 사항 + + ## ✅ 체크리스트 + - [ ] 브랜치는 잘 맞게 올렸는지 - [ ] 관련 이슈를 맞게 연결했는지 - [ ] 로컬에서 정상 동작을 확있했는지 -- [ ] 충돌이 없다(또는 브랜치에서 충돌 해결 후 PR 업데이트 완료) +- [ ] 충돌은 없는지 +- [ ] 불필요한 console.log 제거했는지 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8570f22 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +jobs: + lint-and-build: + runs-on: ubuntu-latest + + steps: + - name: 레포지토리 체크아웃 + uses: actions/checkout@v4 + + - name: pnpm 설치 + uses: pnpm/action-setup@v5 + + - name: Node.js 설치 + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + + - name: 의존성 설치 + run: pnpm install --frozen-lockfile + + - name: ESLint 검사 + run: pnpm lint + + - name: Build 검사 + run: pnpm build diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..5ee7abd --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm exec lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1d690c1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +node_modules +pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..cd3cd5c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 2, + "useTabs": false +} diff --git a/README.md b/README.md index 0a64e94..a1de0d9 100644 --- a/README.md +++ b/README.md @@ -1,172 +1,166 @@ -# 🍽️ Eatsfine FE - -**Eatsfine은 '자리(좌석)'를 기준으로 레스토랑을 탐색하고 원하는 시간에 간편하게 예약까지 이어지는** 지도 기반 레스토랑 예약 웹 서비스입니다. - -🔗 **Service URL** -https://www.eatsfine.co.kr - -📽️ **데모 영상** -https://www.youtube.com/watch?v=Nk1_28zSJaQ - -## 👥 팀 소개 - -|
노바/박재선
|
듀/함이슬
|
서리/유설희
| -| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -|
|
|
| -|
[@jjjsun](https://github.com/jjjsun)
|
[@dew102938](https://github.com/dew102938)
|
[@yooseolhee](https://github.com/yooseolhee)
| - -## 🛠️ Tech Stack - -- Stack: **React + TypeScript + Vite + TailwindCSS + pnpm** -- UI: **shadcn/ui** -- Routing: **react-router-dom** -- Server State: **TanStack Query** -- Form Validation: **React Hook Form + Zod** -- HTTP Client: **axios** -- Client State: **Zustand** - -## 🔥 Git Commit Convention (커밋 규칙) - -효율적인 협업을 위해 다음과 같은 커밋 메세지 규칙을 사용합니다. - -**type은 소문자로 통일합니다.** - -| 커밋 타입 | 설명 | -| ------------- | ------------------------------ | -| 🎉 `feat` | 새로운 기능 추가 | -| 🐛 `fix` | 버그/오류 수정 | -| 🛠 `chore` | 코드/내부 파일/설정 수정 | -| 📝 `docs` | 문서 수정 (README 등) | -| 🔄 `refactor` | 코드 리팩토링 (기능 변경 없음) | -| 🧪 `test` | 테스트 코드 추가/수정 | -| 🎨 `style` | 스타일 변경(포맷, 세미콜론 등) | - -💻 **예시** - -```bash -git commit -m "feat: restaurant card 컴포넌트 추가" -git commit -m "fix: 네이버페이 결제수단 오류 수정" -git commit -m "style: 식당리스트 카드디자인 수정" -``` - -## 📁 폴더 구조 - -```txt -src/ - api/ # axios 인스턴스/요청 함수 - components/ # UI 컴포넌트 (도메인별 폴더 포함) - hooks/ # 커스텀 훅 - layouts/ # 레이아웃 - lib/ # 공용 유틸 (cn 등) - pages/ # 라우트 단위 페이지 - query/ # TanStack Query 설정 - stores/ # 전역 상태관리 - styles/ # 전역 스타일 - types/ # 전역 타입 (UI 모델) - utils/ # 공용 유틸 함수 -``` - -## 🌿 Branch - -- main : 배포/최종 안정 브랜치 **(직접 push 금지)** -- develop: 개발 통합 브랜치 (기본 작업 브랜치) -- 작업 브랜치 네이밍: - - `feat/mainPage` - - `fix/myPagePath` - - `chore/SearchPage` - - `refactor/Header` - -## 🎯 작업 루틴 - -기본 브랜치는 develop - -작업은 항상 `develop`에서 브랜치를 따서 진행하고, PR은 develop으로 올립니다. - -### 1. 작업 시작 전 (최신화) - -```bash -git checkout develop -git pull --rebase origin develop -``` - -### 2. 작업 브랜치 생성 - -```bash -git checkout -b feat/featureName -``` - -### 3. 작업 후 커밋 & 푸시 - -```bash -git add . # 필요하면 git add file명 으로 특정 파일만 추가해도 됨 -git commit -m "feat: 자세한 내용 적기" -git push -u origin feat/featureName -``` - -### 4. PR 생성 - -- feat/ → develop 로 PR 생성 -- PR 본문에 Closes #이슈번호 작성해서 merge 시 이슈가 자동으로 닫히도록 설정 - -```md -Closes #이슈번호 -``` - -### 5. 리뷰 & 머지 - -- 최소 1명 리뷰 후 merge -- main은 배포/최종용 브랜치이기에 **직접 push 금지** - -## 🔒 보안 - -- .env 및 민감정보는 절대 커밋 금지 -- 공유가 필요한 환경변수는 .env.example에서 키 형태로만 관리합니다. - -## 👥 팀 규칙 - -- **작업 시작전 develop 최신화: git pull** -- PR은 가능한 작게 쪼개서 올리기 -- PR에 작업 요약 + 스크린샷/동작 설명 포함하기 -- 충돌 발생 시 브랜치에서 먼저 해결 후 PR 업데이트 - -## 🧩 UI (shadcn/ui) - -- 컴포넌트는 src/components/ui에 생성됩니다. -- className 병합 유틸은 src/lib/utils.ts의 cn()을 사용합니다. - -## 💡 시작 방법 - -### 1. Clone & Install - -```bash -git clone https://github.com/Eatsfine/FE.git -cd eatsfine-fe -pnpm i -``` - -### 2. Environment Values - -.env는 커밋하지 않습니다. .env.example을 복사해서 사용합니다. - -```bash -# macOS/Linux -cp .env.example .env -``` - -```bash -:: Windows (cmd) -copy .env.example .env -``` - -### 3. Run - -```bash -pnpm dev -``` - -### 4. Build/Preview - -```bash -pnpm build -pnpm preview -``` +# 🍽️ Eatsfine FE + +**Eatsfine은 '자리(좌석)'를 기준으로 레스토랑을 탐색하고 원하는 시간에 간편하게 예약까지 이어지는** 지도 기반 레스토랑 예약 웹 서비스입니다. + +🔗 **Service URL** +https://www.eatsfine.co.kr + +🎬 **데모 영상** +https://www.youtube.com/watch?v=Nk1_28zSJaQ + +## 👥 팀 소개 + +|
노바/박재선
|
듀/함이슬
|
서리/유설희
| +| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +|
|
|
| +|
[@jjjsun](https://github.com/jjjsun)
|
[@dew102938](https://github.com/dew102938)
|
[@yooseolhee](https://github.com/yooseolhee)
| + +## 🛠️ Tech Stack + +- Stack: **React + TypeScript + Vite + TailwindCSS + pnpm** +- UI: **shadcn/ui** +- Routing: **react-router-dom** +- Server State: **TanStack Query** +- Form Validation: **React Hook Form + Zod** +- HTTP Client: **axios** +- Client State: **Zustand** + +## 🔥 Git Commit Convention (커밋 규칙) + +효율적인 협업을 위해 다음과 같은 커밋 메세지 규칙을 사용합니다. + +**type은 소문자로 통일합니다.** + +| 커밋 타입 | 설명 | +| ------------- | ------------------------------ | +| ✨ `Feat` | 새로운 기능 추가 | +| 🐞 `Bugfix` | 버그/오류 수정 | +| 📃 `Docs` | 문서 수정 (README 등) | +| 🔨 `Refactor` | 코드 리팩토링 (기능 변경 없음) | +| 🧪 `Test` | 테스트 코드 추가/수정 | +| 🎨 `UI/UX` | 디자인 및 사용성 수정 | +| ⚙️ `Setting` | 기본 세팅 작업 | + +💻 **예시** + +```bash +git commit -m "feat: restaurant card 컴포넌트 추가" +git commit -m "fix: 네이버페이 결제수단 오류 수정" +``` + +## 📁 폴더 구조 + +```txt +src/ + api/ # axios 인스턴스/요청 함수 + components/ # UI 컴포넌트 (도메인별 폴더 포함) + hooks/ # 커스텀 훅 + layouts/ # 레이아웃 + lib/ # 공용 유틸 (cn 등) + pages/ # 라우트 단위 페이지 + query/ # TanStack Query 설정 + stores/ # 전역 상태관리 + styles/ # 전역 스타일 + types/ # 전역 타입 (UI 모델) + utils/ # 공용 유틸 함수 +``` + +## 🌿 Branch + +- main : 배포/최종 안정 브랜치 **(직접 push 금지)** +- develop: 개발 통합 브랜치 (기본 작업 브랜치) +- 작업 브랜치 네이밍: + - `feat/#이슈번호` + - `fix/#13` + - `refactor/#77` + - `docs/#1` + +## 🎯 작업 루틴 + +기본 브랜치는 develop + +작업은 항상 `develop`에서 브랜치를 따서 진행하고, PR은 develop으로 올립니다. + +### 1. 작업 시작 전 (최신화) + +```bash +git checkout develop +git pull --rebase origin develop +``` + +### 2. 작업 브랜치 생성 + +```bash +git checkout -b feat/#이슈번호 +``` + +### 3. 작업 후 커밋 & 푸시 + +```bash +git add . # 필요하면 git add file명 으로 특정 파일만 추가해도 됨 +git commit -m "feat: 자세한 내용 적기" +git push -u origin feat/#이슈번호 +``` + +### 4. PR 생성 + +- feat/#이슈번호 → develop 로 PR 생성 +- PR 본문에 Closes #이슈번호 작성해서 merge 시 이슈가 자동으로 닫히도록 설정 + +```md +Closes #이슈번호 +``` + +### 5. 리뷰 & 머지 + +- 최소 1명 리뷰 후 merge +- main은 배포/최종용 브랜치이기에 **직접 push 금지** + +## 🔒 보안 + +- .env 및 민감정보는 절대 커밋 금지 +- 공유가 필요한 환경변수는 .env.example에서 키 형태로만 관리합니다. + +## 👥 팀 규칙 + +- **작업 시작전 develop 최신화: git pull** +- PR은 가능한 작게 쪼개서 올리기 +- PR에 작업 요약 + 스크린샷/동작 설명 포함하기 +- 충돌 발생 시 브랜치에서 먼저 해결 후 PR 업데이트 + +## 💡 시작 방법 + +### 1. Clone & Install + +```bash +git clone https://github.com/Eatsfine/FE.git +cd eatsfine-fe +pnpm i +``` + +### 2. Environment Values + +.env는 커밋하지 않습니다. .env.example을 복사해서 사용합니다. + +```bash +# macOS/Linux +cp .env.example .env +``` + +```bash +:: Windows (cmd) +copy .env.example .env +``` + +### 3. Run + +```bash +pnpm dev +``` + +### 4. Build/Preview + +```bash +pnpm build +pnpm preview +``` diff --git a/eslint.config.js b/eslint.config.js index 5e6b472..356c51d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,23 +1,46 @@ -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' +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import reactHooks from "eslint-plugin-react-hooks"; +import prettierConfig from "eslint-config-prettier"; +import globals from "globals"; +import react from "eslint-plugin-react"; -export default defineConfig([ - globalIgnores(['dist']), +export default [ { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, + globals: { + ...globals.browser, // 브라우저 환경 변수(window 등) 허용 + }, + }, + plugins: { + react, // 리액트 관련 규칙 추가 + }, + settings: { + react: { version: "detect" }, // 리액트 버전 자동 감지 + }, + }, + { + ignores: ["dist", "node_modules"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + plugins: { + "react-hooks": reactHooks, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + // 추가: any 사용을 에러가 아닌 '경고'로만 표시 (고도화하면서 천천히 고치기 위해) + "@typescript-eslint/no-explicit-any": "warn", + + // 추가: 빈 블록({}) 허용 + "no-empty": "warn", }, }, -]) + prettierConfig, +]; diff --git a/index.html b/index.html index 99fb533..ecad331 100644 --- a/index.html +++ b/index.html @@ -1,27 +1,27 @@ - - - - - - - 잇츠파인(Eatsfine) - - - - - - - -
- - - + + + + + + + 잇츠파인(Eatsfine) + + + + + + + +
+ + + diff --git a/package.json b/package.json index 7eaa1a9..eb37f5f 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,21 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "lint:fix": "eslint . --fix", "preview": "vite preview", - "format": "prettier . --write" + "format": "prettier . --write", + "format:check": "prettier . --check", + "prepare": "husky" + }, + "packageManager": "pnpm@10.0.0", + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md,css}": [ + "prettier --write" + ] }, "dependencies": { "@hookform/resolvers": "^5.2.2", @@ -18,13 +31,11 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", - "@react-oauth/google": "^0.13.4", "@tanstack/react-query": "^5.90.16", "@tosspayments/tosspayments-sdk": "^2.5.0", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "date-fns": "^4.1.0", "immer": "^11.1.3", "lucide-react": "^0.562.0", "react": "^19.2.0", @@ -48,9 +59,12 @@ "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", "prettier": "^3.7.4", "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8601736..843f6c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,9 +29,6 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.4 version: 1.2.4(@types/react@19.2.7)(react@19.2.3) - '@react-oauth/google': - specifier: ^0.13.4 - version: 0.13.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-query': specifier: ^5.90.16 version: 5.90.16(react@19.2.3) @@ -47,9 +44,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 immer: specifier: ^11.1.3 version: 11.1.3 @@ -89,7 +83,7 @@ importers: version: 9.39.2 '@tailwindcss/vite': specifier: ^4.1.18 - version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) '@tanstack/react-query-devtools': specifier: ^5.91.2 version: 5.91.2(@tanstack/react-query@5.90.16(react@19.2.3))(react@19.2.3) @@ -104,7 +98,7 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: ^5.1.1 - version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) eslint: specifier: ^9.39.1 version: 9.39.2(jiti@2.6.1) @@ -114,6 +108,9 @@ importers: eslint-plugin-prettier: specifier: ^5.5.4 version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: ^7.0.1 version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) @@ -123,6 +120,12 @@ importers: globals: specifier: ^16.5.0 version: 16.5.0 + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.4.0 + version: 16.4.0 prettier: specifier: ^3.7.4 version: 3.7.4 @@ -140,7 +143,7 @@ importers: version: 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.2.4 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2) + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) packages: @@ -781,12 +784,6 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-oauth/google@0.13.4': - resolution: {integrity: sha512-hGKyNEH+/PK8M0sFEuo3MAEk0txtHpgs94tDQit+s2LXg7b6z53NtzHfqDvoB2X8O6lGB+FRg80hY//X6hfD+w==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - '@rolldown/pluginutils@1.0.0-beta.53': resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} @@ -1120,10 +1117,22 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1131,9 +1140,45 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} @@ -1159,6 +1204,14 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1173,6 +1226,14 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1184,10 +1245,17 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1205,6 +1273,18 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + date-fns-jalali@4.1.0-0: resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} @@ -1223,6 +1303,14 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1234,6 +1322,10 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1241,10 +1333,21 @@ packages: electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + enhanced-resolve@5.18.4: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1253,6 +1356,10 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-iterator-helpers@1.3.1: + resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} + engines: {node: '>= 0.4'} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1261,6 +1368,14 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -1305,6 +1420,12 @@ packages: peerDependencies: eslint: '>=8.40' + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1347,6 +1468,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1392,6 +1516,10 @@ packages: debug: optional: true + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -1404,10 +1532,25 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1420,6 +1563,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1432,6 +1579,10 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1439,10 +1590,21 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1461,6 +1623,11 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1480,17 +1647,120 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1521,6 +1791,10 @@ packages: engines: {node: '>=6'} hasBin: true + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1598,6 +1872,15 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1605,6 +1888,14 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1628,6 +1919,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1646,13 +1941,53 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1673,6 +2008,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1680,6 +2018,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -1697,6 +2039,9 @@ packages: engines: {node: '>=14'} hasBin: true + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -1726,6 +2071,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -1781,15 +2129,47 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.54.0: resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1805,6 +2185,18 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1813,10 +2205,77 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1825,6 +2284,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1839,6 +2302,10 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -1859,6 +2326,22 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + typescript-eslint@8.51.0: resolution: {integrity: sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1871,6 +2354,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -1943,6 +2430,22 @@ packages: yaml: optional: true + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1952,9 +2455,18 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2545,11 +3057,6 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@react-oauth/google@0.13.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - '@rolldown/pluginutils@1.0.0-beta.53': {} '@rollup/rollup-android-arm-eabi@4.54.0': @@ -2681,12 +3188,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) '@tanstack/query-core@5.90.16': {} @@ -2833,7 +3340,7 @@ snapshots: '@typescript-eslint/types': 8.51.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))': + '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -2841,7 +3348,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -2858,9 +3365,17 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-styles@4.3.0: + ansi-escapes@7.3.0: dependencies: - color-convert: 2.0.1 + environment: 1.1.0 + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} argparse@2.0.1: {} @@ -2868,8 +3383,71 @@ snapshots: dependencies: tslib: 2.8.1 + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + axios@1.13.2: dependencies: follow-redirects: 1.15.11 @@ -2904,6 +3482,18 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} caniuse-lite@1.0.30001762: {} @@ -2917,6 +3507,15 @@ snapshots: dependencies: clsx: 2.1.1 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 + clsx@2.1.1: {} color-convert@2.0.1: @@ -2925,10 +3524,14 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@14.0.3: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -2943,6 +3546,24 @@ snapshots: csstype@3.2.3: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + date-fns-jalali@4.1.0-0: {} date-fns@4.1.0: {} @@ -2953,12 +3574,28 @@ snapshots: deep-is@0.1.4: {} + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + delayed-stream@1.0.0: {} detect-libc@2.1.2: {} detect-node-es@1.1.0: {} + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2967,15 +3604,96 @@ snapshots: electron-to-chromium@1.5.267: {} + emoji-regex@10.6.0: {} + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 + environment@1.1.0: {} + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-iterator-helpers@1.3.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + safe-array-concat: 1.1.3 + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -2987,6 +3705,16 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -3048,6 +3776,28 @@ snapshots: dependencies: eslint: 9.39.2(jiti@2.6.1) + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.1 + eslint: 9.39.2(jiti@2.6.1) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.6 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -3116,6 +3866,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.4: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -3146,6 +3898,10 @@ snapshots: follow-redirects@1.15.11: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -3159,8 +3915,23 @@ snapshots: function-bind@1.1.2: {} + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3181,6 +3952,12 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -3189,12 +3966,27 @@ snapshots: globals@16.5.0: {} + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + gopd@1.2.0: {} graceful-fs@4.2.11: {} + has-bigints@1.1.0: {} + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -3211,6 +4003,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + husky@9.1.7: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3224,14 +4018,135 @@ snapshots: imurmurhash@0.1.4: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-extglob@2.1.1: {} + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + isexe@2.0.0: {} + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + jiti@2.6.1: {} js-tokens@4.0.0: {} @@ -3250,6 +4165,13 @@ snapshots: json5@2.2.3: {} + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3308,12 +4230,42 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.3 + string-argv: 0.3.2 + tinyexec: 1.0.4 + yaml: 2.8.3 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 lodash.merge@4.6.2: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3334,6 +4286,8 @@ snapshots: dependencies: mime-db: 1.52.0 + mimic-function@5.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -3348,8 +4302,55 @@ snapshots: natural-compare@1.4.0: {} + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + node-releases@2.0.27: {} + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3359,6 +4360,12 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -3375,10 +4382,14 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + picocolors@1.1.1: {} picomatch@4.0.3: {} + possible-typed-array-names@1.1.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -3393,6 +4404,12 @@ snapshots: prettier@3.7.4: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-from-env@1.1.0: {} punycode@2.3.1: {} @@ -3417,6 +4434,8 @@ snapshots: dependencies: react: 19.2.3 + react-is@16.13.1: {} + react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3): @@ -3462,8 +4481,44 @@ snapshots: react@19.2.3: {} + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + resolve-from@4.0.0: {} + resolve@2.0.0-next.6: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + rollup@4.54.0: dependencies: '@types/estree': 1.0.8 @@ -3492,6 +4547,25 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.54.0 fsevents: 2.3.3 + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + scheduler@0.27.0: {} semver@6.3.1: {} @@ -3500,20 +4574,150 @@ snapshots: set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-argv@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-json-comments@3.1.1: {} supports-color@7.2.0: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -3524,6 +4728,8 @@ snapshots: tapable@2.3.0: {} + tinyexec@1.0.4: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -3541,6 +4747,39 @@ snapshots: dependencies: prelude-ls: 1.2.1 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typescript-eslint@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) @@ -3554,6 +4793,13 @@ snapshots: typescript@5.9.3: {} + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@7.16.0: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -3581,7 +4827,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 - vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2): + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -3594,6 +4840,48 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 + yaml: 2.8.3 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 which@2.0.2: dependencies: @@ -3601,8 +4889,16 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + yallist@3.1.1: {} + yaml@2.8.3: {} + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@4.3.4): diff --git a/src/api/adapters/store.adapter.ts b/src/api/adapters/store.adapter.ts index 2b1fb14..21d42c6 100644 --- a/src/api/adapters/store.adapter.ts +++ b/src/api/adapters/store.adapter.ts @@ -1,29 +1,5 @@ -import type { - BreakTime, - RestaurantDetail, - RestaurantSummary, -} from "@/types/store"; -import type { - StoreDetailDataDTO, - StoreSearchItemDTO, -} from "@/api/dto/store.dto"; - -export function toRestaurantSummary( - dto: StoreSearchItemDTO, -): RestaurantSummary { - return { - id: Number(dto.storeId), - name: dto.name, - address: dto.address, - category: dto.category, - rating: dto.rating, - reviewCount: dto.reviewCount, - distanceKm: dto.distance, - thumbnailUrl: dto.mainImageUrl, - isOpenNow: dto.isOpenNow, - location: { lat: dto.latitude, lng: dto.longitude }, - }; -} +import type { BreakTime, RestaurantDetail } from "@/types/store"; +import type { StoreDetailDataDTO } from "@/api/dto/store.dto"; export function toRestaurantDetail(dto: StoreDetailDataDTO): RestaurantDetail { const breakTime = toBreakTime(dto.breakStartTime, dto.breakEndTime); diff --git a/src/api/auth.ts b/src/api/auth.ts index 76271a5..cd90b3a 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -30,7 +30,7 @@ export const postLogin = async ( return data.result; }; -export const postLogout = async (): Promise => { +const postLogout = async (): Promise => { const { data } = await api.delete>("/api/auth/logout"); return data.result; diff --git a/src/api/axios.ts b/src/api/axios.ts index 38110ff..33f52f5 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -36,15 +36,20 @@ api.interceptors.request.use((config: InternalAxiosRequestConfig) => { let refreshPromise: ReturnType | null = null; +type ApiResponseWithFlags = { + code?: string; + message?: string; + success?: boolean; + isSuccess?: boolean; +}; + api.interceptors.response.use( (res) => { const data = res.data; if (isApiResponse(data)) { + const responseData: ApiResponseWithFlags = data; const failed = - (typeof (data as any).success === "boolean" && - (data as any).success === false) || - (typeof (data as any).isSuccess === "boolean" && - (data as any).isSuccess === false); + responseData.success === false || responseData.isSuccess === false; if (failed) { return Promise.reject({ diff --git a/src/api/bookings.ts b/src/api/bookings.ts index 01f5356..80b37cc 100644 --- a/src/api/bookings.ts +++ b/src/api/bookings.ts @@ -1,22 +1,21 @@ import { api } from "./axios"; -export type BookingStatus = "예약 확정" | "방문 완료" | "취소됨"; +type ApiBookingStatus = "PENDING" | "CONFIRMED" | "COMPLETED" | "CANCELED"; -export type ApiBookingStatus = "CONFIRMED" | "COMPLETED" | "CANCELED"; - -export interface Booking { +interface Booking { bookingId: number; storeName: string; storeAddress: string; bookingDate: string; bookingTime: string; partySize: number; - amount: number; + tableNumbers: string; + amount: number | null; paymentMethod: string; status: ApiBookingStatus; } -export interface BookingResponse { +interface BookingResponse { bookingList: Booking[]; listSize: number; totalPage: number; @@ -25,16 +24,31 @@ export interface BookingResponse { isLast: boolean; } +type GetBookingParams = { + page: number; + status?: ApiBookingStatus; +}; -export const getBookings = async (status?: ApiBookingStatus, page: number = 1): Promise => { - const params: any = { page }; +export const getBookings = async ( + status?: ApiBookingStatus, + page: number = 1, +): Promise => { + const params: GetBookingParams = { page }; if (status) params.status = status; - const response = await api.get<{ result: BookingResponse }>("/api/v1/users/bookings", { params }); + const response = await api.get<{ result: BookingResponse }>( + "/api/v1/users/bookings", + { params }, + ); return response.data.result as BookingResponse; -}; +}; -export const cancelBooking = async (bookingId: number, reason: string = "사용자 취소") => { - const response = await api.patch(`/api/v1/bookings/${bookingId}/cancel`, { reason }); +export const cancelBooking = async ( + bookingId: number, + reason: string = "사용자 취소", +) => { + const response = await api.patch(`/api/v1/bookings/${bookingId}/cancel`, { + reason, + }); return response.data; }; diff --git a/src/api/dto/store.dto.ts b/src/api/dto/store.dto.ts index 3fc8684..1c962cf 100644 --- a/src/api/dto/store.dto.ts +++ b/src/api/dto/store.dto.ts @@ -1,10 +1,3 @@ -export type ApiResponseDTO = { - success: boolean; - code: string; - data: T; - message: string; -}; - export type CategoryDTO = | "KOREAN" | "CHINESE" @@ -12,35 +5,6 @@ export type CategoryDTO = | "WESTERN" | "CAFE"; -export type StoreSearchItemDTO = { - storeId: string; - name: string; - address: string; - category: CategoryDTO; - rating: number; - reviewCount: number; - distance: number; - mainImageUrl: string; - isOpenNow: boolean; - latitude: number; - longitude: number; -}; - -export type PaginationDTO = { - currentPage: number; - totalPages: number; - totalCount: number; - isFirst: boolean; - isLast: boolean; -}; - -export type StoreSearchDataDTO = { - stores: StoreSearchItemDTO[]; - pagination: PaginationDTO; -}; - -export type StoreSearchResponseDTO = ApiResponseDTO; - export type DayDTO = | "MONDAY" | "TUESDAY" @@ -76,5 +40,3 @@ export type StoreDetailDataDTO = { depositAmount: number; depositRate?: number | null; }; - -export type StoreDetailResponseDTO = ApiResponseDTO; diff --git a/src/api/endpoints/bookings.ts b/src/api/endpoints/bookings.ts index 24319c7..5c89b03 100644 --- a/src/api/endpoints/bookings.ts +++ b/src/api/endpoints/bookings.ts @@ -8,7 +8,7 @@ type APiResult = { result: T; }; -export type BookingListItem = { +type BookingListItem = { bookingId: number; storeName: string; storeAddress: string; @@ -26,7 +26,7 @@ export type BookingListItem = { status: string; }; -export type UserBookingsResult = { +type UserBookingsResult = { bookingList: BookingListItem[]; listSize: number; totalPage: number; diff --git a/src/api/endpoints/member.ts b/src/api/endpoints/member.ts index b7e5d3d..b1b57a1 100644 --- a/src/api/endpoints/member.ts +++ b/src/api/endpoints/member.ts @@ -8,7 +8,7 @@ type ApiEnvelope = { result: T; }; -export type MemberInfo = { +type MemberInfo = { id: number; profileImage: string | null; email: string; @@ -23,7 +23,7 @@ export async function getMemberInfo() { return res.data.result; } -export type PatchMemberInfo = { +type PatchMemberInfo = { name: string; phoneNumber: string; }; @@ -46,13 +46,13 @@ export async function putProfileImage(file: File) { return res.data.result; } -export type ChangePasswordRequest = { +type ChangePasswordRequest = { currentPassword: string; newPassword: string; newPasswordConfirm: string; }; -export type ChangePasswordResponse = { +type ChangePasswordResponse = { change: boolean; changeAt: string; message: string; diff --git a/src/api/endpoints/payments.ts b/src/api/endpoints/payments.ts index 9f7cd31..83296d5 100644 --- a/src/api/endpoints/payments.ts +++ b/src/api/endpoints/payments.ts @@ -8,7 +8,7 @@ type ApiEnvelope = { result: T; }; -export type PaymentRequestResult = { +type PaymentRequestResult = { paymentId: number; bookingId: number; orderId: string; @@ -31,7 +31,7 @@ export async function requestPayment(body: { bookingId: number }) { return res.data.result; } -export type PaymentConfirmResult = { +type PaymentConfirmResult = { paymentId: number; status: string; approvedAt: string; diff --git a/src/api/endpoints/reservations.ts b/src/api/endpoints/reservations.ts index 1639b8c..abe58e7 100644 --- a/src/api/endpoints/reservations.ts +++ b/src/api/endpoints/reservations.ts @@ -7,7 +7,7 @@ type ApiResult = { result: T; }; -export type GetAvailableTimesParams = { +type GetAvailableTimesParams = { storeId: string | number; date: string; partySize: number; @@ -38,9 +38,9 @@ export async function getAvailableTimes( return data.result?.availableTimes ?? []; } -export type SeatsTypes = "WINDOW" | "GENERAL" | string; +type SeatsTypes = "WINDOW" | "GENERAL" | string; -export type AvailableTable = { +type AvailableTable = { tableId: number; tableNumber: string; tableSeats: number; diff --git a/src/api/endpoints/stores.ts b/src/api/endpoints/stores.ts deleted file mode 100644 index e322dd7..0000000 --- a/src/api/endpoints/stores.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { api } from "../axios"; - -export type StoreCategory = - | "KOREAN" - | "CHINESE" - | "JAPANESE" - | "WESTERN" - | "CAFE"; -export type DayOfWeek = - | "MONDAY" - | "TUESDAY" - | "WEDNESDAY" - | "THURSDAY" - | "FRIDAY" - | "SATURDAY" - | "SUNDAY"; - -export type BusinessHour = { - day: DayOfWeek; - openTime: string | null; - closeTime: string | null; - isClosed: boolean; -}; - -export type StoreDetail = { - storeId: string; - storeName: string; - description?: string; - address: string; - phone?: string; - category: StoreCategory; - rating: number; - reviewCount: number; - depositAmount?: number; - mainImageUrl?: string; - tableImageUrls?: string[]; - businessHours: BusinessHour[]; - breakStartTime?: string | null; - breakEndTime?: string | null; - isOpenNow: boolean; -}; - -type ApiResult = { - isSuccess: boolean; - code: string; - message: string; - result: T; -}; - -export async function getStoreDetail(storeId: string): Promise { - const { data } = await api.get>( - `/api/v1/stores/${storeId}`, - ); - if (!data?.isSuccess) { - throw { - status: 0, - code: data?.code, - message: data?.message ?? "식당 상세 조회에 실패했습니다.", - }; - } - return data.result; -} diff --git a/src/api/owner/menus.ts b/src/api/owner/menus.ts index 6126575..d0193ed 100644 --- a/src/api/owner/menus.ts +++ b/src/api/owner/menus.ts @@ -1,7 +1,8 @@ +import axios from "axios"; import { api } from "../axios"; import type { ApiResponse } from "@/types/api"; -export interface ServerMenu { +interface ServerMenu { menuId: number; name: string; description?: string | null; @@ -52,11 +53,7 @@ interface MenuCreateResult { }[]; } -export interface DeleteMenusRequest { - menuIds: number[]; -} - -export interface DeleteMenusResponse { +interface DeleteMenusResponse { isSuccess: boolean; code: string; result: { deletedMenuIds: number[] }; @@ -100,12 +97,20 @@ export const deleteMenuImage = async ( `/api/v1/stores/${storeId}/menus/${menuId}/image`, ); return res.data; - } catch (err: any) { + } catch (err: unknown) { console.error("deleteMenuImage error", err); + if (axios.isAxiosError(err)) { + return { + isSuccess: false, + code: "_MENU_IMAGE_DELETE_FAILED", + message: err?.response?.data?.message || "이미지 삭제 실패", + result: { deletedImageKey: "" }, + }; + } return { isSuccess: false, code: "_MENU_IMAGE_DELETE_FAILED", - message: err?.response?.data?.message || "이미지 삭제 실패", + message: "이미지 삭제 실패", result: { deletedImageKey: "" }, }; } diff --git a/src/api/owner/reservation.ts b/src/api/owner/reservation.ts index 6a89c85..93eabd6 100644 --- a/src/api/owner/reservation.ts +++ b/src/api/owner/reservation.ts @@ -15,23 +15,23 @@ interface GetSlotsResult { export type SlotStatus = "AVAILABLE" | "BLOCKED"; export interface UpdateSlotRequest { - targetDate: string; - startTime: string; + targetDate: string; + startTime: string; status: SlotStatus; } -export interface UpdateSlotResult { +interface UpdateSlotResult { targetDate: string; - startTime: string; + startTime: string; status: SlotStatus; } -export interface PatchBreakTimeRequest { - breakStartTime: string; - breakEndTime: string; +interface PatchBreakTimeRequest { + breakStartTime: string; + breakEndTime: string; } -export interface BookingDetailResult { +interface BookingDetailResult { bookerName: string; partySize: number; amount: number; @@ -40,27 +40,40 @@ export interface BookingDetailResult { export const getTableSlots = (storeId: number, tableId: number, date: string) => api.get>( `/api/v1/stores/${storeId}/tables/${tableId}/slots`, - { params: { date } } + { params: { date } }, ); export const updateTableSlotStatus = ( storeId: number, tableId: number, - body: UpdateSlotRequest + body: UpdateSlotRequest, ) => api.patch>( `/api/v1/stores/${storeId}/tables/${tableId}/slots`, - body + body, ); -export const patchBreakTime = (storeId:number, body:PatchBreakTimeRequest) => { +export const patchBreakTime = ( + storeId: number, + body: PatchBreakTimeRequest, +) => { return api.patch(`/api/v1/stores/${storeId}/break-time`, body); }; -export const getBookingDetail = (storeId: number, tableId: number, bookingId: number) => - api.get>(`/api/v1/stores/${storeId}/tables/${tableId}/slots/${bookingId}`); - -export const cancelBookingByOwner = (storeId: number, tableId: number, bookingId: number) => - api.patch(`/api/v1/stores/${storeId}/tables/${tableId}/slots/${bookingId}/cancel`); - +export const getBookingDetail = ( + storeId: number, + tableId: number, + bookingId: number, +) => + api.get>( + `/api/v1/stores/${storeId}/tables/${tableId}/slots/${bookingId}`, + ); +export const cancelBookingByOwner = ( + storeId: number, + tableId: number, + bookingId: number, +) => + api.patch( + `/api/v1/stores/${storeId}/tables/${tableId}/slots/${bookingId}/cancel`, + ); diff --git a/src/api/owner/storeLayout.ts b/src/api/owner/storeLayout.ts index 546d7e0..f22c065 100644 --- a/src/api/owner/storeLayout.ts +++ b/src/api/owner/storeLayout.ts @@ -1,6 +1,7 @@ import type { ApiResponse } from "@/types/api"; import { api } from "../axios"; import type { SeatsType } from "@/types/table"; +import axios from "axios"; export interface LayoutTable { tableId: number; @@ -15,7 +16,7 @@ export interface LayoutTable { seatsType: SeatsType; } -export interface LayoutResponse { +interface LayoutResponse { layoutId: number; totalTableCount: number; gridInfo: { gridCol: number; gridRow: number }; @@ -30,7 +31,7 @@ export interface CreateTableRequest { seatsType: SeatsType; } -export interface CreateTableResponse { +interface CreateTableResponse { tableId: number; tableNumber: string; minSeatCount: number; @@ -53,8 +54,8 @@ export const getActiveLayout = async ( return null; } return null; - } catch (e: any) { - if (e.response?.status === 404) { + } catch (e: unknown) { + if (axios.isAxiosError(e) && e.response?.status === 404) { console.error("가게를 찾을 수 없음"); } else { console.error(e); @@ -97,8 +98,13 @@ export const createTable = async ( } console.error("테이블 생성 실패 응답:", res.data); return null; - } catch (e: any) { - console.error("테이블 생성 실패:", e?.response?.data ?? e); + } catch (e: unknown) { + if (axios.isAxiosError(e)) { + console.error("테이블 생성 실패:", e?.response?.data ?? e); + } else { + console.error("테이블 생성 실패:", e); + } + return null; } }; diff --git a/src/api/owner/stores.ts b/src/api/owner/stores.ts index 174aa04..093b9e4 100644 --- a/src/api/owner/stores.ts +++ b/src/api/owner/stores.ts @@ -1,7 +1,8 @@ import { api } from "@/api/axios"; import type { ApiResponse } from "@/types/api"; +import type { UpdateStoreResponse } from "@/types/store"; -export interface StoreDetail { +interface StoreDetail { storeId: number; storeName: string; description: string; @@ -13,7 +14,7 @@ export interface StoreDetail { reviewCount?: number; } -export interface BusinessHour { +interface BusinessHour { day: | "MONDAY" | "TUESDAY" @@ -27,13 +28,6 @@ export interface BusinessHour { isClosed: boolean; } -export interface Time { - hour: number; - minute: number; - second: number; - nano: number; -} - export interface MyStore { storeId: number; storeName: string; @@ -60,7 +54,7 @@ export interface TableImage { tableImageUrl: string; } -export interface TableImagesResponse { +interface TableImagesResponse { storeId: number; tableImages: TableImage[]; } @@ -77,7 +71,10 @@ export function updateStore( phoneNumber: string; }, ) { - return api.patch>(`/api/v1/stores/${storeId}`, body); + return api.patch>( + `/api/v1/stores/${storeId}`, + body, + ); } export function updateBusinessHours( diff --git a/src/api/owner/table.ts b/src/api/owner/table.ts index 78eeac4..88cb415 100644 --- a/src/api/owner/table.ts +++ b/src/api/owner/table.ts @@ -2,7 +2,7 @@ import { api } from "../axios"; import type { ApiResponse } from "@/types/api"; import type { AxiosProgressEvent } from "axios"; -export interface UploadTableImageResult { +interface UploadTableImageResult { tableId: number; tableImageUrl: string; } @@ -12,10 +12,10 @@ interface DeleteTableImageResult { } export interface PatchTableRequest { - tableNumber?: string; + tableNumber?: string; minSeatCount?: number; maxSeatCount?: number; - seatsType?: 'GENERAL' | 'WINDOW' | 'ROOM' | 'BAR' | 'OUTDOOR'; + seatsType?: "GENERAL" | "WINDOW" | "ROOM" | "BAR" | "OUTDOOR"; } export interface UpdatedTable { @@ -30,7 +30,7 @@ export const uploadTableImage = ( storeId: number, tableId: number, file: File, - onUploadProgress?: (progressEvent: AxiosProgressEvent) => void + onUploadProgress?: (progressEvent: AxiosProgressEvent) => void, ) => { const formData = new FormData(); formData.append("tableImage", file); @@ -38,17 +38,23 @@ export const uploadTableImage = ( return api.post>( `/api/v1/stores/${storeId}/tables/${tableId}/table-image`, formData, - {onUploadProgress,} + { onUploadProgress }, ); }; -export const deleteTableImage = (storeId: number, tableId: number) : Promise<{ data: ApiResponse }> => { +export const deleteTableImage = ( + storeId: number, + tableId: number, +): Promise<{ data: ApiResponse }> => { return api.delete(`/api/v1/stores/${storeId}/tables/${tableId}/table-image`); }; - -export const patchTableInfo = (storeId: number, tableId: number, body: PatchTableRequest) => +export const patchTableInfo = ( + storeId: number, + tableId: number, + body: PatchTableRequest, +) => api.patch>( `/api/v1/stores/${storeId}/tables/${tableId}`, - body -); + body, + ); diff --git a/src/components/auth/ChangePasswordDiaLog.tsx b/src/components/auth/ChangePasswordDiaLog.tsx index 22d2006..a2f3834 100644 --- a/src/components/auth/ChangePasswordDiaLog.tsx +++ b/src/components/auth/ChangePasswordDiaLog.tsx @@ -5,6 +5,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { Button } from "../ui/button"; import { X } from "lucide-react"; +import axios from "axios"; const schema = z .object({ @@ -43,8 +44,12 @@ export function ChangePasswordDialog({ form.reset(); onOpenChange(false); }, - onError: (e: any) => { - const msg = e?.response?.data?.message ?? "비밀번호 변경에 실패했습니다."; + onError: (e: unknown) => { + let msg = "비밀번호 변경에 실패했습니다."; + + if (axios.isAxiosError(e)) { + msg = e.response?.data?.message ?? msg; + } if (typeof msg === "string" && /현재|기존|일치|틀렸/.test(msg)) { form.setError("currentPassword", { type: "server", message: msg }); return; diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx index da0eb58..32880b6 100644 --- a/src/components/auth/SignupDialog.tsx +++ b/src/components/auth/SignupDialog.tsx @@ -82,7 +82,7 @@ export function SignupDialog({ return ( !open && onClose()}> - + 회원가입 diff --git a/src/components/auth/WithdrawDialog.tsx b/src/components/auth/WithdrawDialog.tsx index 2598c67..01df2b1 100644 --- a/src/components/auth/WithdrawDialog.tsx +++ b/src/components/auth/WithdrawDialog.tsx @@ -6,8 +6,10 @@ import { Button } from "../ui/button"; import { X } from "lucide-react"; import { useState } from "react"; import { logout as performLogout } from "@/api/auth"; +import axios from "axios"; -function isWithdrawBlockByBookings(e: any) { +function isWithdrawBlockByBookings(e: unknown) { + if (!axios.isAxiosError(e)) return false; const msg = e?.response?.data?.message; const result = e?.response?.data?.result; const code = e?.response?.data?.code; @@ -56,12 +58,16 @@ export function WithdrawDialog({ onOpenChange(false); nav("/", { replace: true }); }, - onError: (e: any) => { + onError: (e: unknown) => { if (isWithdrawBlockByBookings(e)) { setBlocked(true); return; } - alert(e?.response?.data?.message ?? "회원 탈퇴에 실패했습니다."); + let msg = "회원 탈퇴에 실패했습니다"; + if (axios.isAxiosError(e)) { + msg = e?.response?.data?.message ?? msg; + } + alert(msg); }, }); diff --git a/src/components/customer-support/SupportHero.tsx b/src/components/customer-support/SupportHero.tsx index 6813201..310460d 100644 --- a/src/components/customer-support/SupportHero.tsx +++ b/src/components/customer-support/SupportHero.tsx @@ -9,7 +9,7 @@ export default function SupportHero() { return ( <> -
+

무엇을 도와드릴까요?

diff --git a/src/components/customer-support/SupportModal.tsx b/src/components/customer-support/SupportModal.tsx index 800ab56..c8c4430 100644 --- a/src/components/customer-support/SupportModal.tsx +++ b/src/components/customer-support/SupportModal.tsx @@ -15,7 +15,6 @@ import { useMutation } from "@tanstack/react-query"; import { postInquiry } from "@/api/inquiry"; import { getErrorMessage } from "@/utils/error"; - interface SupportModalProps { isOpen: boolean; onClose: () => void; diff --git a/src/components/customer-support/faqData.ts b/src/components/customer-support/faqData.ts index e355448..c1b4ba5 100644 --- a/src/components/customer-support/faqData.ts +++ b/src/components/customer-support/faqData.ts @@ -1,4 +1,4 @@ -export interface FaqItem { +interface FaqItem { id: number; category: string; question: string; diff --git a/src/components/map/KakaoMap.tsx b/src/components/map/KakaoMap.tsx index 83776f8..e812e06 100644 --- a/src/components/map/KakaoMap.tsx +++ b/src/components/map/KakaoMap.tsx @@ -1,10 +1,14 @@ import { loadKakaoMapSdk } from "@/lib/kakao"; +import type { + KakaoInfoWindowInstance, + KaKaoMapInstance, + KakaoMarkerInstance, + LatLng, + MarkerWithLocation, +} from "@/types/map"; import type { RestaurantSummary } from "@/types/store"; import { useEffect, useMemo, useRef, useState } from "react"; -type LatLng = { lat: number; lng: number }; -type MarkerWithLocation = RestaurantSummary & { location: LatLng }; - type Props = { center: LatLng; markers: RestaurantSummary[]; @@ -14,22 +18,17 @@ type Props = { defaultLevel?: number; selectedLevel?: number; }; -declare global { - interface Window { - kakao: any; - } -} - const toNum = (v: unknown) => { const n = typeof v === "string" ? parseFloat(v) : Number(v); return Number.isFinite(n) ? n : null; }; -const normalizeLatLng = (loc: any): LatLng | null => { - if (!loc) return null; +const normalizeLatLng = (loc: unknown): LatLng | null => { + if (!loc || typeof loc !== "object") return null; + const maybeLoc = loc as { lat?: unknown; lng?: unknown }; - let lat = toNum(loc.lat); - let lng = toNum(loc.lng); + let lat = toNum(maybeLoc.lat); + let lng = toNum(maybeLoc.lng); if (lat == null || lng == null) return null; @@ -52,17 +51,17 @@ export default function KakaoMap({ selectedLevel, }: Props) { const containerRef = useRef(null); - const mapRef = useRef(null); - const markersRef = useRef>(new Map()); - const infoRef = useRef(null); + const mapRef = useRef(null); + const markersRef = useRef>(new Map()); + const infoRef = useRef(null); const prevSelectedIdRef = useRef(null); const safeMarkers = useMemo(() => { return markers - .map((m) => { - const norm = normalizeLatLng((m as any).location); + .map((marker) => { + const norm = normalizeLatLng((marker as MarkerWithLocation).location); if (!norm) return null; - return { ...m, location: norm } as MarkerWithLocation; + return { ...marker, location: norm } as MarkerWithLocation; }) .filter(Boolean) as MarkerWithLocation[]; }, [markers]); @@ -70,16 +69,15 @@ export default function KakaoMap({ const [sdkReady, setSdkReady] = useState(!!window.kakao?.maps); const [sdkError, setSdkError] = useState(null); - const centerRef = useRef(center); - centerRef.current = center; - const relayout = () => { const kakao = window.kakao; if (!kakao?.maps || !mapRef.current) return; try { mapRef.current.relayout(); - } catch {} + } catch { + //지도 초기화 타이밍에서 발생 가능 + } }; //1. 지도 최초 1회 생성 @@ -90,10 +88,11 @@ export default function KakaoMap({ try { await loadKakaoMapSdk(); if (cancelled) return; + setSdkReady(true); const kakao = window.kakao; - if (!containerRef.current) return; + if (!kakao?.maps || !containerRef.current) return; if (mapRef.current) return; const options = { @@ -115,7 +114,7 @@ export default function KakaoMap({ return () => { cancelled = true; }; - }, [defaultLevel]); + }, [defaultLevel, center.lat, center.lng]); // 2. 컨테이너 사이즈 변하면 relayout useEffect(() => { @@ -163,21 +162,22 @@ export default function KakaoMap({ //5. 마커 바뀌면 마커 재생성 useEffect(() => { const kakao = window.kakao; - if (!kakao?.maps || !mapRef.current) return; + const maps = kakao?.maps; + if (!maps || !mapRef.current) return; markersRef.current.forEach((mk) => mk.setMap(null)); markersRef.current.clear(); safeMarkers.forEach((store) => { - const pos = new kakao.maps.LatLng(store.location.lat, store.location.lng); - const marker = new kakao.maps.Marker({ + const pos = new maps.LatLng(store.location.lat, store.location.lng); + const marker = new maps.Marker({ map: mapRef.current, position: pos, clickable: true, zIndex: 1, }); - kakao.maps.event.addListener(marker, "click", () => { + maps.event.addListener(marker, "click", () => { mapRef.current?.panTo(pos); if (selectedLevel != null) { mapRef.current?.setLevel(selectedLevel); @@ -195,7 +195,7 @@ export default function KakaoMap({ markersRef.current.set(store.id, marker); }); relayout(); - }, [safeMarkers, onSelectMarker]); + }, [safeMarkers, onSelectMarker, selectedLevel]); //6. 선택변경시 zIndex 처리 useEffect(() => { @@ -215,22 +215,22 @@ export default function KakaoMap({ //7. 선택 없으면 bounds 맞추기 useEffect(() => { const kakao = window.kakao; - if (!kakao?.maps || !mapRef.current) return; - if (selectedId != null) return; - if (safeMarkers.length === 0) return; - const bounds = new kakao.maps.LatLngBounds(); + const maps = kakao?.maps; + if (!maps || !mapRef.current) return; + if (selectedId != null || safeMarkers.length === 0) return; + const bounds = new maps.LatLngBounds(); safeMarkers.forEach((store) => { - bounds.extend( - new kakao.maps.LatLng(store.location.lat, store.location.lng), - ); + bounds.extend(new maps.LatLng(store.location.lat, store.location.lng)); }); requestAnimationFrame(() => { try { - mapRef.current.relayout(); - mapRef.current.setBounds(bounds); - } catch {} + mapRef.current?.relayout(); + mapRef.current?.setBounds(bounds); + } catch { + //지도 초기화 타이밍 이슈무시 + } }); if (safeMarkers.length === 1 && defaultLevel != null) { diff --git a/src/components/owner/BreakTimeModal.tsx b/src/components/owner/BreakTimeModal.tsx index 93a649f..8d691a7 100644 --- a/src/components/owner/BreakTimeModal.tsx +++ b/src/components/owner/BreakTimeModal.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; -import { X, Clock } from 'lucide-react'; +import React, { useState } from "react"; +import { X, Clock } from "lucide-react"; export interface BreakTime { start: string; - end: string; + end: string; } interface Props { @@ -13,34 +13,48 @@ interface Props { onConfirm: (breakTime: BreakTime) => void; } - const BreakTimeModal: React.FC = ({ openTime, closeTime, onClose, onConfirm, }) => { - const [start, setStart] = useState('14:00'); - const [end, setEnd] = useState('15:00'); + const [start, setStart] = useState("14:00"); + const [end, setEnd] = useState("15:00"); + const toMinutes = (t: string) => { + const [h, m] = t.split(":").map(Number); + return h * 60 + m; + }; const isInvalid = - start >= end || - start < openTime || - end > closeTime; - + toMinutes(start) >= toMinutes(end) || + toMinutes(start) < toMinutes(openTime) || + toMinutes(end) > toMinutes(closeTime); return (

-
e.stopPropagation()}> - -
+
브레이크 타임 설정
@@ -72,7 +86,6 @@ const BreakTimeModal: React.FC = ({ className="w-full mt-1 border rounded-lg p-2 cursor-pointer" />
-
@@ -91,8 +104,8 @@ const BreakTimeModal: React.FC = ({ }} className={`flex-1 rounded-lg py-2 font-bold ${ isInvalid - ? 'bg-gray-300 cursor-not-allowed' - : 'bg-orange-500 hover:bg-orange-300 text-white' + ? "bg-gray-300 cursor-not-allowed" + : "bg-orange-500 hover:bg-orange-300 text-white" }`} > 브레이크 타임 추가 diff --git a/src/components/owner/MenuManagement.tsx b/src/components/owner/MenuManagement.tsx index 8626689..499dde5 100644 --- a/src/components/owner/MenuManagement.tsx +++ b/src/components/owner/MenuManagement.tsx @@ -3,28 +3,30 @@ import { Plus, Pencil, Trash2 } from "lucide-react"; import MenuFormModal from "./menuFormModal"; import { deleteMenus } from "@/api/owner/menus"; import { getMenus, updateMenuSoldOut } from "@/api/owner/menus"; +import type { MenuCategory, MenuItem } from "@/types/menus"; interface MenuManagementProps { storeId?: string; } interface Category { - id: string; + id: CategoryType; label: string; } -interface LocalMenu { - id: string; - name: string; +type ServerMenu = { + menuId?: number | string; + name?: string; description?: string; - price: number; + price?: number; category?: string; - imageUrl?: string | null; + imageUrl?: string; + imageKey?: string; isSoldOut?: boolean; isActive?: boolean; -} +}; -type CategoryType = string; +type CategoryType = "ALL" | MenuCategory; const MenuManagement: React.FC = ({ storeId }) => { const restaurantId = storeId; @@ -37,24 +39,27 @@ const MenuManagement: React.FC = ({ storeId }) => { { id: "ALCOHOL", label: "주류" }, ]; - const [menus, setMenus] = useState([]); - + const [menus, setMenus] = useState([]); const [categories] = useState(DEFAULT_CATEGORIES); const [activeCategory, setActiveCategory] = useState("ALL"); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); - const [editingMenu, setEditingMenu] = useState(null); + const [editingMenu, setEditingMenu] = useState(null); - const mapServerToLocal = (s: any): LocalMenu => ({ + const mapServerToLocal = ( + s: ServerMenu, + restaurantId?: string, + ): MenuItem => ({ id: String(s.menuId ?? `MENU_${Date.now()}`), + restaurantId: restaurantId ?? "", name: s.name ?? "", description: s.description ?? "", price: s.price ?? 0, - category: s.category ?? undefined, - imageUrl: s.imageUrl ?? null, - isSoldOut: !!s.isSoldOut, - isActive: true, + category: (s.category as MenuCategory) ?? "MAIN", + imageUrl: s.imageUrl ?? undefined, + isSoldOut: s.isSoldOut ?? false, + isActive: s.isActive ?? true, }); useEffect(() => { @@ -63,9 +68,13 @@ const MenuManagement: React.FC = ({ storeId }) => { : "menu-items-temp"; const savedMenus = localStorage.getItem(STORAGE_KEY_MENU); + + let parsedSavedMenus: MenuItem[] = []; + if (savedMenus) { try { - setMenus(JSON.parse(savedMenus)); + parsedSavedMenus = JSON.parse(savedMenus) as MenuItem[]; + setMenus(parsedSavedMenus); } catch { setMenus([]); } @@ -81,9 +90,13 @@ const MenuManagement: React.FC = ({ storeId }) => { try { const res = await getMenus(restaurantId); if (res.isSuccess && res.result && Array.isArray(res.result.menus)) { - const serverMenus = res.result.menus.map(mapServerToLocal); + const serverMenus = (res.result.menus as ServerMenu[]).map((menu) => + mapServerToLocal(menu, restaurantId), + ); - const localTempMenus = menus.filter((m) => m.id.startsWith("MENU_")); + const localTempMenus = parsedSavedMenus.filter((m) => + m.id.startsWith("MENU_"), + ); const mergedMenus = [...serverMenus, ...localTempMenus]; @@ -92,7 +105,7 @@ const MenuManagement: React.FC = ({ storeId }) => { } else { setError(res.message || "메뉴를 가져오는 중 문제가 발생했습니다."); } - } catch (err: any) { + } catch (err: unknown) { console.error("getMenus error", err); setError("메뉴를 불러오는 데 실패했습니다. 네트워크를 확인해주세요."); } finally { @@ -108,7 +121,7 @@ const MenuManagement: React.FC = ({ storeId }) => { } }, [menus, restaurantId]); - const handleFormSubmit = (menuData: any) => { + const handleFormSubmit = (menuData: MenuItem) => { setMenus((prev) => { const incomingId = menuData.id ? String(menuData.id) : null; @@ -130,7 +143,7 @@ const MenuManagement: React.FC = ({ storeId }) => { setEditingMenu(null); }; - const handleEditClick = (menu: any) => { + const handleEditClick = (menu: MenuItem) => { setEditingMenu(menu); setIsModalOpen(true); }; @@ -196,7 +209,7 @@ const MenuManagement: React.FC = ({ storeId }) => { } else { alert("품절 상태 변경 실패: " + res.message); } - } catch (err: any) { + } catch (err: unknown) { console.error("updateMenuSoldOut error", err); alert("품절 상태 변경 중 오류가 발생했습니다."); } @@ -332,14 +345,14 @@ const MenuManagement: React.FC = ({ storeId }) => { onClose={() => setIsModalOpen(false)} onSubmit={handleFormSubmit} categories={categories} - editingMenu={editingMenu} + editingMenu={editingMenu ?? undefined} storeId={storeId!} onImageDelete={() => { if (!editingMenu) return; setMenus((prev) => prev.map((m) => m.id === editingMenu.id - ? { ...m, imageUrl: null, imageKey: null } + ? { ...m, imageUrl: undefined, imageKey: undefined } : m, ), ); diff --git a/src/components/owner/StoreSettings.tsx b/src/components/owner/StoreSettings.tsx index 5028a79..0f54b08 100644 --- a/src/components/owner/StoreSettings.tsx +++ b/src/components/owner/StoreSettings.tsx @@ -11,14 +11,16 @@ import { type TableImage, deleteTableImages, } from "@/api/owner/stores"; +import type { Day } from "@/types/store"; interface StoreSettingsProps { storeId?: string; } -const days = ["월", "화", "수", "목", "금", "토", "일"]; +const days = ["월", "화", "수", "목", "금", "토", "일"] as const; +type DayKor = (typeof days)[number]; -const dayMapFromApi: Record = { +const dayMapFromApi: Record = { MONDAY: "월", TUESDAY: "화", WEDNESDAY: "수", @@ -28,7 +30,7 @@ const dayMapFromApi: Record = { SUNDAY: "일", }; -const dayMapToApi: Record = { +const dayMapToApi: Record = { 월: "MONDAY", 화: "TUESDAY", 수: "WEDNESDAY", @@ -47,7 +49,7 @@ const StoreSettings: React.FC = ({ storeId }) => { const [openTime, setOpenTime] = useState("11:00"); const [closeTime, setCloseTime] = useState("22:00"); - const [closedDays, setClosedDays] = useState([]); + const [closedDays, setClosedDays] = useState([]); const [reservationPeriod, setReservationPeriod] = useState("1주일 전까지"); const [minGuests, setMinGuests] = useState(1); @@ -87,12 +89,13 @@ const StoreSettings: React.FC = ({ storeId }) => { setNewFiles([]); } catch (e) { alert("가게 정보를 불러오는데 실패했습니다."); + console.error(e); return; } })(); }, [storeId]); - const toggleDay = (day: string) => { + const toggleDay = (day: DayKor) => { setClosedDays((prev) => prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day], ); @@ -170,7 +173,7 @@ const StoreSettings: React.FC = ({ storeId }) => {
= ({ storeId }) => { value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="전화번호를 입력하세요" - className={`${inputStyle} pl-12`} />
@@ -188,14 +190,12 @@ const StoreSettings: React.FC = ({ storeId }) => {
@@ -211,7 +211,7 @@ const StoreSettings: React.FC = ({ storeId }) => {
= ({ storeId }) => {
= ({ storeId }) => { }); } catch (e) { alert("기본 정보 저장에 실패했습니다."); + console.error(e); return; } @@ -454,6 +455,7 @@ const StoreSettings: React.FC = ({ storeId }) => { alert( "영업시간 저장에 실패했습니다. 기본 정보는 저장되었습니다.", ); + console.error(e); return; } try { @@ -462,6 +464,7 @@ const StoreSettings: React.FC = ({ storeId }) => { } } catch (e) { alert("이미지 삭제에 실패했습니다."); + console.error(e); return; } try { @@ -470,6 +473,7 @@ const StoreSettings: React.FC = ({ storeId }) => { } } catch (e) { alert("이미지 업로드에 실패했습니다."); + console.error(e); return; } diff --git a/src/components/owner/TableCreateModal.tsx b/src/components/owner/TableCreateModal.tsx index fc092e7..cd77731 100644 --- a/src/components/owner/TableCreateModal.tsx +++ b/src/components/owner/TableCreateModal.tsx @@ -1,55 +1,100 @@ -import React, { useState } from 'react'; -import { X } from 'lucide-react'; +import React, { useState } from "react"; +import { X } from "lucide-react"; -interface Props { - onClose: () => void; - onConfirm: (cols: number, rows: number) => void; +interface Props { + onClose: () => void; + onConfirm: (cols: number, rows: number) => void; } const TableCreateModal: React.FC = ({ onClose, onConfirm }) => { const [cols, setCols] = useState(4); const [rows, setRows] = useState(3); - return ( -
-
e.stopPropagation()}> - -

테이블 생성하기

- +
+
e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="table-create-modal-title" + > + +

+ 테이블 생성하기 +

+
- - + 가로 줄 수 (Columns) + + setCols(Number(e.target.value))} - className="w-full border rounded-xl p-3 focus:ring-2 focus:ring-blue-500 outline-none" + value={cols} + onChange={(e) => { + const next = Number.parseInt(e.target.value, 10); + setCols(Number.isFinite(next) ? next : 1); + }} + className="w-full border rounded-xl p-3 focus:ring-2 focus:ring-blue-500 outline-none" />
- - + 세로 줄 수 (Rows) + + setRows(Number(e.target.value))} - className="w-full border rounded-xl p-3 focus:ring-2 focus:ring-blue-500 outline-none" + value={rows} + onChange={(e) => { + const next = Number.parseInt(e.target.value, 10); + setRows(Number.isFinite(next) ? next : 1); + }} + className="w-full border rounded-xl p-3 focus:ring-2 focus:ring-blue-500 outline-none" />
- - + - ))} - -
- - ); -}; - -export default OwnerHeader; diff --git a/src/components/owner/tableDashboard.tsx b/src/components/owner/tableDashboard.tsx index d0b0284..aad17ce 100644 --- a/src/components/owner/tableDashboard.tsx +++ b/src/components/owner/tableDashboard.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Store, Plus, Clock, Pencil, Check, X, Lightbulb } from "lucide-react"; import TableCreateModal from "./TableCreateModal"; import BreakTimeModal, { type BreakTime } from "./BreakTimeModal"; @@ -11,17 +11,23 @@ import { type CreateTableRequest, type LayoutTable, } from "@/api/owner/storeLayout"; -import { patchTableInfo, type UpdatedTable } from "@/api/owner/table"; +import { + patchTableInfo, + type PatchTableRequest, + type UpdatedTable, +} from "@/api/owner/table"; import { patchBreakTime } from "@/api/owner/reservation"; -import type { SeatsType } from "@/types/table"; +import { SEATS_TYPE_LABEL, type SeatsType } from "@/types/table"; import TableDetailModal from "./tableDetailModal"; +import { getErrorMessage } from "@/utils/error"; +import axios from "axios"; interface TableDashboardProps { storeId: number; storeName?: string; } -export interface TableInfo { +interface TableInfo { tableId: number; numValue: number; minCapacity: number; @@ -89,7 +95,7 @@ const TableDashboard: React.FC = ({ ? `table-dashboard-state-${storeId}` : "table-dashboard-state-temp"; - const getSavedData = () => { + const initialData = (() => { try { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : null; @@ -97,9 +103,7 @@ const TableDashboard: React.FC = ({ console.error("로컬스토리지 파싱 실패", e); return null; } - }; - - const initialData = useMemo(() => getSavedData(), []); + })(); const [config, setConfig] = useState<{ columns: number; rows: number }>( initialData?.config ?? { columns: 0, rows: 0 }, @@ -241,7 +245,7 @@ const TableDashboard: React.FC = ({ isEditingNum: false, isSaved: true, tableImageUrl: - (t as any).tableImageUrl ?? + t.tableImageUrl ?? tableData[t.gridY * layout.gridInfo.gridCol + t.gridX + 1] ?.tableImageUrl ?? null, @@ -272,7 +276,7 @@ const TableDashboard: React.FC = ({ }; fetchLayout(); - }, [storeId]); + }, [storeId, tableData]); const handleCreateLayout = async (columns: number, rows: number) => { if (!storeId) return; @@ -311,8 +315,7 @@ const TableDashboard: React.FC = ({ ]); const slotId = (data.gridY - 1) * config.columns + data.gridX; - const extractedNum = - extractLeadingNumber((newTable as any).tableNumber) ?? slotId; + const extractedNum = extractLeadingNumber(newTable.tableNumber) ?? slotId; setTableData((prev) => ({ ...prev, @@ -370,17 +373,25 @@ const TableDashboard: React.FC = ({ setSelectedTable(null); alert("테이블이 삭제되었습니다."); - } catch (e: any) { - const status = e?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + const message = getErrorMessage(error); if (status === 400) { - alert("미래 예약이 있어 삭제할 수 없습니다. 예약을 먼저 취소해주세요."); + alert( + message || + "미래 예약이 있어 삭제할 수 없습니다. 예약을 먼저 취소해주세요.", + ); } else if (status === 404) { alert( - "가게 또는 테이블을 찾을 수 없습니다. 새로고침 후 다시 시도하세요.", + message || + "가게 또는 테이블을 찾을 수 없습니다. 새로고침 후 다시 시도하세요.", ); } else { - console.error(e); - alert("테이블 삭제 중 오류가 발생했습니다. 콘솔을 확인하세요."); + alert( + message || "테이블 삭제 중 오류가 발생했습니다. 콘솔을 확인하세요.", + ); } } }; @@ -389,9 +400,9 @@ const TableDashboard: React.FC = ({ tableNumber?: number | null; min?: number | null; max?: number | null; - seatsType?: string | null; + seatsType?: PatchTableRequest["seatsType"] | null; }) => { - const body: any = {}; + const body: PatchTableRequest = {}; if (opts.tableNumber !== null && opts.tableNumber !== undefined) body.tableNumber = String(opts.tableNumber); if (opts.min !== null && opts.min !== undefined) @@ -417,7 +428,7 @@ const TableDashboard: React.FC = ({ tableNumber?: number; min?: number; max?: number; - seatsType?: string; + seatsType?: PatchTableRequest["seatsType"]; }, ) => { if (!storeId) { @@ -433,8 +444,9 @@ const TableDashboard: React.FC = ({ max: changes.max ?? null, seatsType: changes.seatsType ?? null, }); - } catch (err: any) { - alert(err.message); + } catch (error: unknown) { + const message = getErrorMessage(error); + alert(message); return; } @@ -479,10 +491,8 @@ const TableDashboard: React.FC = ({ ...pt, tableInfo: { ...pt.tableInfo, - minCapacity: - (match as any).minSeatCount ?? pt.tableInfo.minCapacity, - maxCapacity: - (match as any).maxSeatCount ?? pt.tableInfo.maxCapacity, + minCapacity: match.minSeatCount ?? pt.tableInfo.minCapacity, + maxCapacity: match.maxSeatCount ?? pt.tableInfo.maxCapacity, numValue: extractLeadingNumber(match.tableNumber) ?? pt.tableInfo.numValue, @@ -494,15 +504,19 @@ const TableDashboard: React.FC = ({ ); alert("테이블 정보가 업데이트 되었습니다."); - } catch (e: any) { - const status = e?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + const message = getErrorMessage(error); if (status === 400) { alert("잘못된 요청입니다. (좌석 범위 오류 또는 수정 필드 없음)"); } else if (status === 404) { alert("가게 또는 테이블을 찾을 수 없습니다."); } else { - console.error(e); - alert("테이블 수정 중 오류가 발생했습니다. 콘솔을 확인하세요."); + alert( + message || "테이블 수정 중 오류가 발생했습니다. 콘솔을 확인하세요", + ); } } }; @@ -546,32 +560,24 @@ const TableDashboard: React.FC = ({ } alert("브레이크 타임이 설정되었습니다."); - } catch (err: any) { - console.error("브레이크타임 설정 실패", err?.response?.data ?? err); - const status = err?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + const message = getErrorMessage(error); + if (status === 400) { - alert( - err?.response?.data?.message ?? "잘못된 브레이크타임 요청입니다.", - ); + alert(message || "잘못된 브레이크타임 요청입니다."); } else if (status === 404) { alert("가게를 찾을 수 없습니다."); } else { alert( - err?.response?.data?.message ?? - "브레이크타임 설정에 실패했습니다. 콘솔을 확인하세요.", + message || "브레이크타임 설정에 실패했습니다. 콘솔을 확인하세요.", ); } } }; - const SEATS_TYPE_LABEL: Record = { - GENERAL: "일반석", - WINDOW: "창가석", - ROOM: "룸", - BAR: "바 좌석", - OUTDOOR: "야외석", - }; - return (
@@ -689,7 +695,7 @@ const TableDashboard: React.FC = ({ }} > {Array.from({ length: config.columns * config.rows }).map( - (_: any, i: number) => { + (_, i: number) => { const id = i + 1; const table = getTableData(id); const style = getTableStyle(table.maxCapacity); @@ -866,7 +872,7 @@ const TableDashboard: React.FC = ({ e.stopPropagation(); startEditingCapacity(id); }} - className={`${style.badge} text-white px-2 py-2 rounded-sm text-xs shadow-md min-w-[60px] text-center transition-transform active:scale-95`} + className={`${style.badge} text-white px-2 py-2 rounded-sm text-xs shadow-md min-w-15 text-center transition-transform active:scale-95`} > {table.minCapacity}~{table.maxCapacity}인
diff --git a/src/components/owner/tableDetailModal.tsx b/src/components/owner/tableDetailModal.tsx index a116ee7..723c5ba 100644 --- a/src/components/owner/tableDetailModal.tsx +++ b/src/components/owner/tableDetailModal.tsx @@ -30,7 +30,9 @@ import { uploadTableImage, } from "@/api/owner/table"; import { cancelBookingByOwner } from "@/api/owner/reservation"; -import type { SeatsType } from "@/types/table"; +import { SEATS_TYPE_LABEL, type SeatsType } from "@/types/table"; +import axios, { type AxiosProgressEvent } from "axios"; +import { getErrorMessage } from "@/utils/error"; interface TableInfo { minCapacity: number; @@ -159,9 +161,9 @@ const TableDetailModal: React.FC = ({ onUpdateCapacity(Number(tempMin), Number(tempMax)); setIsEditing(false); - } catch (e: any) { - console.error("테이블 정보 수정 실패", e?.response?.data ?? e); - alert(e?.response?.data?.message ?? "테이블 정보 수정에 실패했습니다."); + } catch (error: unknown) { + const message = getErrorMessage(error); + alert(message); } }; @@ -185,11 +187,10 @@ const TableDetailModal: React.FC = ({ setError(null); const res = await getTableSlots(storeId, tableId, formatDate(date)); setSlots(res.data.result.slots); - } catch (e: any) { - console.error("슬롯 조회 실패", e?.response?.data ?? e); - setError( - e?.response?.data?.message ?? "예약 정보를 불러오지 못했습니다.", - ); + } catch (error: unknown) { + const message = + getErrorMessage(error) || "예약 정보를 불러오지 못했습니다"; + setError(message); } finally { setLoading(false); } @@ -214,15 +215,14 @@ const TableDetailModal: React.FC = ({ partySize: result.partySize, amount: result.amount, }); - } catch (e: any) { - console.error("예약 상세 조회 실패", e?.response?.data ?? e); - const status = e?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + const message = getErrorMessage(error); if (status === 403) setDetailError("접근 권한이 없습니다."); else if (status === 404) setDetailError("해당 예약을 찾을 수 없습니다."); - else - setDetailError( - e?.response?.data?.message ?? "예약 상세를 불러오지 못했습니다.", - ); + else setDetailError(message || "예약 상세 내용을 불러오지 못했습니다"); setBookingDetail(null); setBookingDetailBookingId(null); } finally { @@ -257,15 +257,16 @@ const TableDetailModal: React.FC = ({ await updateTableSlotStatus(storeId, tableId, payload); await fetchSlots(selectedFullDate); - } catch (e: any) { - const statusCode = e?.response?.status; - if (statusCode === 404 && nextStatus === "AVAILABLE") { + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + if (status === 404 && nextStatus === "AVAILABLE") { await fetchSlots(selectedFullDate); return; } - - console.error("슬롯 상태 변경 실패", e?.response?.data ?? e); - alert(e?.response?.data?.message ?? "슬롯 상태 변경에 실패했습니다."); + const message = getErrorMessage(error); + alert(message || "슬롯 상태 변경에 실패했습니다"); } finally { setLoading(false); } @@ -301,7 +302,7 @@ const TableDetailModal: React.FC = ({ storeId, tableId, selectedFile, - (ev) => { + (ev: AxiosProgressEvent) => { if (ev.total) setUploadProgress(Math.round((ev.loaded / ev.total) * 100)); }, @@ -314,9 +315,9 @@ const TableDetailModal: React.FC = ({ if (onImageUpload) onImageUpload(tableId, newUrl); alert("이미지 업로드에 성공했습니다."); - } catch (err: any) { - console.error("이미지 업로드 실패", err?.response?.data ?? err); - alert(err?.response?.data?.message ?? "이미지 업로드에 실패했습니다."); + } catch (error: unknown) { + const message = getErrorMessage(error) || "이미지 업로드에 실패했습니다"; + alert(message); } finally { setUploading(false); } @@ -344,11 +345,10 @@ const TableDetailModal: React.FC = ({ } else { alert("이미지 삭제 실패: " + (res.data.message ?? "알 수 없는 오류")); } - } catch (err: any) { - console.error("이미지 삭제 실패", err?.response?.data ?? err); - alert( - err?.response?.data?.message ?? "이미지 삭제 중 오류가 발생했습니다.", - ); + } catch (error: unknown) { + const message = + getErrorMessage(error) || "이미지 삭제 중 오류가 발생했습니다"; + alert(message); } }; @@ -370,25 +370,32 @@ const TableDetailModal: React.FC = ({ setShowBookingDetail(false); setBookingDetail(null); if (selectedFullDate) fetchSlots(selectedFullDate); - } catch (err: any) { - console.error("예약 취소 실패", err?.response?.data ?? err); - const status = err?.response?.status; - if (status === 403) alert("접근 권한이 없습니다."); - else if (status === 404) alert("예약 정보를 찾을 수 없습니다."); - else alert(err?.response?.data?.message ?? "예약 취소에 실패했습니다."); + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + + if (status === 403) { + alert("접근 권한이 없습니다"); + return; + } + if (status === 404) { + alert("예약 정보를 찾을 수 없습니다"); + setShowBookingDetail(false); + setBookingDetail(null); + setBookingDetailBookingId(null); + if (selectedFullDate) { + await fetchSlots(selectedFullDate); + } + return; + } + const message = getErrorMessage(error); + alert(message || "예약 취소에 실패했습니다"); } finally { setDetailLoading(false); } }; - const SEATS_TYPE_LABEL: Record = { - GENERAL: "일반석", - WINDOW: "창가석", - ROOM: "룸", - BAR: "바 좌석", - OUTDOOR: "야외석", - }; - const capacityText = `${tableInfo.minCapacity}~${tableInfo.maxCapacity}인`; const tableType = getTableType(tableInfo.maxCapacity); @@ -424,7 +431,7 @@ const TableDetailModal: React.FC = ({ className="bg-white w-full max-w-2xl rounded-lg shadow-2xl overflow-hidden flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200" onClick={(e) => e.stopPropagation()} > -
+
{step !== "DETAIL" && (
-
+
인원
@@ -841,7 +848,7 @@ const TableDetailModal: React.FC = ({ 날짜 변경
-
+
{slots.map((slot) => { const isBreak = isBreakTime(slot.time, breakTimes); const isAvailable = !isBreak && slot.status === "AVAILABLE"; diff --git a/src/components/profile/profileAvatar.tsx b/src/components/profile/profileAvatar.tsx index 095b5d8..4bb00d6 100644 --- a/src/components/profile/profileAvatar.tsx +++ b/src/components/profile/profileAvatar.tsx @@ -4,7 +4,7 @@ interface Props { export default function ProfileAvatar({ name }: Props) { return ( -
+
{name[0]}
); diff --git a/src/components/reservation/modals/PaymentModal.tsx b/src/components/reservation/modals/PaymentModal.tsx index 24033f6..c352af9 100644 --- a/src/components/reservation/modals/PaymentModal.tsx +++ b/src/components/reservation/modals/PaymentModal.tsx @@ -26,6 +26,16 @@ type Props = { draft: ReservationDraft; booking: CreateBookingResult | null; }; +type TossPaymentsInstance = Awaited>; +type TossWidgetsInstance = ReturnType; + +type PaymentMethodWidgetInstance = Awaited< + ReturnType +>; + +type AgreementWidgetInstance = Awaited< + ReturnType +>; export default function PaymentModal({ open, @@ -48,10 +58,12 @@ export default function PaymentModal({ const amount = booking?.totalDeposit ?? 0; - const paymentMethodWidgetRef = useRef(null); - const agreementWidgetRef = useRef(null); + const paymentMethodWidgetRef = useRef( + null, + ); + const agreementWidgetRef = useRef(null); - const widgetsRef = useRef(null); + const widgetsRef = useRef(null); const initedRef = useRef(false); const payOrderRef = useRef<{ @@ -78,14 +90,17 @@ export default function PaymentModal({ if (!booking) return; let cancelled = false; - (async () => { + const paymentContainer = paymentMethodContainerRef.current; + const agreementContainer = agreementContainerRef.current; + + void (async () => { try { setLoading(true); - if (paymentMethodContainerRef.current) { - paymentMethodContainerRef.current.innerHTML = ""; + if (paymentContainer) { + paymentContainer.innerHTML = ""; } - if (agreementContainerRef.current) { - agreementContainerRef.current.innerHTML = ""; + if (agreementContainer) { + agreementContainer.innerHTML = ""; } const clientKey = import.meta.env.VITE_TOSS_CLIENT_KEY as | string @@ -108,6 +123,7 @@ export default function PaymentModal({ const tossPayments = await loadTossPayments(clientKey); if (cancelled) return; + const customerKey = `user_${userId}`; const widgets = tossPayments.widgets({ customerKey }); widgetsRef.current = widgets; @@ -139,20 +155,20 @@ export default function PaymentModal({ try { paymentMethodWidgetRef.current?.destroy?.(); agreementWidgetRef.current?.destroy?.(); - } catch {} - + } catch (error: unknown) { + console.error("토스 위젯 정리중 오류발생", error); + } paymentMethodWidgetRef.current = null; agreementWidgetRef.current = null; - widgetsRef.current = null; initedRef.current = false; payOrderRef.current = null; - if (paymentMethodContainerRef.current) { - paymentMethodContainerRef.current.innerHTML = ""; + if (paymentContainer) { + paymentContainer.innerHTML = ""; } - if (agreementContainerRef.current) { - agreementContainerRef.current.innerHTML = ""; + if (agreementContainer) { + agreementContainer.innerHTML = ""; } }; }, [open, booking, nav, userId]); @@ -193,7 +209,7 @@ export default function PaymentModal({ className="fixed inset-0 z-60 flex items-center justify-center p-4" role="dialog" aria-modal="true" - aria-label="예약 내용 확인모달" + aria-label="예약금 결제 모달" >
-
+
매장
{restaurant.name}
diff --git a/src/components/reservation/modals/ReservationCompleteModal.tsx b/src/components/reservation/modals/ReservationCompleteModal.tsx index 4e2899c..5090491 100644 --- a/src/components/reservation/modals/ReservationCompleteModal.tsx +++ b/src/components/reservation/modals/ReservationCompleteModal.tsx @@ -10,8 +10,8 @@ import { useEffect } from "react"; type Props = { open: boolean; - restaurant: RestaurantDetail; - draft: ReservationDraft; + restaurant: Pick; + draft: Pick; onClose: () => void; autoCloseMs?: number; }; diff --git a/src/components/reservation/modals/ReservationConfirmModal.tsx b/src/components/reservation/modals/ReservationConfirmModal.tsx index e14015c..f08aabf 100644 --- a/src/components/reservation/modals/ReservationConfirmModal.tsx +++ b/src/components/reservation/modals/ReservationConfirmModal.tsx @@ -9,6 +9,7 @@ import type { ReservationDraft } from "@/types/restaurant"; import type { RestaurantDetail } from "@/types/store"; import { toYmd } from "@/utils/date"; import { toDepositRate } from "@/utils/depositRate"; +import { getErrorMessage } from "@/utils/error"; import { calcMenuTotal } from "@/utils/menu"; import { backdropMotionClass, panelMotionClass } from "@/utils/modalMotion"; import { formatKrw } from "@/utils/money"; @@ -59,16 +60,21 @@ export default function ReservationConfirmModal({ return; } const tableId = draft.tableId; + const time = draft.time; if (!restaurant.id) return; if (createBookingMutation.isPending) return; + + if (!time) { + alert("예약 시간을 먼저 선택해주세요"); + return; + } if (typeof tableId !== "number" || tableId <= 0) { alert("테이블을 먼저 선택해주세요"); return; } const body = { date: toYmd(draft.date), - time: draft.time, - + time, partySize: draft.people, tableIds: [tableId], menuItems: (draft.selectedMenus ?? []).map((m) => ({ @@ -84,9 +90,9 @@ export default function ReservationConfirmModal({ body, }); onConfirm(result); - } catch (err) { - const msg = (err as any)?.message ?? "예약 생성에 실패했습니다."; - alert(msg); + } catch (error) { + const message = getErrorMessage(error) || "예약 생성에 실패했습니다"; + alert(message); } }; diff --git a/src/components/reservation/modals/ReservationMenuModal.tsx b/src/components/reservation/modals/ReservationMenuModal.tsx index 0bf5061..b589f7b 100644 --- a/src/components/reservation/modals/ReservationMenuModal.tsx +++ b/src/components/reservation/modals/ReservationMenuModal.tsx @@ -1,7 +1,13 @@ import type { ReservationDraft } from "@/types/restaurant"; import { Minus, Plus, X } from "lucide-react"; import { Button } from "../../ui/button"; -import type { SelectedMenu, MenuCategory, MenuItem } from "@/types/menus"; +import { + type SelectedMenu, + type MenuItem, + type MenuCategory, + type UiCategory, + MenuCategoryLabel, +} from "@/types/menus"; import { useMenus } from "@/hooks/reservation/useMenus"; import { useEffect, useMemo, useState } from "react"; import { calcMenuTotal } from "@/utils/menu"; @@ -24,15 +30,6 @@ type Props = { draft: ReservationDraft; }; -const CategoryLabel: Record = { - MAIN: "메인 메뉴", - SIDE: "사이드 메뉴", - DRINK: "음료", - OTHER: "기타", -}; - -type UiCategory = MenuCategory | "OTHER"; - export default function ReservationMenuModal({ open, restaurant, @@ -48,7 +45,16 @@ export default function ReservationMenuModal({ ); useEffect(() => { - setSelectedMenus(draft.selectedMenus ?? []); + const nextMenus = draft.selectedMenus ?? []; + const raf = requestAnimationFrame(() => { + setSelectedMenus((prev) => { + if (JSON.stringify(prev) === JSON.stringify(nextMenus)) { + return prev; + } + return nextMenus; + }); + }); + return () => cancelAnimationFrame(raf); }, [open, draft.selectedMenus]); const qtyMap = useMemo(() => { @@ -68,7 +74,9 @@ export default function ReservationMenuModal({ return "SIDE"; case "DRINK": case "BEVERAGE": - return "DRINK"; + return "BEVERAGE"; + case "ALCOHOL": + return "ALCOHOL"; default: return "OTHER"; } @@ -78,7 +86,8 @@ export default function ReservationMenuModal({ const by: Record = { MAIN: [], SIDE: [], - DRINK: [], + BEVERAGE: [], + ALCOHOL: [], OTHER: [], }; for (const m of activeMenus ?? []) { @@ -174,116 +183,118 @@ export default function ReservationMenuModal({ 아직 등록된 메뉴가 없어요
) : ( - (["MAIN", "SIDE", "DRINK"] as MenuCategory[]).map((cat) => { - const list = grouped[cat]; - if (list.length === 0) return null; - const safeLabel = CategoryLabel[cat] ?? "기타"; + (["MAIN", "SIDE", "BEVERAGE", "ALCOHOL"] as MenuCategory[]).map( + (cat) => { + const list = grouped[cat]; + if (list.length === 0) return null; + const safeLabel = MenuCategoryLabel[cat] ?? "기타"; - return ( -
-
{safeLabel}
-
- {list.map((menu) => { - const qty = qtyMap.get(Number(menu.id)) ?? 0; - const img = - menu.imageUrl && menu.imageUrl.trim().length > 0 - ? menu.imageUrl - : "/modernKoreaRestaurant.jpg"; - return ( -
-
- {/* 음식사진 */} -
- {menu.name} +
{safeLabel}
+
+ {list.map((menu) => { + const qty = qtyMap.get(Number(menu.id)) ?? 0; + const img = + menu.imageUrl && menu.imageUrl.trim().length > 0 + ? menu.imageUrl + : "/modernKoreaRestaurant.jpg"; + return ( +
+
+ {/* 음식사진 */} +
+ {menu.name} + {menu.isSoldOut && ( +
+ + 품절 + +
)} - /> - {menu.isSoldOut && ( -
- - 품절 - -
- )} -
- {/* 내용 */} -
-
-
-
-

- {menu.name} -

- {menu.description ? ( -

- {menu.description} +

+ {/* 내용 */} +
+
+
+
+

+ {menu.name}

+ {menu.description ? ( +

+ {menu.description} +

+ ) : null} +
+ {qty > 0 ? ( + + {qty}개 + ) : null}
- {qty > 0 ? ( - - {qty}개 - - ) : null} +

+ {formatKrw(menu.price)}원 +

-

- {formatKrw(menu.price)}원 -

-
- {/* 수량 조절 */} -
-
- - {qty} - + {/* 수량 조절 */} +
+
+ + {qty} + +
-
- ); - })} -
-
- ); - }) + ); + })} +
+
+ ); + }, + ) )} diff --git a/src/components/reservation/modals/ReservationModal.tsx b/src/components/reservation/modals/ReservationModal.tsx index f088956..a877b4c 100644 --- a/src/components/reservation/modals/ReservationModal.tsx +++ b/src/components/reservation/modals/ReservationModal.tsx @@ -79,22 +79,25 @@ export default function ReservationModal({ } if (didInitRef.current) return; didInitRef.current = true; - if (initialDraft) { - setPeople(initialDraft.people); - setDate(initialDraft.date); - setTime(initialDraft.time); - setSeatType(initialDraft.seatType); - setTablePref(initialDraft.tablePref); - setSelectedTableId(initialDraft.tableId); - } else { - setPeople(2); - setDate(undefined); - setTime(""); - setSeatType(null); - setTablePref("split_ok"); - setSelectedTableId(null); - } - }, [open]); + const raf = requestAnimationFrame(() => { + if (initialDraft) { + setPeople(initialDraft.people); + setDate(initialDraft.date); + setTime(initialDraft.time ?? ""); + setSeatType(initialDraft.seatType); + setTablePref(initialDraft.tablePref); + setSelectedTableId(initialDraft.tableId); + } else { + setPeople(2); + setDate(undefined); + setTime(""); + setSeatType(null); + setTablePref("split_ok"); + setSelectedTableId(null); + } + }); + return () => cancelAnimationFrame(raf); + }, [open, initialDraft]); const todayKst = startOfTodayInKst(); @@ -162,9 +165,18 @@ export default function ReservationModal({ (availableTablesQuery.data?.tables?.length ?? 0) === 0; const handleRequestClose = useConfirmClose(onClose); + const prevDepsRef = useRef({ people, date, time }); useEffect(() => { - setSeatType(null); - setSelectedTableId(null); + const prev = prevDepsRef.current; + const changed = + prev.people !== people || prev.date !== date || prev.time !== time; + prevDepsRef.current = { people, date, time }; + if (!changed) return; + const raf = requestAnimationFrame(() => { + setSeatType(null); + setSelectedTableId(null); + }); + return () => cancelAnimationFrame(raf); }, [people, date, time]); if (!rendered) return null; @@ -252,7 +264,7 @@ export default function ReservationModal({ - + void; -}; - -export default function RestaurantCard({ restaurant, onClick }: Props) { - return ( - - ); -} +import { storeCategoryLabel, type RestaurantSummary } from "@/types/store"; + +type Props = { + restaurant: RestaurantSummary; + onClick: () => void; +}; + +export default function RestaurantCard({ restaurant, onClick }: Props) { + return ( + + ); +} diff --git a/src/components/restaurant/RestaurantList.tsx b/src/components/restaurant/RestaurantList.tsx index bdf2d73..1c34ac8 100644 --- a/src/components/restaurant/RestaurantList.tsx +++ b/src/components/restaurant/RestaurantList.tsx @@ -1,22 +1,22 @@ -import type { RestaurantSummary } from "@/types/store"; -import RestaurantCard from "./RestaurantCard"; - -type Props = { - restaurants: RestaurantSummary[]; - onSelect: (restaurant: RestaurantSummary) => void; -}; - -export default function RestaurantList({ restaurants, onSelect }: Props) { - return ( -
- {restaurants.map((r, idx) => ( -
- onSelect(r)} /> - {idx !== restaurants.length - 1 ? ( -
- ) : null} -
- ))} -
- ); -} +import type { RestaurantSummary } from "@/types/store"; +import RestaurantCard from "./RestaurantCard"; + +type Props = { + restaurants: RestaurantSummary[]; + onSelect: (restaurant: RestaurantSummary) => void; +}; + +export default function RestaurantList({ restaurants, onSelect }: Props) { + return ( +
+ {restaurants.map((r, idx) => ( +
+ onSelect(r)} /> + {idx !== restaurants.length - 1 ? ( +
+ ) : null} +
+ ))} +
+ ); +} diff --git a/src/components/store-registration/CompleteModal.tsx b/src/components/store-registration/CompleteModal.tsx index 4c3ca03..4d44789 100644 --- a/src/components/store-registration/CompleteModal.tsx +++ b/src/components/store-registration/CompleteModal.tsx @@ -8,7 +8,7 @@ import { import { useEffect } from "react"; import type { StoreInfoFormValues } from "./StoreInfo.schema"; import { Check } from "lucide-react"; -import { categoryLabel } from "@/types/store"; +import { storeCategoryLabel } from "@/types/store"; interface CompleteModalProps { isOpen: boolean; @@ -52,7 +52,7 @@ export default function CompleteModal({
음식 종류 - {data.category ? categoryLabel[data.category] : "-"} + {data.category ? (storeCategoryLabel[data.category] ?? "-") : "-"}
diff --git a/src/components/store-registration/Menu.schema.ts b/src/components/store-registration/Menu.schema.ts index cf2d92d..3899f15 100644 --- a/src/components/store-registration/Menu.schema.ts +++ b/src/components/store-registration/Menu.schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -export const MenuCategoryEnum = z.enum(["MAIN", "SIDE", "BEVERAGE", "ALCOHOL"]); +const MenuCategoryEnum = z.enum(["MAIN", "SIDE", "BEVERAGE", "ALCOHOL"]); const MAX_FILE_SIZE = 1 * 1024 * 1024; diff --git a/src/components/store-registration/MenuItemInput.tsx b/src/components/store-registration/MenuItemInput.tsx index 3525e32..93244b5 100644 --- a/src/components/store-registration/MenuItemInput.tsx +++ b/src/components/store-registration/MenuItemInput.tsx @@ -17,6 +17,7 @@ import { } from "react"; import { Trash2, Upload, X } from "lucide-react"; import { Label } from "@/components/ui/label"; +import { MenuCategoryLabel } from "@/types/menus"; interface MenuItemInputProps { index: number; @@ -28,13 +29,6 @@ interface MenuItemInputProps { trigger: UseFormTrigger; } -const CATEGORY_LABELS: Record = { - MAIN: "메인 메뉴", - SIDE: "사이드 메뉴", - BEVERAGE: "음료", - ALCOHOL: "주류", -}; - export default function MenuItemInput({ index, onDelete, @@ -82,6 +76,10 @@ export default function MenuItemInput({ setValue(`menus.${index}.imageKey`, undefined, { shouldValidate: true }); }; + const imageError = errors.menus?.[index]?.imageKey; + const imageErrorMessage = + typeof imageError?.message === "string" ? imageError.message : undefined; + return (
@@ -149,10 +147,8 @@ export default function MenuItemInput({

• 최대 용량: 1MB

• 형식: JPG(JPEG), PNG

- {errors.menus?.[index]?.imageKey && ( -

- • {(errors.menus[index]?.imageKey as any).message} -

+ {imageErrorMessage && ( +

• {imageErrorMessage}

)}
@@ -225,7 +221,7 @@ export default function MenuItemInput({ {...field} className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer" > - {Object.entries(CATEGORY_LABELS).map(([value, label]) => ( + {Object.entries(MenuCategoryLabel).map(([value, label]) => ( diff --git a/src/components/store-registration/RegistrationStepper.tsx b/src/components/store-registration/RegistrationStepper.tsx index aaa677f..4c2f9ae 100644 --- a/src/components/store-registration/RegistrationStepper.tsx +++ b/src/components/store-registration/RegistrationStepper.tsx @@ -17,7 +17,10 @@ export default function RegistrationStepper({
{steps.map((step, index) => ( -
+
{step.number} )}
- {step.label} + + {step.label} +
{index !== steps.length - 1 && (
step.number ? "bg-blue-500" : "bg-gray-200" }`} style={{ minWidth: "80px" }} diff --git a/src/components/store-registration/StepBusinessAuth.tsx b/src/components/store-registration/StepBusinessAuth.tsx index 8970f50..9b6090f 100644 --- a/src/components/store-registration/StepBusinessAuth.tsx +++ b/src/components/store-registration/StepBusinessAuth.tsx @@ -12,6 +12,7 @@ import { getErrorMessage } from "@/utils/error"; import { useNavigate } from "react-router-dom"; import { logout } from "@/api/auth"; import ConfirmModal from "./ConfirmModal"; +import axios from "axios"; interface StepBusinessAuthProps { defaultValues: { @@ -53,7 +54,7 @@ export default function StepBusinessAuth({ const { register, handleSubmit, - watch, + getValues, formState: { isValid, errors, touchedFields }, } = useForm({ resolver: zodResolver(BusinessAuthSchema), @@ -65,10 +66,6 @@ export default function StepBusinessAuth({ }, }); - const name = watch("name"); - const businessNumber = watch("businessNumber"); - const startDate = watch("startDate"); - const onSubmit = async (data: BusinessAuthFormValues) => { verifyOwner(data, { onSuccess: async () => { @@ -77,8 +74,9 @@ export default function StepBusinessAuth({ nav("/", { replace: true }); }, onError: (error) => { - const err = error as any; - const serverCode = err.response?.data?.code || err.code; + const serverCode = axios.isAxiosError(error) + ? error.response?.data?.code + : undefined; if (serverCode === "OWNER409") { setPendingData(data); return; @@ -130,7 +128,9 @@ export default function StepBusinessAuth({ variant="primary" />
{ + void handleSubmit(onSubmit)(e); + }} className="max-w-md mx-auto space-y-6 sm:space-y-8" >
@@ -149,6 +149,7 @@ export default function StepBusinessAuth({ {...register("name", { onChange: (e) => { if (isVerified) { + const { businessNumber, startDate } = getValues(); setIsVerified(false); onComplete({ name: e.target.value, @@ -192,11 +193,12 @@ export default function StepBusinessAuth({ {...register("businessNumber", { onChange: (e) => { if (isVerified) { + const { name, businessNumber } = getValues(); setIsVerified(false); onComplete({ name, - businessNumber: e.target.value, - startDate, + businessNumber, + startDate: e.target.value, isVerified: false, }); } @@ -236,11 +238,12 @@ export default function StepBusinessAuth({ {...register("startDate", { onChange: (e) => { if (isVerified) { + const { name, startDate } = getValues(); setIsVerified(false); onComplete({ name, - businessNumber, - startDate: e.target.value, + businessNumber: e.target.value, + startDate, isVerified: false, }); } diff --git a/src/components/store-registration/StepMenuRegistration.tsx b/src/components/store-registration/StepMenuRegistration.tsx index 6560d8f..1f271d2 100644 --- a/src/components/store-registration/StepMenuRegistration.tsx +++ b/src/components/store-registration/StepMenuRegistration.tsx @@ -1,6 +1,6 @@ import { Plus } from "lucide-react"; import { useEffect } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; +import { useFieldArray, useForm, useWatch } from "react-hook-form"; import { MenuSchema, type MenuFormValues } from "./Menu.schema"; import { zodResolver } from "@hookform/resolvers/zod"; import MenuItemInput from "./MenuItemInput"; @@ -18,9 +18,7 @@ export default function StepMenuRegistration({ register, control, setValue, - watch, trigger, - getValues, formState: { errors, isValid }, } = useForm({ resolver: zodResolver(MenuSchema), @@ -35,13 +33,11 @@ export default function StepMenuRegistration({ name: "menus", }); + const menus = useWatch({ control, name: "menus" }); + useEffect(() => { - const subscription = watch((value) => { - onChange(isValid, value as MenuFormValues); - }); - onChange(isValid, getValues()); - return () => subscription.unsubscribe(); - }, [watch, isValid, onChange, getValues]); + onChange(isValid, { menus: menus ?? [] }); + }, [menus, isValid, onChange]); return (
diff --git a/src/components/store-registration/StepStoreInfo.tsx b/src/components/store-registration/StepStoreInfo.tsx index 13ff869..c32fcbf 100644 --- a/src/components/store-registration/StepStoreInfo.tsx +++ b/src/components/store-registration/StepStoreInfo.tsx @@ -1,18 +1,17 @@ -import { Controller, useForm } from "react-hook-form"; +import { Controller, useForm, useWatch } from "react-hook-form"; import { Label } from "../ui/label"; import { StoreInfoSchema, type StoreInfoFormValues } from "./StoreInfo.schema"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { phoneNumber } from "@/utils/phoneNumber"; import DaumPostcodeEmbed from "react-daum-postcode"; import { loadKakaoMapSdk } from "@/lib/kakao"; import { Upload, X } from "lucide-react"; - -declare global { - interface Window { - kakao: any; - } -} +import type { AddressSearchResult } from "@/types/store"; +import type { + KakaoAddressSearchResult, + KakaoAddressSearchStatus, +} from "@/types/kakao"; interface StepStoreInfoProps { defaultValues: Partial; @@ -33,19 +32,15 @@ export default function StepStoreInfo({ onChange, }: StepStoreInfoProps) { const [isOpenPostcode, setIsOpenPostcode] = useState(false); - - const [previewUrl, setPreviewUrl] = useState(null); const fileInputRef = useRef(null); const { register, control, - watch, setValue, trigger, - getValues, formState: { errors, isValid, touchedFields }, - } = useForm({ + } = useForm({ resolver: zodResolver(StoreInfoSchema), mode: "onChange", defaultValues: { @@ -68,23 +63,28 @@ export default function StepStoreInfo({ }, }); - const watchedMainImage = watch("mainImage"); + const watchedMainImage = useWatch({ + control, + name: "mainImage", + }); - useEffect(() => { - if (watchedMainImage && watchedMainImage instanceof File) { - const url = URL.createObjectURL(watchedMainImage); - setPreviewUrl(url); + const formValues = useWatch({ control }); - return () => { - URL.revokeObjectURL(url); - }; - } else if (typeof watchedMainImage === "string") { - setPreviewUrl(watchedMainImage); - } else { - setPreviewUrl(null); + const previewUrl = useMemo(() => { + if (watchedMainImage instanceof File) { + return URL.createObjectURL(watchedMainImage); } + return null; }, [watchedMainImage]); + useEffect(() => { + return () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + }; + }, [previewUrl]); + const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -99,7 +99,7 @@ export default function StepStoreInfo({ if (fileInputRef.current) { fileInputRef.current.value = ""; } - setValue("mainImage", null, { shouldValidate: true }); + setValue("mainImage", undefined, { shouldValidate: true }); }; useEffect(() => { @@ -123,20 +123,11 @@ export default function StepStoreInfo({ loadKakaoMapSdk().catch((err) => console.error("카카오맵 로드 실패:", err)); }, []); - const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; - useEffect(() => { - const subscription = watch((value) => { - onChangeRef.current(isValid, value as StoreInfoFormValues); - }); - - onChangeRef.current(isValid, getValues() as StoreInfoFormValues); + onChange(isValid, formValues as StoreInfoFormValues); + }, [formValues, isValid, onChange]); - return () => subscription.unsubscribe(); - }, [watch, isValid, getValues]); - - const handleAddressComplete = (data: any) => { + const handleAddressComplete = (data: AddressSearchResult) => { let fullAddress = data.address; let extraAddress = ""; @@ -155,23 +146,30 @@ export default function StepStoreInfo({ setValue("bname", data.bname); if (window.kakao?.maps?.services) { + const maps = window.kakao.maps; const geocoder = new window.kakao.maps.services.Geocoder(); - geocoder.addressSearch(fullAddress, (result: any, status: any) => { - if (status === window.kakao.maps.services.Status.OK) { - const lat = parseFloat(result[0].y); - const lng = parseFloat(result[0].x); - - setValue("latitude", lat, { shouldValidate: true }); - setValue("longitude", lng, { shouldValidate: true }); - - trigger("address"); - } else { - setValue("latitude", 0, { shouldValidate: true }); - setValue("longitude", 0, { shouldValidate: true }); - trigger("address"); - } - }); + geocoder.addressSearch( + fullAddress, + ( + result: KakaoAddressSearchResult[], + status: KakaoAddressSearchStatus, + ) => { + if (status === maps.services.Status.OK) { + const lat = parseFloat(result[0].y); + const lng = parseFloat(result[0].x); + + setValue("latitude", lat, { shouldValidate: true }); + setValue("longitude", lng, { shouldValidate: true }); + + trigger("address"); + } else { + setValue("latitude", 0, { shouldValidate: true }); + setValue("longitude", 0, { shouldValidate: true }); + trigger("address"); + } + }, + ); } else { alert("지도 서비스 로딩에 실패했습니다. 잠시 후 다시 시도해주세요."); setValue("latitude", 0, { shouldValidate: true }); @@ -182,6 +180,12 @@ export default function StepStoreInfo({ setIsOpenPostcode(false); }; + const mainImageError = errors.mainImage; + const mainImageErrorMessage = + typeof mainImageError?.message === "string" + ? mainImageError.message + : undefined; + return (
@@ -465,9 +469,9 @@ export default function StepStoreInfo({

• 최대 용량: 1MB

• 형식: JPG(JPEG), PNG

- {errors.mainImage && ( + {mainImageErrorMessage && (

- • {(errors.mainImage as any).message} + • {mainImageErrorMessage}

)}
@@ -499,7 +503,7 @@ export default function StepStoreInfo({ aria-modal="true" >
e.stopPropagation()} >
- ) + ); }, ...components, }} {...props} /> - ) + ); } function CalendarDayButton({ @@ -183,12 +183,12 @@ function CalendarDayButton({ modifiers, ...props }: React.ComponentProps) { - const defaultClassNames = getDefaultClassNames() + const defaultClassNames = getDefaultClassNames(); - const ref = React.useRef(null) + const ref = React.useRef(null); React.useEffect(() => { - if (modifiers.focused) ref.current?.focus() - }, [modifiers.focused]) + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); return ( - )} -
- -
- -
-
- ); -} +import { logout } from "@/api/auth"; +import { getMemberInfo } from "@/api/endpoints/member"; +import { Button } from "@/components/ui/button"; +import { + useAuthStore, + useAuthToken, + useIsAuthenticated, + useUserId, +} from "@/stores/useAuthStore"; +import { isAxiosError } from "axios"; +import { useEffect } from "react"; +import { Link, Outlet, useNavigate } from "react-router-dom"; + +export default function PublicLayout() { + const nav = useNavigate(); + const isAuthenticated = useIsAuthenticated(); + + const accessToken = useAuthToken(); + const userId = useUserId(); + const { setUserId, logout: clearAuth } = useAuthStore((s) => s.actions); + useEffect(() => { + if (!accessToken) return; + if (userId != null) return; + let cancelled = false; + (async () => { + try { + const member = await getMemberInfo(); + if (cancelled) return; + const rawId = member?.id; + + let parsedId: number | null = null; + if (typeof rawId === "number") parsedId = rawId; + if (typeof rawId === "string" && /^\d+$/.test(rawId)) + parsedId = Number(rawId); + if (parsedId != null && Number.isFinite(parsedId)) { + setUserId(parsedId); + } else { + console.warn("[member/info] invalid id:", rawId, member); + nav("/", { replace: true }); + } + } catch (e: unknown) { + if (cancelled) return; + if (isAxiosError(e)) { + const status = e.response?.status; + if (status === 401 || status === 403) { + clearAuth(); + nav("/", { replace: true }); + } else { + console.error("[member/info] failed", status, e); + } + } else { + console.error("[member/info] unexpected error", e); + } + } + })(); + return () => { + cancelled = true; + }; + }, [accessToken, userId, setUserId, clearAuth, nav]); + + const handleLogout = async () => { + if (!confirm("로그아웃 하시겠습니까?")) return; + + await logout(); + clearAuth(); + alert("로그아웃 되었습니다."); + nav("/", { replace: true }); + }; + + return ( +
+
+
+ + 잇츠파인 로고 +
+

잇츠파인

+

원하는 자리를 선택하는 스마트 식당 예약

+
+ + {isAuthenticated && ( + + )} +
+
+
+ +
+
+ ); +} diff --git a/src/lib/kakao.ts b/src/lib/kakao.ts index 3f72f50..8bcda8b 100644 --- a/src/lib/kakao.ts +++ b/src/lib/kakao.ts @@ -1,9 +1,3 @@ -declare global { - interface Window { - kakao: any; - } -} - let loadingPromise: Promise | null = null; export function loadKakaoMapSdk(): Promise { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f021764..a5ef193 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/main.tsx b/src/main.tsx index e05250d..6bfc5a8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,14 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./index.css"; -import App from "./App.tsx"; -import { QueryClientProvider } from "@tanstack/react-query"; -import { queryClient } from "@/query/queryClient.ts"; - -createRoot(document.getElementById("root")!).render( - - - - - , -); +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { queryClient } from "@/query/queryClient.ts"; + +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/src/pages/CustomerSupportPage.tsx b/src/pages/CustomerSupportPage.tsx index c5084b9..ae1e9ee 100644 --- a/src/pages/CustomerSupportPage.tsx +++ b/src/pages/CustomerSupportPage.tsx @@ -6,7 +6,7 @@ import { Link } from "react-router-dom"; export default function CustomerSupportPage() { return ( -
+
(null); + const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -54,13 +55,13 @@ export default function ReservationCompletePage() { }; const restaurant = useMemo(() => { - return { id: 0, name: item?.storeName ?? "예약완료" } as any; + return { id: 0, name: item?.storeName ?? "예약완료" }; }, [item]); const draft = useMemo(() => { - if (!item) return { people: 0, date: new Date(), time: "" } as any; + if (!item) return { people: 0, date: new Date(), time: "" }; const date = new Date(`${item.bookingDate}T00:00:00`); const time = toHHmm(item.bookingTime) ?? ""; - return { people: item.partySize, date, time } as any; + return { people: item.partySize, date, time }; }, [item]); return ( diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index dff0ab1..c0fd923 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -1,391 +1,410 @@ -import { useEffect, useMemo, useState } from "react"; -import { Search } from "lucide-react"; -import RestaurantList from "@/components/restaurant/RestaurantList"; -import type { ReservationDraft } from "@/types/restaurant"; -import RestaurantDetailModal from "@/components/restaurant/RestaurantDetailModal"; -import ReservationModal from "@/components/reservation/modals/ReservationModal"; -import ReservationConfirmMoodal from "@/components/reservation/modals/ReservationConfirmModal"; -import PaymentModal from "@/components/reservation/modals/PaymentModal"; -import ReservationMenuModal from "@/components/reservation/modals/ReservationMenuModal"; -import type { RestaurantSummary } from "@/types/store"; -import KakaoMap from "@/components/map/KakaoMap"; -import { useRestaurantDetail } from "@/hooks/store/useRestaurantDetail"; -import { useSearchStores } from "@/hooks/store/useSearchStores"; -import type { CreateBookingResult } from "@/api/endpoints/reservations"; -import { toHHmm } from "@/utils/time"; -import RestaurantListSkeleton from "@/components/restaurant/RestaurantListSkeleton"; - -export default function SearchPage() { - const [query, setQuery] = useState(""); - const [selectedStoreId, setSelectedStoreId] = useState(null); - - const [detailOpen, setDetailOpen] = useState(false); - const [reserveOpen, setReserveOpen] = useState(false); - const [reserveMenuOpen, setReserveMenuOpen] = useState(false); - const [confirmOpen, setConfirmOpen] = useState(false); - const [draft, setDraft] = useState(null); - const [paymentOpen, setPaymentOpen] = useState(false); - - const [coords, setCoords] = useState<{ lat: number; lng: number } | null>( - null, - ); - const [hasSearched, setHasSearched] = useState(false); - const FALLBACK_COORDS = { lat: 37.5665, lng: 126.978 }; - const [mapCenter, setMapCenter] = useState(FALLBACK_COORDS); - - const detailQuery = useRestaurantDetail(selectedStoreId); - - const [isSearchingUI, setIsSearchingUI] = useState(false); - - const [searchParams, setSearchParams] = useState<{ - keyword: string; - lat: number; - lng: number; - } | null>(null); - const searchQuery = useSearchStores( - searchParams - ? { ...searchParams, radius: 50, sort: "DISTANCE", page: 1, limit: 20 } - : null, - ); - - const results = searchQuery.data ?? []; - - const searchError = searchQuery.isError - ? searchQuery.error instanceof Error - ? searchQuery.error.message - : "검색에 실패했어요" - : null; - - const [booking, setBooking] = useState(null); - - const normalizeDraft = (d: ReservationDraft): ReservationDraft => { - const normalizedTime = toHHmm(d.time); - const safeTime = - !normalizedTime || normalizedTime.includes("undefined") - ? undefined - : normalizedTime; - - return { - ...d, - time: safeTime as any, - }; - }; - type LatLng = { lat: number; lng: number }; - const [geoMap, setGeoMap] = useState>(new Map()); - - function isValidLatLng(loc: any): loc is LatLng { - return ( - loc && - typeof loc.lat === "number" && - typeof loc.lng === "number" && - Number.isFinite(loc.lat) && - Number.isFinite(loc.lng) - ); - } - async function geocodeAddress(address: string): Promise { - const kakao = window.kakao; - if (!kakao?.maps?.services) { - return null; - } - - const geocoder = new kakao.maps.services.Geocoder(); - - return new Promise((resolve) => { - geocoder.addressSearch(address, (res: any[], status: string) => { - if (status !== kakao.maps.services.Status.OK || !res?.[0]) { - resolve(null); - return; - } - const lng = parseFloat(res[0].x); - const lat = parseFloat(res[0].y); - if (!Number.isFinite(lat) || !Number.isFinite(lng)) { - resolve(null); - return; - } - - resolve({ lat, lng }); - }); - }); - } - - const openDetail = async (restaurant: RestaurantSummary) => { - const storeId = restaurant.id; - setSelectedStoreId(storeId); - setDetailOpen(true); - setDraft(null); - setConfirmOpen(false); - setReserveOpen(false); - setReserveMenuOpen(false); - setPaymentOpen(false); - setBooking(null); - }; - - const handleSelectStore = (store: RestaurantSummary) => { - openDetail(store); - }; - - const goReserve = () => { - setDraft(null); - setDetailOpen(false); - setReserveOpen(true); - }; - - const goReserveMenu = (d: ReservationDraft) => { - setDraft(normalizeDraft(d)); - setReserveOpen(false); - setReserveMenuOpen(true); - }; - - const backToReserve = () => { - setReserveMenuOpen(false); - setReserveOpen(true); - }; - const goConfirm = (d: ReservationDraft) => { - setDraft(normalizeDraft(d)); - setReserveMenuOpen(false); - setConfirmOpen(true); - }; - const backToReserveMenu = () => { - setConfirmOpen(false); - setReserveMenuOpen(true); - }; - - const goPayment = (bookingResult: CreateBookingResult) => { - setBooking(bookingResult); - setConfirmOpen(false); - setPaymentOpen(true); - }; - - const backToConfirm = () => { - setPaymentOpen(false); - setConfirmOpen(true); - }; - - const closeModalsOnly = () => { - setDetailOpen(false); - setReserveOpen(false); - setReserveMenuOpen(false); - setConfirmOpen(false); - setPaymentOpen(false); - }; - - function getCoords(): Promise<{ lat: number; lng: number }> { - return new Promise((resolve) => { - if (!navigator.geolocation) { - resolve(FALLBACK_COORDS); - return; - } - navigator.geolocation.getCurrentPosition( - (pos) => - resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }), - () => resolve(FALLBACK_COORDS), - { enableHighAccuracy: false, timeout: 5000 }, - ); - }); - } - - useEffect(() => { - let cancelled = false; - const run = async () => { - if (!results || results.length === 0) return; - - const kakao = window.kakao; - if (!kakao?.maps?.services) { - setTimeout(() => { - if (!cancelled) run(); - }, 200); - return; - } - const targets = results.filter((r) => !isValidLatLng(r.location)); - if (targets.length === 0) return; - const next = new Map(geoMap); - - for (const r of targets) { - if (next.has(r.id)) continue; - const loc = await geocodeAddress(r.address); - if (loc) next.set(r.id, loc); - } - setGeoMap(next); - }; - run(); - return () => { - cancelled = true; - }; - }, [results]); - - const geocodedResults = useMemo(() => { - return results - .map((r) => { - const loc = isValidLatLng(r.location) ? r.location : geoMap.get(r.id); - return loc ? { ...r, location: loc } : null; - }) - .filter(Boolean) as RestaurantSummary[]; - }, [results, geoMap]); - - const runSearch = async () => { - setHasSearched(true); - setSelectedStoreId(null); - const keyword = query.trim(); - - if (!keyword) { - setSearchParams(null); - setIsSearchingUI(false); - return; - } - setIsSearchingUI(true); - - const c = coords ?? (await getCoords()); - setCoords(c); - setMapCenter({ lat: c.lat, lng: c.lng }); - setSearchParams({ keyword, lat: c.lat, lng: c.lng }); - }; - - useEffect(() => { - if (!hasSearched) return; - if (!isSearchingUI) return; - - if (searchQuery.isSuccess || searchQuery.isError) { - setIsSearchingUI(false); - } - }, [hasSearched, isSearchingUI, searchQuery.isSuccess, searchQuery.isError]); - - return ( - <> -
-
- setQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") runSearch(); - }} - /> - -
-
- - - -
- {hasSearched ? ( - <> - {searchError ? ( -

{searchError}

- ) : isSearchingUI || searchQuery.isFetching ? ( - <> -
- - 검색 중... -
- - - ) : results.length === 0 ? ( -
- 검색 결과가 없어요. -
- ) : ( - - )} - - ) : null} -
- - {detailOpen && ( - { - setDetailOpen(o); - if (!o) { - closeModalsOnly(); - } - }} - status={ - !selectedStoreId - ? "idle" - : detailQuery.isLoading - ? "loading" - : detailQuery.isError - ? "error" - : "success" - } - restaurant={detailQuery.data ?? null} - errorMessage={ - detailQuery.isError - ? detailQuery.error instanceof Error - ? detailQuery.error.message - : "상세 조회 실패" - : undefined - } - onRetry={() => detailQuery.refetch()} - onClickReserve={goReserve} - /> - )} - {reserveOpen && selectedStoreId && detailQuery.data && ( - - )} - {selectedStoreId && draft && detailQuery.data && ( - - )} - {selectedStoreId && draft && detailQuery.data && ( - - )} - - {selectedStoreId && - draft && - paymentOpen && - booking && - detailQuery.data && ( - - )} - - ); -} +import { useEffect, useMemo, useState } from "react"; +import { Search } from "lucide-react"; +import RestaurantList from "@/components/restaurant/RestaurantList"; +import type { ReservationDraft } from "@/types/restaurant"; +import RestaurantDetailModal from "@/components/restaurant/RestaurantDetailModal"; +import ReservationModal from "@/components/reservation/modals/ReservationModal"; +import ReservationConfirmMoodal from "@/components/reservation/modals/ReservationConfirmModal"; +import PaymentModal from "@/components/reservation/modals/PaymentModal"; +import ReservationMenuModal from "@/components/reservation/modals/ReservationMenuModal"; +import type { RestaurantSummary } from "@/types/store"; +import KakaoMap from "@/components/map/KakaoMap"; +import { useRestaurantDetail } from "@/hooks/store/useRestaurantDetail"; +import { useSearchStores } from "@/hooks/store/useSearchStores"; +import type { CreateBookingResult } from "@/api/endpoints/reservations"; +import { toHHmm } from "@/utils/time"; +import RestaurantListSkeleton from "@/components/restaurant/RestaurantListSkeleton"; +import type { + KakaoAddressSearchResult, + KakaoAddressSearchStatus, +} from "@/types/kakao"; +import type { LatLng } from "@/types/map"; + +function isValidLatLng(loc: unknown): loc is LatLng { + return ( + typeof loc === "object" && + loc !== null && + "lat" in loc && + "lng" in loc && + typeof loc.lat === "number" && + typeof loc.lng === "number" && + Number.isFinite(loc.lat) && + Number.isFinite(loc.lng) + ); +} + +async function geocodeAddress(address: string): Promise { + const maps = window.kakao?.maps; + const services = maps?.services; + if (!services) { + return null; + } + + const geocoder = new services.Geocoder(); + + return new Promise((resolve) => { + geocoder.addressSearch( + address, + (res: KakaoAddressSearchResult[], status: KakaoAddressSearchStatus) => { + if (status !== services.Status.OK || !res[0]) { + resolve(null); + return; + } + const lng = parseFloat(res[0].x); + const lat = parseFloat(res[0].y); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) { + resolve(null); + return; + } + + resolve({ lat, lng }); + }, + ); + }); +} + +export default function SearchPage() { + const [query, setQuery] = useState(""); + const [selectedStoreId, setSelectedStoreId] = useState(null); + + const [detailOpen, setDetailOpen] = useState(false); + const [reserveOpen, setReserveOpen] = useState(false); + const [reserveMenuOpen, setReserveMenuOpen] = useState(false); + const [confirmOpen, setConfirmOpen] = useState(false); + const [draft, setDraft] = useState(null); + const [paymentOpen, setPaymentOpen] = useState(false); + + const [coords, setCoords] = useState<{ lat: number; lng: number } | null>( + null, + ); + const [hasSearched, setHasSearched] = useState(false); + const FALLBACK_COORDS = { lat: 37.5665, lng: 126.978 }; + const [mapCenter, setMapCenter] = useState(FALLBACK_COORDS); + + const detailQuery = useRestaurantDetail(selectedStoreId); + + const [isSearchingUI, setIsSearchingUI] = useState(false); + + const [searchParams, setSearchParams] = useState<{ + keyword: string; + lat: number; + lng: number; + } | null>(null); + const searchQuery = useSearchStores( + searchParams + ? { ...searchParams, radius: 50, sort: "DISTANCE", page: 1, limit: 20 } + : null, + ); + + const results = useMemo(() => searchQuery.data ?? [], [searchQuery.data]); + + const searchError = searchQuery.isError + ? searchQuery.error instanceof Error + ? searchQuery.error.message + : "검색에 실패했어요" + : null; + + const [booking, setBooking] = useState(null); + const [geoMap, setGeoMap] = useState>(new Map()); + + const normalizeDraft = (d: ReservationDraft): ReservationDraft => { + const normalizedTime = toHHmm(d.time); + const safeTime = + !normalizedTime || normalizedTime.includes("undefined") + ? undefined + : normalizedTime; + + return { + ...d, + time: safeTime, + }; + }; + + const openDetail = async (restaurant: RestaurantSummary) => { + const storeId = restaurant.id; + setSelectedStoreId(storeId); + setDetailOpen(true); + setDraft(null); + setConfirmOpen(false); + setReserveOpen(false); + setReserveMenuOpen(false); + setPaymentOpen(false); + setBooking(null); + }; + + const handleSelectStore = (store: RestaurantSummary) => { + openDetail(store); + }; + + const goReserve = () => { + setDraft(null); + setDetailOpen(false); + setReserveOpen(true); + }; + + const goReserveMenu = (d: ReservationDraft) => { + setDraft(normalizeDraft(d)); + setReserveOpen(false); + setReserveMenuOpen(true); + }; + + const backToReserve = () => { + setReserveMenuOpen(false); + setReserveOpen(true); + }; + const goConfirm = (d: ReservationDraft) => { + setDraft(normalizeDraft(d)); + setReserveMenuOpen(false); + setConfirmOpen(true); + }; + const backToReserveMenu = () => { + setConfirmOpen(false); + setReserveMenuOpen(true); + }; + + const goPayment = (bookingResult: CreateBookingResult) => { + setBooking(bookingResult); + setConfirmOpen(false); + setPaymentOpen(true); + }; + + const backToConfirm = () => { + setPaymentOpen(false); + setConfirmOpen(true); + }; + + const closeModalsOnly = () => { + setDetailOpen(false); + setReserveOpen(false); + setReserveMenuOpen(false); + setConfirmOpen(false); + setPaymentOpen(false); + }; + + function getCoords(): Promise<{ lat: number; lng: number }> { + return new Promise((resolve) => { + if (!navigator.geolocation) { + resolve(FALLBACK_COORDS); + return; + } + navigator.geolocation.getCurrentPosition( + (pos) => + resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }), + () => resolve(FALLBACK_COORDS), + { enableHighAccuracy: false, timeout: 5000 }, + ); + }); + } + + useEffect(() => { + let cancelled = false; + + const run = async () => { + if (!results || results.length === 0) return; + + const kakao = window.kakao; + if (!kakao?.maps?.services) { + setTimeout(() => { + if (!cancelled) run(); + }, 200); + return; + } + const targets = results.filter((r) => !isValidLatLng(r.location)); + if (targets.length === 0) return; + + const next = new Map(geoMap); + + for (const r of targets) { + if (next.has(r.id)) continue; + const loc = await geocodeAddress(r.address); + if (loc) next.set(r.id, loc); + } + if (!cancelled) { + setGeoMap(next); + } + }; + void run(); + return () => { + cancelled = true; + }; + }, [results, geoMap]); + + const geocodedResults = useMemo(() => { + return results + .map((r) => { + const loc = isValidLatLng(r.location) ? r.location : geoMap.get(r.id); + return loc ? { ...r, location: loc } : null; + }) + .filter(Boolean) as RestaurantSummary[]; + }, [results, geoMap]); + + const runSearch = async () => { + setHasSearched(true); + setSelectedStoreId(null); + const keyword = query.trim(); + + if (!keyword) { + setSearchParams(null); + setIsSearchingUI(false); + return; + } + setIsSearchingUI(true); + + const c = coords ?? (await getCoords()); + setCoords(c); + setMapCenter({ lat: c.lat, lng: c.lng }); + setSearchParams({ keyword, lat: c.lat, lng: c.lng }); + }; + + useEffect(() => { + if (!hasSearched) return; + if (!isSearchingUI) return; + + if (searchQuery.isSuccess || searchQuery.isError) { + const raf = requestAnimationFrame(() => { + setIsSearchingUI((prev) => (prev === false ? prev : false)); + }); + return () => cancelAnimationFrame(raf); + } + }, [hasSearched, isSearchingUI, searchQuery.isSuccess, searchQuery.isError]); + + return ( + <> +
+
+ setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") runSearch(); + }} + /> + +
+
+ + + +
+ {hasSearched ? ( + <> + {searchError ? ( +

{searchError}

+ ) : isSearchingUI || searchQuery.isFetching ? ( + <> +
+ + 검색 중... +
+ + + ) : results.length === 0 ? ( +
+ 검색 결과가 없어요. +
+ ) : ( + + )} + + ) : null} +
+ + {detailOpen && ( + { + setDetailOpen(o); + if (!o) { + closeModalsOnly(); + } + }} + status={ + !selectedStoreId + ? "idle" + : detailQuery.isLoading + ? "loading" + : detailQuery.isError + ? "error" + : "success" + } + restaurant={detailQuery.data ?? null} + errorMessage={ + detailQuery.isError + ? detailQuery.error instanceof Error + ? detailQuery.error.message + : "상세 조회 실패" + : undefined + } + onRetry={() => detailQuery.refetch()} + onClickReserve={goReserve} + /> + )} + {reserveOpen && selectedStoreId && detailQuery.data && ( + + )} + {selectedStoreId && draft && detailQuery.data && ( + + )} + {selectedStoreId && draft && detailQuery.data && ( + + )} + + {selectedStoreId && + draft && + paymentOpen && + booking && + detailQuery.data && ( + + )} + + ); +} diff --git a/src/pages/myPage/MyInfoPage.tsx b/src/pages/myPage/MyInfoPage.tsx index 6fb10bc..9c09546 100644 --- a/src/pages/myPage/MyInfoPage.tsx +++ b/src/pages/myPage/MyInfoPage.tsx @@ -3,11 +3,12 @@ import { patchMemberInfo, putProfileImage, } from "@/api/endpoints/member"; +import ProfileAvatar from "@/components/profile/profileAvatar"; import { Button } from "@/components/ui/button"; import { phoneNumber } from "@/utils/phoneNumber"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Camera, Save } from "lucide-react"; -import { useEffect, useRef, useState, type ChangeEvent } from "react"; +import { useMemo, useRef, useState, type ChangeEvent } from "react"; type Form = { email: string; @@ -25,67 +26,46 @@ export default function MyInfoPage() { const [draftImageFile, setDraftImageFile] = useState(null); const shownFile = isEditing ? draftImageFile : originalImageFile; - const [shownUrl, setShownUrl] = useState(null); - - const [serverProfileUrl, setServerProfileUrl] = useState(null); const [imageUploadError, setImageUploadError] = useState(null); - useEffect(() => { - if (!shownFile) { - setShownUrl(null); - return; - } - const url = URL.createObjectURL(shownFile); - setShownUrl(url); - return () => URL.revokeObjectURL(url); - }, [shownFile]); - const { data, isLoading, isError } = useQuery({ queryKey: ["memberInfo"], queryFn: getMemberInfo, refetchOnWindowFocus: false, }); - const [original, setOriginal] = useState({ + const original = useMemo( + () => ({ + email: data?.email ?? "", + nickname: data?.name ?? "", + phone: phoneNumber(data?.phoneNumber ?? ""), + }), + [data], + ); + + const serverProfileUrl = data?.profileImage ?? null; + const [draft, setDraft] = useState({ email: "", nickname: "", phone: "", }); - const [draft, setDraft] = useState(original); - - const toAbsolute = (url: string | null) => { - if (!url) return null; - if (url.startsWith("http")) return url; - const apiBase = - (import.meta.env.VITE_API_URL as string | undefined)?.replace( - /\/api\/?$/, - "", - ) ?? ""; - return `${apiBase}${url.startsWith("/") ? "" : "/"}${url}`; - }; - - useEffect(() => { - if (!data) return; + const currentForm = isEditing ? draft : original; - const nextOriginal: Form = { - email: data.email ?? "", - nickname: data.name ?? "", - phone: phoneNumber(data.phoneNumber ?? ""), - }; - setOriginal(nextOriginal); - setDraft(nextOriginal); - setServerProfileUrl(toAbsolute(data.profileImage ?? null)); - }, [data]); + const displayProfileSrc = useMemo(() => { + if (shownFile) { + return URL.createObjectURL(shownFile); + } + return serverProfileUrl; + }, [shownFile, serverProfileUrl]); const { mutate: saveMutate, isPending: isSaving } = useMutation({ mutationFn: patchMemberInfo, onSuccess: async () => { - setOriginal(draft); - setOriginalImageFile(draftImageFile); - setIsEditing(false); await qc.invalidateQueries({ queryKey: ["memberInfo"] }); + setOriginalImageFile(null); + setIsEditing(false); }, onError: () => { alert("저장에 실패했습니다. 다시 시도해주세요"); @@ -133,7 +113,6 @@ export default function MyInfoPage() { setImageUploadError(null); setDraftImageFile(file); uploadImage(file); - }; const isValidPhone = (value: string) => { @@ -174,8 +153,6 @@ export default function MyInfoPage() { ); } - const displayProfileSrc = shownUrl ?? serverProfileUrl ?? null; - return (
@@ -220,9 +197,7 @@ export default function MyInfoPage() { className="h-full w-full object-cover" /> ) : ( - - {draft.nickname?.[0] ?? "맛"} - + )}
@@ -252,7 +227,7 @@ export default function MyInfoPage() { handleChange("nickname", e.target.value)} className={`w-full rounded-lg border px-4 py-3 ${ isEditing @@ -285,7 +260,7 @@ export default function MyInfoPage() { handleChange("phone", phoneNumber(e.target.value)) } diff --git a/src/pages/myPage/StoreRegistrationPage.tsx b/src/pages/myPage/StoreRegistrationPage.tsx index e9d4e4e..46f803f 100644 --- a/src/pages/myPage/StoreRegistrationPage.tsx +++ b/src/pages/myPage/StoreRegistrationPage.tsx @@ -10,8 +10,8 @@ import type { StoreInfoFormValues } from "@/components/store-registration/StoreI import { transformToRegister } from "@/components/store-registration/StoreTransform.utils"; import { useMenuCreate, useMenuImage } from "@/hooks/queries/useMenu"; import { useMainImage, useRegisterStore } from "@/hooks/queries/useStore"; -import type { ApiError } from "@/types/api"; import { getErrorMessage } from "@/utils/error"; +import axios from "axios"; import { X } from "lucide-react"; import { useCallback, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -131,13 +131,15 @@ export default function StoreRegistrationPage() { await Promise.all(promises); setIsCompleteModalOpen(true); - } catch (error: any) { - console.error("가게 등록 실패:", error); - const errorResponse = error.response?.data as ApiError; - if (errorResponse?.code === "REGION404") { + } catch (error: unknown) { + const errorCode = axios.isAxiosError<{ code?: string }>(error) + ? error.response?.data?.code + : undefined; + if (errorCode === "REGION404") { alert("현재 서울 지역만 등록 가능합니다."); } else { - alert(getErrorMessage(error)); + const message = getErrorMessage(error); + alert(message || "가게 등록 실패"); } } } diff --git a/src/pages/myPage/reservationPage.tsx b/src/pages/myPage/reservationPage.tsx index 5ed5da7..2acecaa 100644 --- a/src/pages/myPage/reservationPage.tsx +++ b/src/pages/myPage/reservationPage.tsx @@ -1,5 +1,5 @@ import { Calendar, Clock, User, CreditCard, X } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { cn } from "@/lib/utils"; import { getBookings } from "@/api/bookings"; import { cancelBooking } from "@/api/bookings"; @@ -21,30 +21,31 @@ type Reservation = { export default function ReservationPage() { const [activeTab, setActiveTab] = useState("전체"); -const [reservations, setReservations] = useState([]); -const [loading, setLoading] = useState(false); -const [error, setError] = useState(null); + const [reservations, setReservations] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); -const fetchReservations = async () => { + const fetchReservations = useCallback(async () => { try { setLoading(true); + setError(null); let apiStatus: "CONFIRMED" | "COMPLETED" | "CANCELED" | undefined; switch (activeTab) { - case "예정된 예약": - apiStatus = "CONFIRMED"; - break; - case "방문 완료": - apiStatus = "COMPLETED"; - break; - case "취소된 예약": - apiStatus = "CANCELED"; - break; - case "전체": - default: - apiStatus = undefined; - break; - } + case "예정된 예약": + apiStatus = "CONFIRMED"; + break; + case "방문 완료": + apiStatus = "COMPLETED"; + break; + case "취소된 예약": + apiStatus = "CANCELED"; + break; + case "전체": + default: + apiStatus = undefined; + break; + } const data = await getBookings(apiStatus); const mapped: Reservation[] = (data.bookingList ?? []).map((b) => ({ @@ -52,7 +53,7 @@ const fetchReservations = async () => { shopName: b.storeName, address: b.storeAddress, date: b.bookingDate, - time: b.bookingTime ?? "--:--", + time: b.bookingTime ?? "--:--", people: b.partySize?.toString() ?? "0", payment: `${b.amount?.toLocaleString() ?? 0}원`, method: b.paymentMethod ?? "-", @@ -60,17 +61,16 @@ const fetchReservations = async () => { b.status === "CONFIRMED" ? "예약 확정" : b.status === "COMPLETED" - ? "방문 완료" - : "취소됨", + ? "방문 완료" + : "취소됨", step: b.status === "CONFIRMED" ? "예약 진행중" : b.status === "COMPLETED" - ? "방문 완료" - : "취소됨", + ? "방문 완료" + : "취소됨", })); - setReservations(mapped); } catch (error) { console.error("예약 내역 조회 실패", error); @@ -78,13 +78,11 @@ const fetchReservations = async () => { } finally { setLoading(false); } - }; - - useEffect(() => { - fetchReservations(); }, [activeTab]); - + useEffect(() => { + void fetchReservations(); + }, [fetchReservations]); return (
@@ -114,23 +112,37 @@ const fetchReservations = async () => {
{loading ? ( -
로딩 중...
- ) : error ? ( -
{error}
+
+ 로딩 중... +
+ ) : error ? ( +
{error}
) : reservations.length > 0 ? (
{reservations.map((res) => ( - + ))}
) : ( -
해당 내역이 없습니다.
+
+ 해당 내역이 없습니다. +
)}
); } -function ReservationCard({ res, onCancel }: { res: Reservation; onCancel: () => void }) { +function ReservationCard({ + res, + onCancel, +}: { + res: Reservation; + onCancel: () => void; +}) { const [loading, setLoading] = useState(false); const handleCancel = async () => { @@ -160,8 +172,8 @@ function ReservationCard({ res, onCancel }: { res: Reservation; onCancel: () => res.status === "예약 확정" ? "bg-blue-50 text-blue-600" : res.status === "방문 완료" - ? "bg-green-50 text-green-600" - : "bg-gray-100 text-gray-500", + ? "bg-green-50 text-green-600" + : "bg-gray-100 text-gray-500", )} > {res.status} @@ -172,8 +184,16 @@ function ReservationCard({ res, onCancel }: { res: Reservation; onCancel: () =>
- } label="예약 날짜" value={res.date} /> - } label="예약 시간" value={res.time} /> + } + label="예약 날짜" + value={res.date} + /> + } + label="예약 시간" + value={res.time} + /> } label="인원" value={res.people} /> } @@ -184,39 +204,54 @@ function ReservationCard({ res, onCancel }: { res: Reservation; onCancel: () =>
- + {res.step}
{res.status === "예약 확정" && ( - + )}
- - ); } -function InfoItem({ icon, label, value, isMultiLine = false }: { icon: React.ReactNode; label: string; value: string; isMultiLine?: boolean }) { +function InfoItem({ + icon, + label, + value, + isMultiLine = false, +}: { + icon: React.ReactNode; + label: string; + value: string; + isMultiLine?: boolean; +}) { return (
{icon}

{label}

-

{value}

+

+ {value} +

); diff --git a/src/pages/myPage/settingPage.tsx b/src/pages/myPage/settingPage.tsx index aa515b2..191c1e2 100644 --- a/src/pages/myPage/settingPage.tsx +++ b/src/pages/myPage/settingPage.tsx @@ -1,5 +1,5 @@ import { Lock, Bell, Trash2 } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { cn } from "@/lib/utils"; import { ChangePasswordDialog } from "@/components/auth/ChangePasswordDiaLog"; import { WithdrawDialog } from "@/components/auth/WithdrawDialog"; @@ -64,30 +64,35 @@ function Switch({ ); } +const getInitialNotifications = () => { + if (typeof window === "undefined") return defaultNotifications; + + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return defaultNotifications; + const parsed = JSON.parse(raw); + return { ...defaultNotifications, ...parsed }; + } catch (e) { + console.warn("알림 설정 로드 실패, 기본값 사용", e); + return defaultNotifications; + } +}; +type NotificationSettings = typeof defaultNotifications; + export default function SettingsPage() { const [pwOpen, setPwOpen] = useState(false); const [withdrawOpen, setWithdrawOpen] = useState(false); - const [notifications, setNotifications] = useState(defaultNotifications); - const [savedNotifications, setSavedNotifications] = useState(notifications); - - useEffect(() => { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return; - const parsed = JSON.parse(raw); - const merged = { ...defaultNotifications, ...parsed }; - setNotifications(merged); - setSavedNotifications(merged); - } catch (e) { - console.warn("알림 설정 로드 실패, 기본값 사용", e); - } - }, []); + const [notifications, setNotifications] = useState( + getInitialNotifications, + ); + const [savedNotifications, setSavedNotifications] = + useState(notifications); const isDirty = useMemo(() => { return JSON.stringify(notifications) !== JSON.stringify(savedNotifications); }, [notifications, savedNotifications]); - const toggleNotification = (key: keyof typeof notifications) => { + const toggleNotification = (key: keyof NotificationSettings) => { setNotifications((prev) => ({ ...prev, [key]: !prev[key] })); }; diff --git a/src/pages/payment/FailPage.tsx b/src/pages/payment/FailPage.tsx index b2bd252..031a416 100644 --- a/src/pages/payment/FailPage.tsx +++ b/src/pages/payment/FailPage.tsx @@ -51,7 +51,7 @@ export default function FailPage() { }; return ( -
+
diff --git a/src/pages/payment/SuccessPage.tsx b/src/pages/payment/SuccessPage.tsx index 284de77..e820762 100644 --- a/src/pages/payment/SuccessPage.tsx +++ b/src/pages/payment/SuccessPage.tsx @@ -41,6 +41,6 @@ export default function SuccessPage() { nav("/payment/fail", { replace: true }); } })(); - }, [sp.toString(), nav]); + }, [sp, nav]); return
결제 승인 처리중..
; } diff --git a/src/styles/globals.css b/src/styles/globals.css index acbde7f..74a6ec2 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,3 +1,4 @@ +/* global.css 안써서 지웠습니다. 추후에 디자인 완료후 공통스타일 설정해서 global.css 정리후 여기에만 있는 스타일만 사용할 예정입니다. */ @import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap"); @import "tailwindcss"; @import "tw-animate-css"; diff --git a/src/types/api.ts b/src/types/api.ts index fe5e126..ae9c4a9 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,4 +1,3 @@ - export interface ApiResponse { isSuccess: boolean; code: string; @@ -6,7 +5,6 @@ export interface ApiResponse { result: T; } - export interface ApiError { status: number; code?: string; diff --git a/src/types/booking.ts b/src/types/booking.ts new file mode 100644 index 0000000..3ae848b --- /dev/null +++ b/src/types/booking.ts @@ -0,0 +1,5 @@ +import type { getUserBookings } from "@/api/endpoints/bookings"; + +export type UserBookingItem = Awaited< + ReturnType +>["bookingList"][number]; diff --git a/src/types/kakao.d.ts b/src/types/kakao.d.ts new file mode 100644 index 0000000..a3b9c9a --- /dev/null +++ b/src/types/kakao.d.ts @@ -0,0 +1,67 @@ +declare global { + interface Window { + kakao?: { + maps?: { + load: (callback: () => void) => void; + LatLng: new (lat: number, lng: number) => unknown; + services: { + Geocoder: new () => { + addressSearch: ( + address: string, + callback: ( + result: KakaoAddressSearchResult[], + status: KakaoAddressSearchStatus, + ) => void, + ) => void; + }; + Status: { + OK: "OK"; + ZERO_RESULT: "ZERO_RESULT"; + ERROR: "ERROR"; + }; + }; + Map: new ( + container: HTMLDivElement, + options: { center: unknown; level: number }, + ) => { + relayout: () => void; + panTo: (latlng: unknown) => void; + setLevel: (level: number) => void; + setBounds: (bounds: unknown) => void; + }; + Marker: new (option: { + map: unknown; + position: unknown; + clickable: boolean; + zIndex: number; + }) => { + setMap: (map: unknown) => void; + setZIndex: (zIndex: number) => void; + }; + InfoWindow: new (options: { zIndex: number }) => { + setContent: (content: Node | string) => void; + open: (map: unknown, marker: unknown) => void; + }; + LatLngBounds: new () => { + extend: (latlng: unknown) => void; + }; + event: { + addListener: ( + target: unknown, + type: string, + handler: () => void, + ) => void; + }; + }; + }; + } +} + +export {}; + +export type KakaoAddressSearchStatus = "OK" | "ZERO_RESULT" | "ERROR"; + +export type KakaoAddressSearchResult = { + x: string; + y: string; +}; diff --git a/src/types/map.ts b/src/types/map.ts new file mode 100644 index 0000000..9252878 --- /dev/null +++ b/src/types/map.ts @@ -0,0 +1,9 @@ +import type { RestaurantSummary } from "./store"; + +export type LatLng = { lat: number; lng: number }; +export type MarkerWithLocation = RestaurantSummary & { location: LatLng }; + +type KakaoMaps = NonNullable["maps"]>; +export type KaKaoMapInstance = InstanceType; +export type KakaoMarkerInstance = InstanceType; +export type KakaoInfoWindowInstance = InstanceType; diff --git a/src/types/menus.ts b/src/types/menus.ts index f4e58ef..0050881 100644 --- a/src/types/menus.ts +++ b/src/types/menus.ts @@ -1,5 +1,3 @@ -export type MenuCategory = "MAIN" | "SIDE" | "DRINK"; - export type MenuItem = { id: string; restaurantId: string; @@ -20,13 +18,26 @@ export type SelectedMenu = { quantity: number; }; -export type ApiMenuCategory = "MAIN" | "SIDE" | "BEVERAGE" | "ALCOHOL"; +export type MenuCategory = "MAIN" | "SIDE" | "BEVERAGE" | "ALCOHOL"; + +export type UiCategory = MenuCategory | "OTHER"; + +export const MenuCategoryLabel: Record = { + MAIN: "메인 메뉴", + SIDE: "사이드 메뉴", + BEVERAGE: "음료", + ALCOHOL: "주류", +}; +export const UiMenuCategoryLabel: Record = { + ...MenuCategoryLabel, + OTHER: "기타", +}; export type MenuCreateItemDto = { name: string; description?: string; price: number; - category: ApiMenuCategory; + category: MenuCategory; imageKey?: string; }; diff --git a/src/types/restaurant.ts b/src/types/restaurant.ts index f3f72fc..7a003b1 100644 --- a/src/types/restaurant.ts +++ b/src/types/restaurant.ts @@ -1,39 +1,4 @@ import type { SelectedMenu } from "@/types/menus"; -import type { DepositRate } from "@/types/payment"; - -export type Restaurant = { - id: number; - name: string; - category: string; - rating: number; - reviewCount: number; - isApproved: boolean; - operatingHours: { - open: string; - close: string; - breakTime?: { - start: string; - end: string; - }; - }; - totalSeats: number; - address: string; - location?: { - lat: number; - lng: number; - }; - description: string; - seatImages: Array<{ - url: string; - alt: string; - }>; - markerPosition: { - leftPct: number; - topPct: number; - }; - thumbnailUrl?: string; - paymentPolicy?: PaymentPolicy; -}; export const SEATS = [ "일반석", @@ -48,7 +13,7 @@ export type TablePref = "split_ok" | "one_table"; export type ReservationDraft = { people: number; date: Date; - time: string; + time?: string; seatType: SeatType; tablePref: TablePref; tableId: number; @@ -56,11 +21,6 @@ export type ReservationDraft = { selectedMenus: SelectedMenu[]; }; -export type PaymentPolicy = { - depositRate: DepositRate; - notice?: string; -}; - export type SeatTable = { id: number; tableNo: number; @@ -77,5 +37,3 @@ export type SeatLayout = { gridRows: number; tables: SeatTable[]; }; - -export type Step = "form" | "confirm"; diff --git a/src/types/store.ts b/src/types/store.ts index 54987e0..c9d0d82 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -58,7 +58,7 @@ export type RestaurantDetail = { depositRate?: number; }; -export const categoryLabel: Record = { +export const storeCategoryLabel: Record = { KOREAN: "한식", CHINESE: "중식", JAPANESE: "일식", @@ -101,3 +101,19 @@ export type ResponseMainImageDto = { storeId: number; mainImageUrl: string; }; + +export type UpdateStoreResponse = { + storeId: number; + storeName: string; + description: string; + phoneNumber: string; +}; + +export type AddressSearchResult = { + address: string; + addressType: string; + bname: string; + buildingName: string; + sido: string; + sigungu: string; +}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index b430ab5..513b601 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -4,14 +4,10 @@ interface ImportMetaEnv { readonly VITE_API_URL: string; + readonly VITE_KAKAO_JS_KEY: string; // 다른 환경 변수들에 대한 타입 정의... } interface ImportMeta { readonly env: ImportMetaEnv; } - -interface Window { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Kakao: any; -} diff --git a/vite.config.ts b/vite.config.ts index 6fb7fb1..ed7684f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,25 +1,25 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import tailwindcss from "@tailwindcss/vite"; -import { fileURLToPath, URL } from "node:url"; - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react(), tailwindcss()], - resolve: { - alias: { - "@": fileURLToPath(new URL("./src", import.meta.url)), - }, - }, - //백엔드연결성공시삭제예정 - // CORS 오류허용을 위한 임시장치 - server: { - proxy: { - "/api": { - target: "https://eatsfine.co.kr", - changeOrigin: true, - secure: true, - }, - }, - }, -}); +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { fileURLToPath, URL } from "node:url"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, + //백엔드연결성공시삭제예정 + // CORS 오류허용을 위한 임시장치 + server: { + proxy: { + "/api": { + target: "https://eatsfine.co.kr", + changeOrigin: true, + secure: true, + }, + }, + }, +});