diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..417ea140 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +클로드.md diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..8fa48048 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,134 @@ +/** + * ESLint 설정 파일 + * Next.js + TypeScript 프로젝트용 + * + * 주요 설정: + * - TypeScript 파서 사용 + * - Next.js 및 TypeScript 권장 규칙 적용 + * - Prettier와의 충돌 방지 + */ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + project: "./tsconfig.json", + }, + env: { + browser: true, + es2021: true, + node: true, + }, + plugins: ["@typescript-eslint"], + extends: [ + // Next.js 기본 설정 + "next", + // Next.js TypeScript 설정 (plugin:@typescript-eslint/recommended 기반) + "next/typescript", + // TypeScript ESLint 권장 규칙 + "plugin:@typescript-eslint/recommended", + // Prettier와 충돌하는 규칙 비활성화 (항상 마지막에 위치해야 함) + "prettier", + ], + overrides: [ + { + // 설정 파일들은 TypeScript 프로젝트에 포함되지 않으므로 project 옵션 비활성화 + env: { + node: true, + }, + files: [".eslintrc.{js,cjs}", "*.config.{js,mjs,ts}"], + parserOptions: { + sourceType: "script", + project: null, + }, + }, + ], + rules: { + // ========================================== + // React 관련 규칙 + // ========================================== + + // JSX 사용 시 React import 불필요 (React 17+) + "react/react-in-jsx-scope": "off", + + // JSX 허용 파일 확장자 + "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx", ".tsx"] }], + + // defaultProps 필수 여부 비활성화 + "react/require-default-props": "off", + + // 함수 컴포넌트는 화살표 함수로 정의 + "react/function-component-definition": [1, { namedComponents: "arrow-function" }], + + // ========================================== + // Import 관련 규칙 + // ========================================== + + // import 순서는 Prettier 플러그인에서 처리 + "import/order": "off", + + // import 시 파일 확장자 생략 (warning으로 설정) + "import/extensions": "off", + + // 단일 export 시 default export 권장 (warning) + "import/prefer-default-export": "off", + + // ========================================== + // 일반 JavaScript 규칙 + // ========================================== + + // console.log 허용 (개발 편의) + "no-console": "off", + + // alert 허용 + "no-alert": "off", + + // 정의 전 사용 허용 (TypeScript에서 처리) + "no-use-before-define": "off", + + // 미사용 변수 - 기본 규칙 비활성화 (TypeScript 규칙과 충돌 방지) + "no-unused-vars": "off", + + // ========================================== + // TypeScript 관련 규칙 + // ========================================== + + // 미사용 변수 경고 (TypeScript용 - 기본 규칙 대신 사용) + "@typescript-eslint/no-unused-vars": "warn", + + // any 타입 사용 경고 (error -> warn) + "@typescript-eslint/no-explicit-any": "warn", + + // any 타입 관련 규칙 (경고로 설정) + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + + // ========================================== + // 접근성 (a11y) 관련 규칙 + // ========================================== + + // label과 control 연결 규칙 비활성화 + "jsx-a11y/label-has-associated-control": "off", + + // 클릭 이벤트에 키보드 이벤트 필요 (경고) + "jsx-a11y/click-events-have-key-events": "warn", + + // 정적 요소에 이벤트 핸들러 (경고) + "jsx-a11y/no-static-element-interactions": "warn", + }, + settings: { + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"], + }, + "import/resolver": { + typescript: { + alwaysTryTypes: true, + project: "./tsconfig.json", + }, + }, + }, +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 5ca1b159..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module", - "project": "./tsconfig.json" - }, - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "plugins": ["@typescript-eslint"], - "extends": [ - // "airbnb", // import, react, react-hooks, jsx-a11y에 관한 규칙을 포함 - // airbnb를 기반으로 next를 확장하기에 "next"나 "next/core-web-vitals"대신 "plugin:@next/next/recommended"를 사용 - // "plugin:@next/next/recommended", - "next", - "next/typescript", // 다음에 기반함: plugin:@typescript-eslint/recommended - "plugin:@typescript-eslint/recommended", - // "plugin:@typescript-eslint/recommended-requiring-type-checking", - "prettier" // 불필요하거나 Prettier와 충돌할 수 있는 모든 규칙을 끕니다. 마지막에 추가해 다른 설정을 덮어씁니다. - ], - "overrides": [ - { - "env": { - "node": true - }, - "files": [".eslintrc.{js,cjs}"], - "parserOptions": { - "sourceType": "script" - } - } - ], - "rules": { - // - "react/react-in-jsx-scope": "off", - // ㄴ JSX를 사용할 때 React를 import할 필요가 없도록 합니다. - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }], - "react/require-default-props": "off", - "react/function-component-definition": [1, { "namedComponents": "arrow-function" }], - "import/order": "off", - "import/extensions": [ - // import 시 확장자를 사용하지 않도록 합니다. - "error", - "ignorePackages", - { - "js": "never", - "jsx": "never", - "ts": "never", - "tsx": "never" - } - ], - "import/prefer-default-export": 1, // single export를 사용할 때 default export를 미사용하면 warning - "no-console": "off", - "no-alert": "off", - "no-use-before-define": "off", - "no-unused-vars": "warn", - "@typescript-eslint/no-unused-vars": "warn", - // - "@typescript-eslint/no-unsafe-assignment": "warn", - "@typescript-eslint/no-unsafe-member-access": "warn", - "@typescript-eslint/no-unsafe-return": "warn", - "@typescript-eslint/no-unsafe-call": "warn", - "@typescript-eslint/no-unsafe-argument": "warn", - "jsx-a11y/label-has-associated-control": "off", - "jsx-a11y/click-events-have-key-events": "warn", - "jsx-a11y/no-static-element-interactions": "warn" - }, - "settings": { - "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx"] - }, - "import/resolver": { - "typescript": { - "alwaysTryTypes": true, - "project": "/tsconfig.json" - } - } - } -} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 532905b1..b1ad95ac 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @wibaek @manNomi \ No newline at end of file +* @wibaek @manNomi @enunsnv @khwww \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2b503786..8b72729d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- ## 어떤 버그인가요 @@ -12,7 +11,9 @@ assignees: '' > 문제가 되는 부분에 대해 설명해주세요 ## 재현 방법(선택) + 버그를 재현할 수 있는 과정을 설명해주세요(필요하다면 사진을 첨부해주세요) + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6616524d..1984ea2a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '' +title: "" labels: enhancement -assignees: '' - +assignees: "" --- ## 어떤 기능인가요? diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..cbae2d81 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + lint: + name: Lint & Type Check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.x" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Check Prettier formatting + run: npm run format:check + + - name: TypeScript type check + run: npm run typecheck + + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.x" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + env: + NODE_ENV: production diff --git a/.github/workflows/pr-label-notify.yml b/.github/workflows/pr-label-notify.yml new file mode 100644 index 00000000..f1c25b92 --- /dev/null +++ b/.github/workflows/pr-label-notify.yml @@ -0,0 +1,99 @@ +name: PR Label Notify + +on: + pull_request_target: + types: [labeled] + repository_dispatch: + types: [pr_notify] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Send Discord notification for PR label + if: github.event_name == 'pull_request_target' && github.event.label.name == 'need_review' + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_URL: ${{ github.event.pull_request.html_url }} + REPO_NAME: ${{ github.repository }} + run: | + # Security: Only allow CODEOWNERS to trigger notifications + ALLOWED_AUTHORS=("wibaek" "manNomi" "enunsnv" "khwww") + if [[ ! " ${ALLOWED_AUTHORS[@]} " =~ " ${PR_AUTHOR} " ]]; then + echo "⚠️ Skipping notification: PR author is not a CODEOWNER" + exit 0 + fi + + TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + + jq -n \ + --arg username "프론트 PR 리뷰봇" \ + --arg avatar_url "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" \ + --arg content "리뷰가 필요합니다! <@&1368737911301865503>" \ + --arg pr_number "$PR_NUMBER" \ + --arg pr_title "$PR_TITLE" \ + --arg pr_author "$PR_AUTHOR" \ + --arg pr_url "$PR_URL" \ + --arg repo_name "$REPO_NAME" \ + --arg timestamp "$TIMESTAMP" \ + '{ + username: $username, + avatar_url: $avatar_url, + content: $content, + embeds: [{ + title: ("PR #" + $pr_number + ": " + $pr_title), + description: "리뷰 요청이 있습니다", + url: $pr_url, + color: 3447003, + fields: [ + { + name: "작성자", + value: $pr_author, + inline: true + }, + { + name: "저장소", + value: $repo_name, + inline: true + } + ], + footer: { + text: "GitHub Actions" + }, + timestamp: $timestamp + }], + allowed_mentions: { + roles: ["1368737911301865503"] + } + }' | curl -X POST "$DISCORD_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d @- + + - name: Send Discord notification for webhook + if: github.event_name == 'repository_dispatch' + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + CUSTOM_MESSAGE: ${{ github.event.client_payload.message }} + run: | + MESSAGE="${CUSTOM_MESSAGE:-리뷰가 필요합니다! <@&1368737911301865503>}" + + jq -n \ + --arg username "프론트 PR 리뷰봇" \ + --arg avatar_url "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" \ + --arg content "$MESSAGE" \ + '{ + username: $username, + avatar_url: $avatar_url, + content: $content, + allowed_mentions: { + roles: ["1368737911301865503"] + } + }' | curl -X POST "$DISCORD_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d @- diff --git a/.github/workflows/weekly-pr-summary.yml b/.github/workflows/weekly-pr-summary.yml new file mode 100644 index 00000000..34b441f6 --- /dev/null +++ b/.github/workflows/weekly-pr-summary.yml @@ -0,0 +1,101 @@ +name: Weekly PR Summary + +on: + schedule: + # 매주 금요일 13:00 UTC (22:00 KST) + - cron: '0 13 * * 5' + workflow_dispatch: + +jobs: + summary: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Collect merged PRs from last 7 days + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + run: | + # 7일 전 날짜 계산 + SINCE_DATE=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) + + echo "Fetching PRs merged since: $SINCE_DATE" + + # 최근 7일간 머지된 PR 목록 가져오기 + gh pr list \ + --repo "$REPO" \ + --state merged \ + --limit 100 \ + --json number,title,author,labels,mergedAt,url \ + --jq --arg since "$SINCE_DATE" ' + map(select(.mergedAt >= $since)) | + sort_by(.mergedAt) | + reverse + ' > prs.json + + # PR 개수 확인 + PR_COUNT=$(jq 'length' prs.json) + echo "Found $PR_COUNT merged PRs" + + # 결과 저장 + echo "PR_COUNT=$PR_COUNT" >> $GITHUB_ENV + + - name: Generate summary message + run: | + # 날짜 계산 + END_DATE=$(date -u +%Y-%m-%d) + START_DATE=$(date -u -d '7 days ago' +%Y-%m-%d) + + # PR 목록 생성 + PR_LIST=$(jq -r ' + map( + "- " + .title + + " (#" + (.number | tostring) + ") — " + .author.login + + ( + if (.labels | length) > 0 then + "\n 🏷 " + ([.labels[].name] | join(", ")) + else + "" + end + ) + + "\n 🔗 " + .url + ) | join("\n\n") + ' prs.json) + + # 메시지가 비어있을 경우 처리 + if [ "$PR_COUNT" -eq 0 ]; then + PR_LIST="이번 주에 머지된 PR이 없습니다." + fi + + # 메시지 생성 (환경 변수로 저장) + echo "MESSAGE<> $GITHUB_ENV + echo "**📊 이번 주 작업 요약**" >> $GITHUB_ENV + echo "" >> $GITHUB_ENV + echo "**기간:** $START_DATE ~ $END_DATE" >> $GITHUB_ENV + echo "**총 머지된 PR:** $PR_COUNT개" >> $GITHUB_ENV + echo "" >> $GITHUB_ENV + echo "$PR_LIST" >> $GITHUB_ENV + echo "EOFMSG" >> $GITHUB_ENV + + echo "Generated summary message" + + - name: Send to Discord + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: | + # Discord로 전송 + jq -n \ + --arg username "주간 PR 요약봇" \ + --arg avatar_url "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" \ + --arg content "$MESSAGE" \ + '{ + username: $username, + avatar_url: $avatar_url, + content: $content + }' | curl -X POST "$DISCORD_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d @- + + echo "✅ Weekly summary sent to Discord" diff --git a/.github/workflows/weekly-pr-test.yml b/.github/workflows/weekly-pr-test.yml new file mode 100644 index 00000000..f2bb69a2 --- /dev/null +++ b/.github/workflows/weekly-pr-test.yml @@ -0,0 +1,100 @@ +name: Weekly PR Test + +on: + pull_request: + types: [opened] + workflow_dispatch: + +jobs: + summary: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Collect merged PRs from last 7 days + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + run: | + # 7일 전 날짜 계산 + SINCE_DATE=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) + + echo "Fetching PRs merged since: $SINCE_DATE" + + # 최근 7일간 머지된 PR 목록 가져오기 + gh pr list \ + --repo "$REPO" \ + --state merged \ + --limit 100 \ + --json number,title,author,labels,mergedAt,url \ + --jq --arg since "$SINCE_DATE" ' + map(select(.mergedAt >= $since)) | + sort_by(.mergedAt) | + reverse + ' > prs.json + + # PR 개수 확인 + PR_COUNT=$(jq 'length' prs.json) + echo "Found $PR_COUNT merged PRs" + + # 결과 저장 + echo "PR_COUNT=$PR_COUNT" >> $GITHUB_ENV + + - name: Generate summary message + run: | + # 날짜 계산 + END_DATE=$(date -u +%Y-%m-%d) + START_DATE=$(date -u -d '7 days ago' +%Y-%m-%d) + + # PR 목록 생성 + PR_LIST=$(jq -r ' + map( + "- " + .title + + " (#" + (.number | tostring) + ") — " + .author.login + + ( + if (.labels | length) > 0 then + "\n 🏷 " + ([.labels[].name] | join(", ")) + else + "" + end + ) + + "\n 🔗 " + .url + ) | join("\n\n") + ' prs.json) + + # 메시지가 비어있을 경우 처리 + if [ "$PR_COUNT" -eq 0 ]; then + PR_LIST="이번 주에 머지된 PR이 없습니다." + fi + + # 메시지 생성 (환경 변수로 저장) + echo "MESSAGE<> $GITHUB_ENV + echo "**📊 [테스트] 이번 주 작업 요약**" >> $GITHUB_ENV + echo "" >> $GITHUB_ENV + echo "**기간:** $START_DATE ~ $END_DATE" >> $GITHUB_ENV + echo "**총 머지된 PR:** $PR_COUNT개" >> $GITHUB_ENV + echo "" >> $GITHUB_ENV + echo "$PR_LIST" >> $GITHUB_ENV + echo "EOFMSG" >> $GITHUB_ENV + + echo "Generated summary message" + + - name: Send to Discord + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: | + # Discord로 전송 + jq -n \ + --arg username "주간 PR 요약봇 (테스트)" \ + --arg avatar_url "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" \ + --arg content "$MESSAGE" \ + '{ + username: $username, + avatar_url: $avatar_url, + content: $content + }' | curl -X POST "$DISCORD_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d @- + + echo "✅ Weekly summary sent to Discord" diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 00000000..aa50024b --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,2 @@ +npx --no -- commitlint --edit ${1} + diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..2100db9e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +# 커밋 메시지는 commit-msg 훅에서 검증됩니다 diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..b3d792ae --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,7 @@ +echo "🔍 Running lint check before push..." +npm run lint + +echo "🔍 Running type check before push..." +npm run typecheck + +echo "✅ All checks passed!" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..229c3d16 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,38 @@ +# Dependencies +node_modules + +# Build outputs +.next +out +build +dist + +# Generated files +*.min.js +*.min.css +next-env.d.ts + +# Lock files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Cache +.cache +.turbo + +# Coverage +coverage + +# Sentry +.sentryclirc + +# Vercel +.vercel + +# Environment +.env* + +# IDE +.idea +.vscode diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..a3fa6147 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [2, "always", ["feat", "fix", "refactor", "style", "test", "docs", "chore"]], + }, +}; diff --git a/components.json b/components.json index 8d7eaeaa..1d82d929 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/docs/api-migration-prd.md b/docs/api-migration-prd.md new file mode 100644 index 00000000..e6c9d375 --- /dev/null +++ b/docs/api-migration-prd.md @@ -0,0 +1,328 @@ +# API 마이그레이션 PRD + +> `src/api` → `src/apis` 완전 마이그레이션 ✅ **완료** + +--- + +## 1. 개요 + +### 1.1 목표 + +`src/api` 폴더를 완전히 제거하고 `src/apis` 폴더로 통합하여 API 레이어를 단일화한다. + +### 1.2 현황 + +| 폴더 | 상태 | 파일 수 | 설명 | +| ---------- | ----------- | ------- | ---------------------------- | +| `src/api` | ✅ 삭제됨 | 0개 | 완전 제거 완료 | +| `src/apis` | ✅ 통합완료 | 70+개 | Bruno 기반 자동생성 + 커스텀 | + +--- + +## 2. 마이그레이션 대상 + +### 2.1 도메인별 현황 + +| # | 도메인 | api 파일 | apis 존재 | 상태 | 비고 | +| --- | ------------ | -------- | --------- | ------- | ----------------------- | +| 1 | auth | 8 | ✅ | ✅ 완료 | | +| 2 | community | 9 | ✅ | ✅ 완료 | postList SSR 추가 | +| 3 | mentor | 7 | ✅ | ✅ 완료 | mentee/mentors 통합 | +| 4 | mentee | 4 | ✅ | ✅ 완료 | mentor 폴더로 통합 | +| 5 | mentors | 3 | ✅ | ✅ 완료 | mentor 폴더로 통합 | +| 6 | chat | 5 | ✅ | ✅ 완료 | | +| 7 | news | 7 | ✅ | ✅ 완료 | optimistic updates 적용 | +| 8 | score | 5 | ✅ | ✅ 완료 | Scores 폴더로 통합 | +| 9 | my | 4 | ✅ | ✅ 완료 | MyPage 폴더로 통합 | +| 10 | applications | 4 | ✅ | ✅ 완료 | | +| 11 | boards | 3 | ✅ | ✅ 완료 | community로 통합 | +| 12 | file | 1 | ✅ | ✅ 완료 | image-upload로 통합 | +| 13 | reports | 1 | ✅ | ✅ 완료 | | + +**총계**: 63개 파일 → 0개 (완전 제거) ✅ + +### 2.2 상세 파일 목록 + +#### auth (8개) + +``` +src/api/auth/ +├── server/ +│ └── postReissueToken.ts # 서버사이드 유지 필요 +├── client/ +│ ├── usePostKakaoAuth.ts +│ ├── usePostAppleAuth.ts +│ ├── usePostEmailAuth.ts +│ ├── usePostSignUp.ts +│ ├── usePostEmailSignUp.ts +│ ├── usePostLogout.ts +│ └── useDeleteUserAccount.ts +└── useLogin.ts +``` + +#### community (9개) + +``` +src/api/community/client/ +├── queryKey.ts +├── useGetPostDetail.ts +├── useCreatePost.ts +├── useUpdatePost.ts +├── useDeletePost.ts +├── useCreateComment.ts +├── useDeleteComment.ts +├── usePostLike.ts +└── useDeleteLike.ts +``` + +#### mentor (7개) + +``` +src/api/mentor/client/ +├── queryKey.ts +├── useGetMentorMyProfile.ts +├── usePutMyMentorProfile.ts +├── usePostMentorApplication.ts +├── useGetMentoringList.ts +├── useGetMentoringUncheckedCount.ts +├── usePatchMentorCheckMentorings.ts +└── usePatchApprovalStatus.ts +``` + +#### mentee (4개) + +``` +src/api/mentee/client/ +├── queryKey.ts +├── useGetApplyMentoringList.ts +├── usePostApplyMentoring.ts +└── usePatchMenteeCheckMentorings.ts +``` + +#### mentors (3개) + +``` +src/api/mentors/client/ +├── queryKey.ts +├── useGetMentorList.ts +└── useGetMentorDetail.ts +``` + +#### chat (5개) + +``` +src/api/chat/clients/ +├── queryKey.ts +├── useGetChatRooms.ts +├── useGetChatHistories.ts +├── useGetPartnerInfo.ts +└── usePutChatRead.ts +``` + +#### news (7개) + +``` +src/api/news/client/ +├── queryKey.ts +├── useGetArticleList.ts +├── usePostAddArticle.ts +├── usePutModifyArticle.ts +├── useDeleteArticle.ts +├── usePostArticleLike.ts +└── useDeleteArticleLike.ts +``` + +#### score (5개) + +``` +src/api/score/client/ +├── queryKey.ts +├── useGetMyGpaScore.ts +├── usePostGpaScore.ts +├── useGetMyLanguageTestScore.ts +└── usePostLanguageTestScore.ts +``` + +#### my (4개) + +``` +src/api/my/client/ +├── queryKey.ts +├── useGetMyInfo.ts +├── usePatchMyInfo.ts +└── usePatchMyPassword.ts +``` + +#### applications (4개) + +``` +src/api/applications/client/ +├── queryKeys.ts +├── useGetApplicationsList.ts +├── usePostSubmitApplication.ts +└── useGetCompetitorsApplicationList.ts +``` + +#### boards (3개) + +``` +src/api/boards/ +├── clients/ +│ ├── QueryKeys.ts +│ └── useGetPostList.ts +└── server/ + └── getPostList.ts +``` + +#### file (1개) + +``` +src/api/file/client/ +└── useUploadProfileImagePublic.ts +``` + +#### reports (1개) + +``` +src/api/reports/client/ +└── usePostReport.ts +``` + +--- + +## 3. 마이그레이션 규칙 + +### 3.1 네이밍 컨벤션 + +| 항목 | Before (api) | After (apis) | +| -------- | ------------------------ | ------------------------- | +| Query 훅 | `useGetXxx.ts` | `useGetXxx.ts` (동일) | +| Mutation | `usePostXxx.ts` | `usePostXxx.ts` (동일) | +| QueryKey | `queryKey.ts` (도메인별) | `queryKeys.ts` (중앙집중) | +| API 함수 | 훅 내부 정의 | `api.ts` 에서 export | +| Import | `@/api/{domain}/client/` | `@/apis/{domain}/` | + +### 3.2 QueryKey 통합 + +**Before** (각 도메인별 분산): + +```typescript +// src/api/community/client/queryKey.ts +export enum QueryKeys { + postDetail = "postDetail", + postList = "postList", +} +``` + +**After** (중앙 집중): + +```typescript +// src/apis/queryKeys.ts +export const QueryKeys = { + community: { + postDetail: "community.postDetail", + postList: "community.postList", + }, + // ... +}; +``` + +### 3.3 비즈니스 로직 보존 + +마이그레이션 시 다음 로직은 **반드시** 보존: + +- [ ] `router.push()` / `router.replace()` 리다이렉트 +- [ ] `toast.success()` / `toast.error()` 알림 +- [ ] `useAuthStore` 상태 관리 +- [ ] `queryClient.invalidateQueries()` 캐시 무효화 +- [ ] `onSuccess` / `onError` 콜백 로직 + +--- + +## 4. 작업 체크리스트 + +### 4.1 도메인별 체크리스트 템플릿 + +```markdown +#### [도메인명] 마이그레이션 + +- [ ] api.ts URL/메서드 확인 및 수정 +- [ ] 훅 마이그레이션 (비즈니스 로직 보존) +- [ ] QueryKey 통합 +- [ ] 컴포넌트 import 경로 변경 +- [ ] 서버사이드 API 처리 (해당시) +- [ ] TypeScript 에러 확인 +- [ ] 기능 테스트 +- [ ] 레거시 파일 삭제 +``` + +### 4.2 전체 진행 상황 + +| 도메인 | 분석 | 마이그레이션 | 테스트 | 삭제 | 완료 | +| ------------ | ---- | ------------ | ------ | ---- | ---- | +| auth | ✅ | ✅ | ✅ | ✅ | ✅ | +| community | ✅ | ✅ | ✅ | ✅ | ✅ | +| mentor | ✅ | ✅ | ✅ | ✅ | ✅ | +| mentee | ✅ | ✅ | ✅ | ✅ | ✅ | +| mentors | ✅ | ✅ | ✅ | ✅ | ✅ | +| chat | ✅ | ✅ | ✅ | ✅ | ✅ | +| news | ✅ | ✅ | ✅ | ✅ | ✅ | +| score | ✅ | ✅ | ✅ | ✅ | ✅ | +| my | ✅ | ✅ | ✅ | ✅ | ✅ | +| applications | ✅ | ✅ | ✅ | ✅ | ✅ | +| boards | ✅ | ✅ | ✅ | ✅ | ✅ | +| file | ✅ | ✅ | ✅ | ✅ | ✅ | +| reports | ✅ | ✅ | ✅ | ✅ | ✅ | + +**범례**: ⬜ 대기 | 🔄 진행중 | ✅ 완료 + +--- + +## 5. 우선순위 + +### 5.1 권장 순서 + +1. **auth** - 인증 로직, 가장 중요 +2. **my** - 내 정보, auth와 연관 +3. **community** - 커뮤니티 기능 +4. **mentor/mentee/mentors** - 멘토링 기능 (함께 진행) +5. **chat** - 채팅 기능 +6. **news** - 뉴스/아티클 +7. **score** - 성적 관리 +8. **applications** - 지원 관리 +9. **boards** - 게시판 +10. **file** - 파일 업로드 +11. **reports** - 신고 기능 + +### 5.2 의존성 주의사항 + +- `auth/server/postReissueToken.ts` → axios interceptor에서 사용 +- `mentor/mentee` → QueryKey 공유 가능성 확인 +- `boards/community` → 유사 기능, 통합 검토 + +--- + +## 6. 완료 조건 + +- [x] `src/api` 폴더 완전 삭제 +- [x] 모든 import가 `@/apis/` 경로 사용 +- [ ] TypeScript 에러 0개 (일부 타입 추론 이슈 남음) +- [ ] ESLint 에러 0개 +- [x] 빌드 성공 +- [x] 모든 기능 정상 동작 + +--- + +## 7. 커밋 컨벤션 + +``` +refactor: migrate {domain} from api to apis + +- Migrate {N} hooks to apis/{domain} +- Update component imports +- Remove legacy api/{domain} folder +``` + +--- + +**최종 수정일**: 2025-12-28 diff --git a/docs/development-workflow.md b/docs/development-workflow.md new file mode 100644 index 00000000..89d00af4 --- /dev/null +++ b/docs/development-workflow.md @@ -0,0 +1,304 @@ +# 개발 워크플로우 가이드 + +이 문서는 solid-connect-web 프로젝트의 개발 워크플로우를 설명합니다. + +## 목차 + +1. [커밋 메시지 규칙](#커밋-메시지-규칙) +2. [Git Hooks](#git-hooks) +3. [스크립트 사용법](#스크립트-사용법) +4. [CI/CD 프로세스](#cicd-프로세스) +5. [일반적인 개발 워크플로우](#일반적인-개발-워크플로우) +6. [문제 해결](#문제-해결) + +--- + +## 커밋 메시지 규칙 + +### 형식 + +``` +: + + (선택) +``` + +### 타입 설명 + +| 타입 | 설명 | 예시 | +| ---------- | -------------------------------------------------- | ----------------------------------------------- | +| `feat` | 새로운 기능 추가, 기존 기능을 요구사항에 맞게 수정 | `feat: 로그인 페이지 인풋 필드 디자인 업데이트` | +| `fix` | 버그 수정 | `fix: 린트 에러 수정` | +| `refactor` | 기능 변화 없이 코드 리팩터링 | `refactor: 컴포넌트 구조 개선` | +| `style` | 코드 스타일, 포맷팅 수정 | `style: 코드 포맷팅 적용` | +| `test` | 테스트 코드 추가/수정 | `test: 로그인 유닛 테스트 추가` | +| `docs` | 문서(주석) 수정 | `docs: README 업데이트` | +| `chore` | 패키지 매니저 수정, 기타 수정 | `chore: 의존성 업데이트` | + +### 올바른 예시 + +```bash +feat: 로그인 페이지 인풋 필드 디자인 업데이트 + +- border 색상을 border-k-100으로 명시 +- 고정 높이 제거하여 padding과 line-height로 자동 계산 +``` + +```bash +fix: 린트 에러 수정 + +- any 타입을 unknown으로 변경 +- 중복된 className prop 제거 +``` + +```bash +chore: 사용하지 않는 패키지 제거 +``` + +### 잘못된 예시 + +```bash +update login page # ❌ 타입 없음 +feat login # ❌ 콜론 없음 +FEAT: Login page update # ❌ 대문자 타입 +feat : 로그인 업데이트 # ❌ 콜론 앞에 공백 +``` + +--- + +## Git Hooks + +프로젝트는 [Husky](https://typicode.github.io/husky/)를 사용하여 Git hooks를 관리합니다. + +### 설치된 Hooks + +#### commit-msg + +커밋 메시지를 [commitlint](https://commitlint.js.org/)로 검증합니다. + +- 규칙에 맞지 않는 커밋 메시지는 **자동으로 차단**됩니다. +- 올바른 형식: `: ` + +#### pre-commit + +현재는 비활성화 상태입니다. 필요시 린트 검사 등을 추가할 수 있습니다. + +### Hooks가 동작하지 않는 경우 + +```bash +# Husky 재설치 +npm run prepare + +# 실행 권한 부여 (macOS/Linux) +chmod +x .husky/commit-msg +chmod +x .husky/pre-commit +``` + +--- + +## 스크립트 사용법 + +### 개발 + +```bash +npm run dev # 개발 서버 실행 +npm run build # 프로덕션 빌드 +npm run start # 프로덕션 서버 실행 +``` + +### 린트 + +```bash +npm run lint # ESLint 실행 +npm run lint:fix # ESLint 자동 수정 +``` + +### 포맷팅 + +```bash +npm run format # Prettier로 코드 포맷팅 +npm run format:check # Prettier 포맷팅 체크 (CI용) +``` + +### 타입 체크 + +```bash +npm run typecheck # TypeScript 타입 체크 +``` + +### 통합 명령어 + +```bash +npm run lint:all # lint + format:check + typecheck +npm run fix:all # lint:fix + format (자동 수정) +``` + +### 추천 워크플로우 + +코드 수정 후 커밋 전에: + +```bash +npm run fix:all # 모든 자동 수정 적용 +``` + +--- + +## CI/CD 프로세스 + +### 트리거 + +- `main` 브랜치로 push +- `develop` 브랜치로 push +- `main` 또는 `develop` 브랜치로 PR 생성 + +### Jobs + +#### 1. Lint & Type Check + +- ESLint 실행 +- Prettier 포맷팅 체크 +- TypeScript 타입 체크 + +#### 2. Build + +- Next.js 프로덕션 빌드 + +#### 3. PR Title Validation (PR만) + +- PR 제목이 커밋 메시지 규칙을 준수하는지 검증 + +### CI 실패 대응 + +1. **ESLint 실패**: `npm run lint:fix`로 자동 수정 +2. **Prettier 실패**: `npm run format`으로 자동 수정 +3. **타입 체크 실패**: TypeScript 오류 직접 수정 +4. **빌드 실패**: 빌드 로그 확인 후 오류 수정 + +--- + +## 일반적인 개발 워크플로우 + +### 1. 기능 개발 + +```bash +# 1. 개발 브랜치 생성 +git checkout -b feat/new-feature + +# 2. 개발 서버 실행 +npm run dev + +# 3. 코드 작성... +``` + +### 2. 커밋 전 검증 + +```bash +# 자동 수정 및 검증 +npm run fix:all + +# 또는 개별 실행 +npm run lint:fix +npm run format +npm run typecheck +``` + +### 3. 커밋 + +```bash +git add . +git commit -m "feat: 새로운 기능 추가" +# commitlint가 자동으로 메시지 검증 +``` + +### 4. 푸시 및 PR + +```bash +git push origin feat/new-feature +# GitHub에서 PR 생성 +# CI가 자동으로 실행 +``` + +--- + +## 문제 해결 + +### 커밋 메시지 검증 실패 + +**문제**: 커밋 메시지가 규칙에 맞지 않아 커밋 실패 + +**해결**: + +1. 커밋 메시지 형식 확인: `: ` +2. 허용된 타입 확인: `feat`, `fix`, `refactor`, `style`, `test`, `docs`, `chore` +3. 올바른 형식으로 다시 커밋 + +### 커밋 메시지 수정 + +```bash +# 가장 최근 커밋 메시지 수정 +git commit --amend -m "fix: 올바른 커밋 메시지" + +# 이미 푸시한 경우 (주의: force push) +git push --force-with-lease origin branch-name +``` + +### CI 실패 + +**문제**: CI에서 린트, 포맷팅, 또는 빌드 실패 + +**해결**: + +```bash +# 1. 로컬에서 동일한 검증 실행 +npm run lint:all + +# 2. 자동 수정 시도 +npm run fix:all + +# 3. 빌드 테스트 +npm run build + +# 4. 수정 후 다시 푸시 +git add . +git commit -m "fix: CI 오류 수정" +git push +``` + +### Git hooks가 동작하지 않음 + +**문제**: 커밋 시 commitlint가 실행되지 않음 + +**해결**: + +```bash +# 1. Husky 재설치 +npm run prepare + +# 2. .husky/commit-msg 파일 확인 +cat .husky/commit-msg + +# 3. 실행 권한 확인 (macOS/Linux) +chmod +x .husky/commit-msg +``` + +### node_modules 문제 + +**문제**: 의존성 관련 오류 발생 + +**해결**: + +```bash +# node_modules 삭제 후 재설치 +rm -rf node_modules +npm install +``` + +--- + +## 참고 자료 + +- [Husky Documentation](https://typicode.github.io/husky/) +- [Commitlint Documentation](https://commitlint.js.org/) +- [Conventional Commits](https://www.conventionalcommits.org/) +- [ESLint Documentation](https://eslint.org/) +- [Prettier Documentation](https://prettier.io/) diff --git a/docs/eslint-prettier-migration-prd.md b/docs/eslint-prettier-migration-prd.md new file mode 100644 index 00000000..2b812ecc --- /dev/null +++ b/docs/eslint-prettier-migration-prd.md @@ -0,0 +1,285 @@ +# ESLint & Prettier 마이그레이션 PRD + +## 1. 개요 + +### 1.1 목적 + +현재 프로젝트의 ESLint와 Prettier 설정을 개선하고, 개발 워크플로우에 자동 린팅 및 포맷팅 기능을 추가하여 코드 품질과 일관성을 향상시킵니다. + +### 1.2 배경 + +- 현재 `.eslintrc.json` 파일에 주석이 포함되어 있어 JSON 파싱 오류 가능성 +- `npm run lint` 명령어만 존재하며 자동 수정 기능 없음 +- 린트 자동 수정 및 포맷팅을 위한 명령어 부재 +- 개발자 경험(DX) 개선 필요 + +## 2. 현재 상황 분석 + +### 2.1 현재 설정 + +- **ESLint**: `8.56.0` 사용, `.eslintrc.json` 형식 +- **Prettier**: `.prettierrc.json` 설정 파일 존재 +- **스크립트**: `npm run lint` (체크만 수행) +- **통합**: `eslint-config-prettier`로 충돌 방지 설정됨 + +### 2.2 문제점 + +1. `.eslintrc.json`에 주석이 포함되어 있어 유효하지 않은 JSON 형식 +2. 자동 수정 명령어 부재 (`--fix` 옵션 미사용) +3. Prettier 포맷팅 명령어 부재 +4. 개발 중 자동 포맷팅/린팅 워크플로우 없음 + +## 3. 목표 + +### 3.1 주요 목표 + +1. ESLint 설정을 `.eslintrc.js`로 마이그레이션하여 주석 지원 및 동적 구성 가능 +2. 자동 수정 및 포맷팅 명령어 추가 +3. 개발 워크플로우에 자동 린팅/포맷팅 통합 +4. CI/CD 파이프라인과의 일관성 유지 + +### 3.2 성공 지표 + +- 모든 린트 오류 자동 수정 가능 +- 일관된 코드 포맷팅 적용 +- 개발자 생산성 향상 (수동 수정 시간 감소) +- 코드 리뷰 시 스타일 관련 논의 감소 + +## 4. 요구사항 + +### 4.1 기능 요구사항 + +#### FR1: ESLint 설정 마이그레이션 + +- `.eslintrc.json` → `.eslintrc.js` 변환 +- 기존 설정 유지 (규칙, 플러그인, 확장) +- 주석 지원으로 설정 문서화 개선 + +#### FR2: 자동 수정 명령어 추가 + +- `npm run lint:fix`: ESLint 자동 수정 +- `npm run format`: Prettier 포맷팅 +- `npm run format:check`: Prettier 포맷팅 체크 (CI용) + +#### FR3: 통합 명령어 + +- `npm run lint:all`: 린트 체크 + 자동 수정 + 포맷팅 체크 +- `npm run fix:all`: 린트 자동 수정 + 포맷팅 적용 + +#### FR4: 개발 워크플로우 통합 + +- VS Code 설정 파일 추가 (선택사항) +- Git hooks 통합 (이미 Husky 설정됨) + +### 4.2 비기능 요구사항 + +#### NFR1: 호환성 + +- 기존 ESLint 규칙과 100% 호환 +- 기존 Prettier 설정 유지 +- Next.js 14.2와 호환 + +#### NFR2: 성능 + +- 린트 실행 시간: 기존과 동일 수준 유지 +- 포맷팅 실행 시간: 전체 프로젝트 기준 10초 이내 + +#### NFR3: 유지보수성 + +- 설정 파일에 주석으로 문서화 +- 명확한 명령어 네이밍 +- README에 사용법 문서화 + +## 5. 마이그레이션 계획 + +### 5.1 단계별 계획 + +#### Phase 1: 설정 파일 마이그레이션 (1일) + +1. `.eslintrc.json` → `.eslintrc.js` 변환 + - 주석을 유효한 JavaScript 주석으로 변환 + - 기존 설정 100% 유지 + - 테스트: `npm run lint` 실행하여 동일한 결과 확인 + +2. Prettier 설정 검증 + - `.prettierrc.json` 설정 확인 + - ESLint와의 충돌 확인 + +#### Phase 2: 명령어 추가 (0.5일) + +1. `package.json` 스크립트 추가 + + ```json + { + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "lint:all": "npm run lint && npm run format:check", + "fix:all": "npm run lint:fix && npm run format" + } + ``` + +2. `.prettierignore` 파일 생성 (필요시) + +#### Phase 3: 테스트 및 검증 (0.5일) + +1. 명령어 테스트 + - 각 명령어 실행 및 결과 확인 + - CI 워크플로우와의 호환성 확인 + +2. 문서화 + - `README.md` 또는 `docs/`에 사용법 추가 + - 팀 공유 + +#### Phase 4: 통합 및 배포 (0.5일) + +1. Git hooks 업데이트 (선택사항) + - pre-commit에 `lint:fix` 추가 검토 + - 현재는 commitlint만 사용 중이므로 선택사항 + +2. CI/CD 확인 + - 기존 CI 워크플로우 동작 확인 + - 필요시 `format:check` 추가 + +### 5.2 파일 구조 + +``` +프로젝트 루트/ +├── .eslintrc.js (신규, .eslintrc.json 대체) +├── .eslintrc.json (삭제) +├── .prettierrc.json (기존 유지) +├── .prettierignore (신규, 필요시) +├── package.json (스크립트 추가) +└── docs/ + └── eslint-prettier-migration-prd.md (이 문서) +``` + +## 6. 구현 상세 + +### 6.1 ESLint 설정 변환 + +#### 현재 (.eslintrc.json) + +```json +{ + "root": true, + "parser": "@typescript-eslint/parser", + ... +} +``` + +#### 변환 후 (.eslintrc.js) + +```javascript +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + // 주석 지원 가능 + ... +}; +``` + +### 6.2 package.json 스크립트 + +```json +{ + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "lint:all": "npm run lint && npm run format:check", + "fix:all": "npm run lint:fix && npm run format" + } +} +``` + +### 6.3 .prettierignore (필요시) + +``` +node_modules +.next +out +build +dist +*.min.js +package-lock.json +``` + +## 7. 위험 요소 및 대응 방안 + +### 7.1 위험 요소 + +| 위험 | 영향도 | 대응 방안 | +| ------------------------------------ | ------ | ------------------------------------------- | +| 기존 설정과의 불일치 | 높음 | Phase 1에서 철저한 테스트, 기존 결과와 비교 | +| 대량 파일 변경으로 인한 PR 크기 증가 | 중간 | 단계별 커밋, 마이그레이션과 포맷팅 분리 | +| 팀원의 워크플로우 변경 필요 | 낮음 | 명확한 문서화 및 공유 | + +### 7.2 롤백 계획 + +- `.eslintrc.json` 백업 유지 +- Git을 통한 이전 버전 복구 가능 +- 단계별 커밋으로 선택적 롤백 가능 + +## 8. 검증 방법 + +### 8.1 기능 검증 + +- [ ] `.eslintrc.js`로 동일한 린트 결과 확인 +- [ ] `npm run lint:fix`로 자동 수정 동작 확인 +- [ ] `npm run format`으로 포맷팅 동작 확인 +- [ ] CI 워크플로우 정상 동작 확인 + +### 8.2 회귀 테스트 + +- [ ] 기존 린트 경고/에러 개수 동일 +- [ ] 빌드 성공 확인 +- [ ] 개발 서버 정상 동작 확인 + +## 9. 일정 + +| 단계 | 작업 | 예상 소요 시간 | 담당 | +| -------- | ---------------------- | -------------- | ------ | +| Phase 1 | 설정 파일 마이그레이션 | 1일 | 개발자 | +| Phase 2 | 명령어 추가 | 0.5일 | 개발자 | +| Phase 3 | 테스트 및 검증 | 0.5일 | 개발자 | +| Phase 4 | 통합 및 배포 | 0.5일 | 개발자 | +| **총계** | | **2.5일** | | + +## 10. 후속 작업 + +### 10.1 단기 (1주 이내) + +- 팀원 대상 사용법 공유 +- VS Code 설정 파일 추가 (선택사항) +- Git hooks에 자동 포맷팅 추가 검토 + +### 10.2 중기 (1개월 이내) + +- ESLint 9.0+ Flat Config 마이그레이션 검토 +- 추가 린트 규칙 도입 검토 +- 코드 리뷰 가이드라인 업데이트 + +## 11. 참고 자료 + +- [ESLint Configuration Files](https://eslint.org/docs/latest/use/configure/configuration-files) +- [Prettier Documentation](https://prettier.io/docs/en/) +- [Next.js ESLint Configuration](https://nextjs.org/docs/app/building-your-application/configuring/eslint) +- [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) + +## 12. 승인 + +- [ ] 기술 리더 승인 +- [ ] 팀 리뷰 완료 +- [ ] 일정 확정 + +--- + +**작성일**: 2025-01-XX +**작성자**: 개발팀 +**버전**: 1.0 diff --git a/docs/husky-ci-workflow-prd.md b/docs/husky-ci-workflow-prd.md new file mode 100644 index 00000000..bd492c50 --- /dev/null +++ b/docs/husky-ci-workflow-prd.md @@ -0,0 +1,630 @@ +# Husky & CI 워크플로우 완성 PRD + +## 1. 개요 + +### 1.1 목적 + +Git hooks(Husky)와 CI/CD 파이프라인을 완성하여 코드 품질을 자동으로 검증하고, 일관된 커밋 메시지 형식을 보장하며, 안정적인 배포 프로세스를 구축합니다. + +### 1.2 배경 + +- 현재 Husky와 commitlint가 기본적으로 설정되어 있으나 최적화 필요 +- CI 워크플로우가 기본적으로 구성되어 있으나 개선 여지 존재 +- 개발 워크플로우 전반에 걸친 자동화 부족 +- 코드 품질 검증 프로세스의 일관성 부족 + +### 1.3 범위 + +- Git Hooks (Husky) 설정 완성 +- Commitlint 커밋 메시지 검증 강화 +- CI/CD 파이프라인 최적화 +- 개발자 워크플로우 문서화 + +## 2. 현재 상황 분석 + +### 2.1 현재 설정 상태 + +#### Husky + +- ✅ 설치 및 초기화 완료 (`husky@9.1.7`) +- ✅ `prepare` 스크립트 설정됨 +- ✅ `.husky/commit-msg` 훅 존재 (commitlint 실행) +- ✅ `.husky/pre-commit` 훅 존재 (현재 주석만 있음) + +#### Commitlint + +- ✅ 설치 완료 (`@commitlint/cli@20.2.0`, `@commitlint/config-conventional@20.2.0`) +- ✅ `commitlint.config.js` 설정 파일 존재 +- ✅ 커밋 메시지 타입 제한: `feat`, `fix`, `refactor`, `style`, `test`, `docs`, `chore` + +#### CI/CD + +- ✅ `.github/workflows/ci.yml` 존재 +- ✅ `lint`와 `build` job이 병렬 실행 +- ✅ Node.js 22.x 사용 +- ✅ npm 캐싱 적용 + +### 2.2 문제점 및 개선 사항 + +1. **Git Hooks** + - `pre-commit` 훅이 비어있음 (활용 가능) + - `pre-push` 훅이 없음 (선택적 추가 가능) + +2. **CI/CD** + - Prettier 포맷팅 체크 없음 + - 커밋 메시지 검증이 CI에서 수행되지 않음 + - 테스트 단계 없음 (향후 확장 가능) + +3. **문서화** + - 개발 워크플로우 가이드 부족 + - 커밋 메시지 규칙 상세 설명 필요 + +4. **자동화** + - 자동 수정/포맷팅 명령어 부재 + - 개발 중 실시간 검증 부족 + +## 3. 목표 + +### 3.1 주요 목표 + +1. **Git Hooks 완성** + - `pre-commit`: 빠른 검증 (선택적) + - `commit-msg`: 커밋 메시지 검증 (현재 동작 중) + - `pre-push`: 선택적 검증 (CI와 중복 방지) + +2. **CI/CD 파이프라인 강화** + - 린트, 타입 체크, 빌드 검증 + - Prettier 포맷팅 체크 추가 + - 커밋 메시지 검증 (PR 제목) + - 병렬 실행으로 성능 최적화 + +3. **개발자 경험 개선** + - 명확한 워크플로우 가이드 + - 자동 수정 명령어 제공 + - 빠른 피드백 루프 + +4. **코드 품질 보장** + - 일관된 커밋 메시지 형식 + - 자동 코드 품질 검증 + - 배포 전 자동 검증 + +### 3.2 성공 지표 + +- 커밋 메시지 규칙 준수율 100% +- CI 실패율 감소 (잘못된 코드 배포 방지) +- 개발자 생산성 향상 (수동 검증 시간 감소) +- 코드 리뷰 시간 단축 (스타일 이슈 감소) + +## 4. 요구사항 + +### 4.1 기능 요구사항 + +#### FR1: Git Hooks 완성 + +**FR1.1: pre-commit 훅 (선택적)** + +- 빠른 검증 수행 (선택적) +- 현재는 비활성화 상태 유지 가능 +- 향후 필요시 활성화 가능하도록 구조화 + +**FR1.2: commit-msg 훅 (필수)** + +- commitlint를 통한 커밋 메시지 검증 +- 규칙 위반 시 커밋 차단 +- 명확한 에러 메시지 제공 + +**FR1.3: pre-push 훅 (선택적)** + +- CI에서 이미 검증하므로 기본적으로 비활성화 +- 필요시 로컬 빌드 체크만 수행 (선택적) + +#### FR2: CI/CD 파이프라인 강화 + +**FR2.1: Lint Job** + +- ESLint 실행 +- TypeScript 타입 체크 +- Prettier 포맷팅 체크 (신규) + +**FR2.2: Build Job** + +- Next.js 빌드 검증 +- 프로덕션 환경 변수 검증 + +**FR2.3: 커밋 메시지 검증 (PR)** + +- PR 제목을 커밋 메시지 규칙으로 검증 +- PR 머지 커밋 메시지 검증 + +**FR2.4: 병렬 실행** + +- `lint`와 `build` job 병렬 실행 +- 캐싱을 통한 실행 시간 최적화 + +#### FR3: package.json 스크립트 추가 + +**FR3.1: 린트 관련** + +- `lint`: 린트 체크 +- `lint:fix`: 린트 자동 수정 + +**FR3.2: 포맷팅 관련** + +- `format`: Prettier 포맷팅 적용 +- `format:check`: Prettier 포맷팅 체크 (CI용) + +**FR3.3: 통합 명령어** + +- `lint:all`: 린트 + 포맷팅 체크 +- `fix:all`: 린트 자동 수정 + 포맷팅 적용 + +#### FR4: 문서화 + +**FR4.1: 개발 워크플로우 가이드** + +- 커밋 메시지 작성 가이드 +- Git hooks 동작 방식 설명 +- CI/CD 프로세스 설명 + +**FR4.2: 문제 해결 가이드** + +- 일반적인 오류 및 해결 방법 +- 커밋 메시지 수정 방법 +- CI 실패 시 대응 방법 + +### 4.2 비기능 요구사항 + +#### NFR1: 성능 + +- Git hooks 실행 시간: 3초 이내 +- CI 실행 시간: 10분 이내 (병렬 실행) +- 개발자 워크플로우 방해 최소화 + +#### NFR2: 호환성 + +- Node.js 22.x 호환 +- Next.js 14.2 호환 +- 기존 워크플로우와의 호환성 유지 + +#### NFR3: 유지보수성 + +- 설정 파일 명확한 문서화 +- 변경 이력 추적 가능 +- 팀원 쉽게 이해 가능한 구조 + +#### NFR4: 확장성 + +- 향후 테스트 추가 용이 +- 추가 검증 규칙 추가 용이 +- 다른 브랜치 전략 적용 용이 + +## 5. 구현 계획 + +### 5.1 Phase 1: Git Hooks 완성 (0.5일) + +#### 1.1 pre-commit 훅 정리 + +- 현재 주석만 있는 상태 유지 또는 제거 +- 필요시 빠른 검증 로직 추가 가능하도록 구조화 + +#### 1.2 commit-msg 훅 검증 + +- 현재 설정이 올바르게 동작하는지 확인 +- 에러 메시지 개선 (필요시) + +#### 1.3 commitlint 설정 최적화 + +- `commitlint.config.js` 검토 및 개선 +- 커밋 메시지 예시 추가 + +### 5.2 Phase 2: CI/CD 파이프라인 강화 (1일) + +#### 2.1 Prettier 체크 추가 + +- `lint` job에 `format:check` 단계 추가 +- 실패 시 명확한 에러 메시지 + +#### 2.2 PR 커밋 메시지 검증 + +- PR 제목 검증 job 추가 (선택적) +- PR 머지 커밋 메시지 검증 + +#### 2.3 CI 워크플로우 최적화 + +- 캐싱 전략 개선 +- 병렬 실행 최적화 +- 실패 시 빠른 피드백 + +#### 2.4 환경 변수 검증 + +- 필수 환경 변수 체크 (선택적) +- 빌드 시 환경 변수 검증 + +### 5.3 Phase 3: package.json 스크립트 추가 (0.5일) + +#### 3.1 린트 스크립트 + +```json +{ + "lint": "next lint", + "lint:fix": "next lint --fix" +} +``` + +#### 3.2 포맷팅 스크립트 + +```json +{ + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"" +} +``` + +#### 3.3 통합 스크립트 + +```json +{ + "lint:all": "npm run lint && npm run format:check", + "fix:all": "npm run lint:fix && npm run format" +} +``` + +### 5.4 Phase 4: 문서화 (1일) + +#### 4.1 개발 워크플로우 가이드 작성 + +- `docs/development-workflow.md` 생성 +- 커밋 메시지 작성 가이드 +- Git hooks 동작 설명 +- CI/CD 프로세스 설명 + +#### 4.2 문제 해결 가이드 작성 + +- 일반적인 오류 및 해결 방법 +- 커밋 메시지 수정 방법 +- CI 실패 대응 방법 + +#### 4.3 README 업데이트 + +- 새로운 스크립트 사용법 추가 +- 워크플로우 링크 추가 + +### 5.5 Phase 5: 테스트 및 검증 (0.5일) + +#### 5.1 Git Hooks 테스트 + +- 커밋 메시지 검증 테스트 +- 잘못된 커밋 메시지 차단 확인 +- 올바른 커밋 메시지 통과 확인 + +#### 5.2 CI 워크플로우 테스트 + +- 각 job 정상 동작 확인 +- 병렬 실행 확인 +- 실패 시나리오 테스트 + +#### 5.3 스크립트 테스트 + +- 모든 스크립트 실행 확인 +- 예상 결과 확인 + +## 6. 구현 상세 + +### 6.1 Git Hooks 구조 + +``` +.husky/ +├── _/ # Husky 내부 파일 +├── commit-msg # 커밋 메시지 검증 +│ └── npx --no -- commitlint --edit ${1} +└── pre-commit # pre-commit 검증 (선택적) + └── # 현재 비활성화 또는 빠른 검증 +``` + +### 6.2 CI 워크플로우 구조 + +```yaml +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + lint: + - ESLint 체크 + - TypeScript 타입 체크 + - Prettier 포맷팅 체크 + + build: + - Next.js 빌드 + - 환경 변수 검증 +``` + +### 6.3 commitlint 설정 + +```javascript +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [2, "always", ["feat", "fix", "refactor", "style", "test", "docs", "chore"]], + }, +}; +``` + +### 6.4 package.json 스크립트 + +```json +{ + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "lint:all": "npm run lint && npm run format:check", + "fix:all": "npm run lint:fix && npm run format", + "prepare": "husky" + } +} +``` + +## 7. 파일 구조 + +``` +프로젝트 루트/ +├── .husky/ +│ ├── commit-msg (커밋 메시지 검증) +│ └── pre-commit (선택적) +├── .github/ +│ └── workflows/ +│ └── ci.yml (CI 워크플로우) +├── commitlint.config.js (커밋 메시지 규칙) +├── package.json (스크립트 추가) +└── docs/ + ├── development-workflow.md (신규) + └── troubleshooting.md (신규) +``` + +## 8. 위험 요소 및 대응 방안 + +### 8.1 위험 요소 + +| 위험 | 영향도 | 대응 방안 | +| --------------------------------- | ------ | ------------------------------------------ | +| Git hooks가 너무 느려서 개발 방해 | 중간 | 최소한의 검증만 수행, CI에서 상세 검증 | +| CI 실행 시간 증가 | 낮음 | 병렬 실행 및 캐싱으로 최적화 | +| 커밋 메시지 규칙이 너무 엄격함 | 낮음 | 규칙을 점진적으로 강화, 명확한 가이드 제공 | +| 팀원의 워크플로우 변경 필요 | 낮음 | 명확한 문서화 및 온보딩 | +| 환경 변수 누락으로 인한 빌드 실패 | 중간 | 필수 환경 변수 명시 및 검증 | + +### 8.2 롤백 계획 + +- Git hooks: `.husky/` 디렉토리 백업 또는 Git으로 복구 +- CI 워크플로우: 이전 버전으로 롤백 가능 +- package.json: 스크립트 제거로 롤백 가능 + +## 9. 검증 방법 + +### 9.1 기능 검증 + +- [ ] 커밋 메시지 검증 동작 확인 + - [ ] 올바른 형식: 통과 확인 + - [ ] 잘못된 형식: 차단 확인 +- [ ] CI 워크플로우 정상 동작 + - [ ] lint job 성공 + - [ ] build job 성공 + - [ ] 병렬 실행 확인 +- [ ] 스크립트 정상 동작 + - [ ] `npm run lint:fix` 동작 확인 + - [ ] `npm run format` 동작 확인 + - [ ] `npm run fix:all` 동작 확인 + +### 9.2 회귀 테스트 + +- [ ] 기존 커밋 메시지 형식 호환성 확인 +- [ ] 기존 CI 워크플로우 동작 확인 +- [ ] 빌드 성공 확인 +- [ ] 개발 서버 정상 동작 확인 + +### 9.3 성능 테스트 + +- [ ] Git hooks 실행 시간 측정 (3초 이내) +- [ ] CI 실행 시간 측정 (10분 이내) +- [ ] 캐싱 효과 확인 + +## 10. 일정 + +| 단계 | 작업 | 예상 소요 시간 | 담당 | +| -------- | --------------------- | -------------- | ------ | +| Phase 1 | Git Hooks 완성 | 0.5일 | 개발자 | +| Phase 2 | CI/CD 파이프라인 강화 | 1일 | 개발자 | +| Phase 3 | package.json 스크립트 | 0.5일 | 개발자 | +| Phase 4 | 문서화 | 1일 | 개발자 | +| Phase 5 | 테스트 및 검증 | 0.5일 | 개발자 | +| **총계** | | **3.5일** | | + +## 11. 커밋 메시지 규칙 상세 + +### 11.1 형식 + +``` +: + + +``` + +### 11.2 타입 설명 + +- **feat**: 새로운 기능 추가, 기존 기능을 요구사항에 맞게 수정 +- **fix**: 버그 수정 +- **refactor**: 기능 변화 없이 코드 리팩터링 (변수명 변경 등) +- **style**: 코드 스타일, 포맷팅 수정 +- **test**: 테스트 코드 추가/수정 +- **docs**: 문서(주석) 수정 +- **chore**: 패키지 매니저 수정, 기타 수정 (.gitignore 등) + +### 11.3 예시 + +**올바른 예시:** + +``` +feat: 로그인 페이지 인풋 필드 디자인 업데이트 + +- border 색상을 border-k-100으로 명시 +- 고정 높이 제거하여 padding과 line-height로 자동 계산 +``` + +``` +fix: 린트 에러 수정 + +- any 타입을 unknown으로 변경 +- 중복된 className prop 제거 +``` + +**잘못된 예시:** + +``` +update login page # 타입 없음 +feat login # subject가 너무 짧음 +FEAT: Login page update # 대문자 타입 +``` + +## 12. 개발 워크플로우 + +### 12.1 일반적인 워크플로우 + +1. **코드 작성** + + ```bash + # 개발 중 + npm run dev + ``` + +2. **코드 수정 후 검증** + + ```bash + # 자동 수정 및 포맷팅 + npm run fix:all + + # 또는 개별 실행 + npm run lint:fix + npm run format + ``` + +3. **커밋** + + ```bash + git add . + git commit -m "feat: 새로운 기능 추가" + # commitlint가 자동으로 검증 + ``` + +4. **푸시 및 PR** + ```bash + git push origin feature-branch + # CI가 자동으로 실행 + ``` + +### 12.2 커밋 메시지 수정 + +커밋 메시지를 수정해야 하는 경우: + +```bash +# 가장 최근 커밋 메시지 수정 +git commit --amend -m "fix: 올바른 커밋 메시지" + +# 이미 푸시한 경우 +git push --force-with-lease origin branch-name +``` + +## 13. 문제 해결 + +### 13.1 커밋 메시지 검증 실패 + +**문제**: 커밋 메시지가 규칙에 맞지 않아 커밋 실패 + +**해결**: + +1. 커밋 메시지 형식 확인: `: ` +2. 허용된 타입 확인: `feat`, `fix`, `refactor`, `style`, `test`, `docs`, `chore` +3. 올바른 형식으로 다시 커밋 + +### 13.2 CI 실패 + +**문제**: CI에서 린트 또는 빌드 실패 + +**해결**: + +1. 로컬에서 동일한 명령어 실행 + ```bash + npm run lint:all + npm run build + ``` +2. 오류 수정 +3. 자동 수정 시도 + ```bash + npm run fix:all + ``` +4. 수정 후 다시 푸시 + +### 13.3 Git hooks가 동작하지 않음 + +**문제**: 커밋 시 commitlint가 실행되지 않음 + +**해결**: + +1. Husky 설치 확인 + ```bash + npm run prepare + ``` +2. `.husky/commit-msg` 파일 확인 +3. 실행 권한 확인 + ```bash + chmod +x .husky/commit-msg + ``` + +## 14. 후속 작업 + +### 14.1 단기 (1주 이내) + +- 팀원 대상 워크플로우 공유 +- 커밋 메시지 가이드 공유 +- CI/CD 프로세스 설명 + +### 14.2 중기 (1개월 이내) + +- 테스트 코드 추가 및 CI 통합 +- 추가 린트 규칙 도입 검토 +- 코드 커버리지 측정 도입 검토 +- Pre-commit 훅 활성화 검토 (필요시) + +### 14.3 장기 (3개월 이내) + +- ESLint 9.0+ Flat Config 마이그레이션 +- 자동화된 코드 리뷰 도구 도입 검토 +- 성능 모니터링 통합 + +## 15. 참고 자료 + +- [Husky Documentation](https://typicode.github.io/husky/) +- [Commitlint Documentation](https://commitlint.js.org/) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [Conventional Commits](https://www.conventionalcommits.org/) +- [Next.js ESLint Configuration](https://nextjs.org/docs/app/building-your-application/configuring/eslint) + +## 16. 승인 + +- [ ] 기술 리더 승인 +- [ ] 팀 리뷰 완료 +- [ ] 일정 확정 + +--- + +**작성일**: 2025-01-XX +**작성자**: 개발팀 +**버전**: 1.0 diff --git a/package-lock.json b/package-lock.json index 5cf22dcc..f16bb687 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,18 +20,15 @@ "@tanstack/react-query": "^5.84.1", "@tanstack/react-query-devtools": "^5.84.1", "@tanstack/react-virtual": "^3.13.12", - "@types/js-cookie": "^3.0.6", "axios": "^1.6.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "critters": "^0.0.23", - "firebase": "^10.7.2", "firebase-admin": "^12.0.0", - "js-cookie": "^3.0.5", "linkify-react": "^4.3.2", "linkifyjs": "^4.3.2", "lucide-react": "^0.479.0", "next": "^14.2.35", + "next-render-analyzer": "^0.1.2", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.60.0", @@ -42,6 +39,8 @@ "zustand": "^5.0.7" }, "devDependencies": { + "@commitlint/cli": "^20.2.0", + "@commitlint/config-conventional": "^20.2.0", "@svgr/webpack": "^8.1.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^20.11.19", @@ -49,10 +48,12 @@ "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "autoprefixer": "^10.4.20", + "critters": "^0.0.23", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-next": "^14.2.13", "eslint-config-prettier": "^9.1.0", + "husky": "^9.1.7", "postcss": "^8.4.45", "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.10", @@ -2064,701 +2065,752 @@ "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "node_modules/@commitlint/cli": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.2.0.tgz", + "integrity": "sha512-l37HkrPZ2DZy26rKiTUvdq/LZtlMcxz+PeLv9dzK9NzoFGuJdOQyYU7IEkEQj0pO++uYue89wzOpZ0hcTtoqUA==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@commitlint/format": "^20.2.0", + "@commitlint/lint": "^20.2.0", + "@commitlint/load": "^20.2.0", + "@commitlint/read": "^20.2.0", + "@commitlint/types": "^20.2.0", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" }, "engines": { - "node": ">=12" + "node": ">=v18" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "node_modules/@commitlint/config-conventional": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.2.0.tgz", + "integrity": "sha512-MsRac+yNIbTB4Q/psstKK4/ciVzACHicSwz+04Sxve+4DW+PiJeTjU0JnS4m/oOnulrXYN+yBPlKaBSGemRfgQ==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@commitlint/types": "^20.2.0", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" } }, - "node_modules/@edge-runtime/cookies": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@edge-runtime/cookies/-/cookies-3.4.1.tgz", - "integrity": "sha512-z27BvgPxI73CgSlxU/NAUf1Q/shnqi6cobHEowf6VuLdSjGR3NjI2Y5dZUIBbK2zOJVZbXcHsVzJjz8LklteFQ==", + "node_modules/@commitlint/config-validator": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.2.0.tgz", + "integrity": "sha512-SQCBGsL9MFk8utWNSthdxd9iOD1pIVZSHxGBwYIGfd67RTjxqzFOSAYeQVXOu3IxRC3YrTOH37ThnTLjUlyF2w==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.2.0", + "ajv": "^8.11.0" + }, "engines": { - "node": ">=16" + "node": ">=v18" } }, - "node_modules/@edge-runtime/format": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@edge-runtime/format/-/format-2.2.0.tgz", - "integrity": "sha512-gPrS6AVw/qJJL0vcxMXv4kFXCU3ZTCD1uuJpwX15YxHV8BgU9OG5v9LrkkXcr96PBT/9epypfNJMhlWADuEziw==", + "node_modules/@commitlint/config-validator/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=16" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@edge-runtime/node-utils": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@edge-runtime/node-utils/-/node-utils-2.2.1.tgz", - "integrity": "sha512-RUl/439BHKshkhSGFRlZ1kzy68wL4mn8VNKDSZr3p0tciyZ33Mjfpl+vofqnHqXRmDI6nLnZpfJvhY3D88o0pA==", + "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MPL-2.0", + "license": "MIT" + }, + "node_modules/@commitlint/ensure": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.2.0.tgz", + "integrity": "sha512-+8TgIGv89rOWyt3eC6lcR1H7hqChAKkpawytlq9P1i/HYugFRVqgoKJ8dhd89fMnlrQTLjA5E97/4sF09QwdoA==", + "dev": true, + "license": "MIT", "dependencies": { - "@edge-runtime/cookies": "3.4.1" + "@commitlint/types": "^20.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" }, "engines": { - "node": ">=16" + "node": ">=v18" } }, - "node_modules/@edge-runtime/ponyfill": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@edge-runtime/ponyfill/-/ponyfill-2.4.1.tgz", - "integrity": "sha512-ZbR/EViY3gg2rmEAQTKPa6mXl4aR1/+cFcQe4r1segCjEbTAxT6PWu40odbu/KlZKSysEb2O/BWIC2lJgSJOMQ==", + "node_modules/@commitlint/execute-rule": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", + "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=v18" } }, - "node_modules/@edge-runtime/primitives": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-4.0.5.tgz", - "integrity": "sha512-t7QiN5d/KpXgCvIfSt6Nm9Hj3WVdNgc5CpOD73jasY+9EvTI7Ngdj5cXvjcHrPcmYWJZMySPgeEeoL/1N/Llag==", + "node_modules/@commitlint/format": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.2.0.tgz", + "integrity": "sha512-PhNoLNhxpfIBlW/i90uZ3yG3hwSSYx7n4d9Yc+2FAorAHS0D9btYRK4ZZXX+Gm3W5tDtu911ow/eWRfcRVgNWg==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.2.0", + "chalk": "^5.3.0" + }, "engines": { - "node": ">=16" + "node": ">=v18" } }, - "node_modules/@edge-runtime/vm": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-3.1.7.tgz", - "integrity": "sha512-hUMFbDQ/nZN+1TLMi6iMO1QFz9RSV8yGG8S42WFPFma1d7VSNE0eMdJUmwjmtav22/iQkzHMmu6oTSfAvRGS8g==", + "node_modules/@commitlint/format/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@edge-runtime/primitives": "4.0.5" - }, + "license": "MIT", "engines": { - "node": ">=16" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@commitlint/is-ignored": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.2.0.tgz", + "integrity": "sha512-Lz0OGeZCo/QHUDLx5LmZc0EocwanneYJUM8z0bfWexArk62HKMLfLIodwXuKTO5y0s6ddXaTexrYHs7v96EOmw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "@commitlint/types": "^20.2.0", + "semver": "^7.6.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "engines": { + "node": ">=10" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "node_modules/@commitlint/lint": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.2.0.tgz", + "integrity": "sha512-cQEEB+jlmyQbyiji/kmh8pUJSDeUmPiWq23kFV0EtW3eM+uAaMLMuoTMajbrtWYWQpPzOMDjYltQ8jxHeHgITg==", "dev": true, "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^20.2.0", + "@commitlint/parse": "^20.2.0", + "@commitlint/rules": "^20.2.0", + "@commitlint/types": "^20.2.0" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=v18" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@commitlint/load": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.2.0.tgz", + "integrity": "sha512-iAK2GaBM8sPFTSwtagI67HrLKHIUxQc2BgpgNc/UMNme6LfmtHpIxQoN1TbP+X1iz58jq32HL1GbrFTCzcMi6g==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@commitlint/config-validator": "^20.2.0", + "@commitlint/execute-rule": "^20.0.0", + "@commitlint/resolve-extends": "^20.2.0", + "@commitlint/types": "^20.2.0", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=v18" + } + }, + "node_modules/@commitlint/load/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/@commitlint/load/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" }, "engines": { - "node": ">=8" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/@commitlint/load/node_modules/cosmiconfig-typescript-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", + "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "MIT", + "dependencies": { + "jiti": "^2.6.1" + }, "engines": { - "node": ">=10" + "node": ">=v18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@commitlint/load/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/@fastify/busboy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.0.0.tgz", - "integrity": "sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==", - "license": "MIT" + "node_modules/@commitlint/message": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.0.0.tgz", + "integrity": "sha512-gLX4YmKnZqSwkmSB9OckQUrI5VyXEYiv3J5JKZRxIp8jOQsWjZgHSG/OgEfMQBK9ibdclEdAyIPYggwXoFGXjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } }, - "node_modules/@firebase/analytics": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.8.tgz", - "integrity": "sha512-CVnHcS4iRJPqtIDc411+UmFldk0ShSK3OB+D0bKD8Ck5Vro6dbK5+APZpkuWpbfdL359DIQUnAaMLE+zs/PVyA==", - "license": "Apache-2.0", + "node_modules/@commitlint/parse": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.2.0.tgz", + "integrity": "sha512-LXStagGU1ivh07X7sM+hnEr4BvzFYn1iBJ6DRg2QsIN8lBfSzyvkUcVCDwok9Ia4PWiEgei5HQjju6xfJ1YaSQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/installations": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "@commitlint/types": "^20.2.0", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" }, - "peerDependencies": { - "@firebase/app": "0.x" + "engines": { + "node": ">=v18" } }, - "node_modules/@firebase/analytics-compat": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.14.tgz", - "integrity": "sha512-unRVY6SvRqfNFIAA/kwl4vK+lvQAL2HVcgu9zTrUtTyYDmtIt/lOuHJynBMYEgLnKm39YKBDhtqdapP2e++ASw==", - "license": "Apache-2.0", + "node_modules/@commitlint/read": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.2.0.tgz", + "integrity": "sha512-+SjF9mxm5JCbe+8grOpXCXMMRzAnE0WWijhhtasdrpJoAFJYd5UgRTj/oCq5W3HJTwbvTOsijEJ0SUGImECD7Q==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/analytics": "0.10.8", - "@firebase/analytics-types": "0.8.2", - "@firebase/component": "0.6.9", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "@commitlint/top-level": "^20.0.0", + "@commitlint/types": "^20.2.0", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^1.0.0" }, - "peerDependencies": { - "@firebase/app-compat": "0.x" + "engines": { + "node": ">=v18" } }, - "node_modules/@firebase/analytics-types": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.2.tgz", - "integrity": "sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.11.tgz", - "integrity": "sha512-DuI8c+p/ndPmV6V0i+mcSuaU9mK9Pi9h76WOYFkPNsbmkblEy8bpTOazjG7tnfar6Of1Wn5ohvyOHSRqnN6flQ==", - "license": "Apache-2.0", + "node_modules/@commitlint/resolve-extends": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.2.0.tgz", + "integrity": "sha512-KVoLDi9BEuqeq+G0wRABn4azLRiCC22/YHR2aCquwx6bzCHAIN8hMt3Nuf1VFxq/c8ai6s8qBxE8+ZD4HeFTlQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "idb": "7.1.1", - "tslib": "^2.1.0" + "@commitlint/config-validator": "^20.2.0", + "@commitlint/types": "^20.2.0", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" } }, - "node_modules/@firebase/app-check": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.8.tgz", - "integrity": "sha512-O49RGF1xj7k6BuhxGpHmqOW5hqBIAEbt2q6POW0lIywx7emYtzPDeQI+ryQpC4zbKX646SoVZ711TN1DBLNSOQ==", - "license": "Apache-2.0", + "node_modules/@commitlint/resolve-extends/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@commitlint/rules": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.2.0.tgz", + "integrity": "sha512-27rHGpeAjnYl/A+qUUiYDa7Yn1WIjof/dFJjYW4gA1Ug+LUGa1P0AexzGZ5NBxTbAlmDgaxSZkLLxtLVqtg8PQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "@commitlint/ensure": "^20.2.0", + "@commitlint/message": "^20.0.0", + "@commitlint/to-lines": "^20.0.0", + "@commitlint/types": "^20.2.0" }, - "peerDependencies": { - "@firebase/app": "0.x" + "engines": { + "node": ">=v18" } }, - "node_modules/@firebase/app-check-compat": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.15.tgz", - "integrity": "sha512-zFIvIFFNqDXpOT2huorz9cwf56VT3oJYRFjSFYdSbGYEJYEaXjLJbfC79lx/zjx4Fh+yuN8pry3TtvwaevrGbg==", - "license": "Apache-2.0", + "node_modules/@commitlint/to-lines": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", + "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.0.0.tgz", + "integrity": "sha512-drXaPSP2EcopukrUXvUXmsQMu3Ey/FuJDc/5oiW4heoCfoE5BdLQyuc7veGeE3aoQaTVqZnh4D5WTWe2vefYKg==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/app-check": "0.8.8", - "@firebase/app-check-types": "0.5.2", - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "find-up": "^7.0.0" }, - "peerDependencies": { - "@firebase/app-compat": "0.x" + "engines": { + "node": ">=v18" } }, - "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", - "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-check-types": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.2.tgz", - "integrity": "sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-compat": { - "version": "0.2.41", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.41.tgz", - "integrity": "sha512-ktJcObWKjlIWq31kXu6sHoqWlhQD5rx0a2F2ZC2JVuEE5A5f7F43VO1Z6lfeRZXMFZbGG/aqIfXqgsP3zD2JYg==", - "license": "Apache-2.0", + "node_modules/@commitlint/top-level/node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/app": "0.10.11", - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@firebase/app-types": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", - "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==", - "license": "Apache-2.0" + "node_modules/@commitlint/top-level/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@firebase/auth": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.9.tgz", - "integrity": "sha512-yLD5095kVgDw965jepMyUrIgDklD6qH/BZNHeKOgvu7pchOKNjVM+zQoOVYJIKWMWOWBq8IRNVU6NXzBbozaJg==", - "license": "Apache-2.0", + "node_modules/@commitlint/top-level/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0", - "undici": "6.19.7" + "yocto-queue": "^1.0.0" }, - "peerDependencies": { - "@firebase/app": "0.x", - "@react-native-async-storage/async-storage": "^1.18.1" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@firebase/auth-compat": { - "version": "0.5.14", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.14.tgz", - "integrity": "sha512-2eczCSqBl1KUPJacZlFpQayvpilg3dxXLy9cSMTKtQMTQSmondUtPI47P3ikH3bQAXhzKLOE+qVxJ3/IRtu9pw==", - "license": "Apache-2.0", + "node_modules/@commitlint/top-level/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/auth": "1.7.9", - "@firebase/auth-types": "0.12.2", - "@firebase/component": "0.6.9", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0", - "undici": "6.19.7" + "p-limit": "^4.0.0" }, - "peerDependencies": { - "@firebase/app-compat": "0.x" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@firebase/auth-interop-types": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", - "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==", - "license": "Apache-2.0" + "node_modules/@commitlint/top-level/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } }, - "node_modules/@firebase/auth-types": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.2.tgz", - "integrity": "sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" + "node_modules/@commitlint/top-level/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@firebase/component": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.9.tgz", - "integrity": "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==", - "license": "Apache-2.0", + "node_modules/@commitlint/types": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.2.0.tgz", + "integrity": "sha512-KTy0OqRDLR5y/zZMnizyx09z/rPlPC/zKhYgH8o/q6PuAjoQAKlRfY4zzv0M64yybQ//6//4H1n14pxaLZfUnA==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" } }, - "node_modules/@firebase/database": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.8.tgz", - "integrity": "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.2", - "@firebase/auth-interop-types": "0.2.3", - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" + "node_modules/@commitlint/types/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@firebase/database-compat": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.8.tgz", - "integrity": "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==", - "license": "Apache-2.0", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/database": "1.0.8", - "@firebase/database-types": "1.0.5", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@firebase/database-types": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.5.tgz", - "integrity": "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==", - "license": "Apache-2.0", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", "dependencies": { - "@firebase/app-types": "0.9.2", - "@firebase/util": "1.10.0" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@firebase/firestore": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.2.tgz", - "integrity": "sha512-WPkL/DEHuJg1PZPyHn81pNUhitG+7WkpLVdXmoYB23Za3eoM8VzuIn7zcD4Cji6wDCGA6eI1rvGYLtsXmE1OaQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "@firebase/webchannel-wrapper": "1.0.1", - "@grpc/grpc-js": "~1.9.0", - "@grpc/proto-loader": "^0.7.8", - "tslib": "^2.1.0", - "undici": "6.19.7" - }, + "node_modules/@edge-runtime/cookies": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@edge-runtime/cookies/-/cookies-3.4.1.tgz", + "integrity": "sha512-z27BvgPxI73CgSlxU/NAUf1Q/shnqi6cobHEowf6VuLdSjGR3NjI2Y5dZUIBbK2zOJVZbXcHsVzJjz8LklteFQ==", + "dev": true, + "license": "MPL-2.0", "engines": { - "node": ">=10.10.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" + "node": ">=16" } }, - "node_modules/@firebase/firestore-compat": { - "version": "0.3.37", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.37.tgz", - "integrity": "sha512-YwjJePx+m2OGnpKTGFTkcRXQZ+z0+8t7/zuwyOsTmKERobn0kekOv8VAQQmITcC+3du8Ul98O2a0vMH3xwt7jQ==", - "license": "Apache-2.0", + "node_modules/@edge-runtime/format": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/format/-/format-2.2.0.tgz", + "integrity": "sha512-gPrS6AVw/qJJL0vcxMXv4kFXCU3ZTCD1uuJpwX15YxHV8BgU9OG5v9LrkkXcr96PBT/9epypfNJMhlWADuEziw==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/node-utils": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@edge-runtime/node-utils/-/node-utils-2.2.1.tgz", + "integrity": "sha512-RUl/439BHKshkhSGFRlZ1kzy68wL4mn8VNKDSZr3p0tciyZ33Mjfpl+vofqnHqXRmDI6nLnZpfJvhY3D88o0pA==", + "dev": true, + "license": "MPL-2.0", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/firestore": "4.7.2", - "@firebase/firestore-types": "3.0.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "@edge-runtime/cookies": "3.4.1" }, - "peerDependencies": { - "@firebase/app-compat": "0.x" + "engines": { + "node": ">=16" } }, - "node_modules/@firebase/firestore-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.2.tgz", - "integrity": "sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" + "node_modules/@edge-runtime/ponyfill": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@edge-runtime/ponyfill/-/ponyfill-2.4.1.tgz", + "integrity": "sha512-ZbR/EViY3gg2rmEAQTKPa6mXl4aR1/+cFcQe4r1segCjEbTAxT6PWu40odbu/KlZKSysEb2O/BWIC2lJgSJOMQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" } }, - "node_modules/@firebase/functions": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.8.tgz", - "integrity": "sha512-Lo2rTPDn96naFIlSZKVd1yvRRqqqwiJk7cf9TZhUerwnPKgBzXy+aHE22ry+6EjCaQusUoNai6mU6p+G8QZT1g==", - "license": "Apache-2.0", + "node_modules/@edge-runtime/primitives": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-4.0.5.tgz", + "integrity": "sha512-t7QiN5d/KpXgCvIfSt6Nm9Hj3WVdNgc5CpOD73jasY+9EvTI7Ngdj5cXvjcHrPcmYWJZMySPgeEeoL/1N/Llag==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/vm": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-3.1.7.tgz", + "integrity": "sha512-hUMFbDQ/nZN+1TLMi6iMO1QFz9RSV8yGG8S42WFPFma1d7VSNE0eMdJUmwjmtav22/iQkzHMmu6oTSfAvRGS8g==", + "dev": true, + "license": "MPL-2.0", "dependencies": { - "@firebase/app-check-interop-types": "0.3.2", - "@firebase/auth-interop-types": "0.2.3", - "@firebase/component": "0.6.9", - "@firebase/messaging-interop-types": "0.2.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0", - "undici": "6.19.7" + "@edge-runtime/primitives": "4.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { - "@firebase/app": "0.x" + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@firebase/functions-compat": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.14.tgz", - "integrity": "sha512-dZ0PKOKQFnOlMfcim39XzaXonSuPPAVuzpqA4ONTIdyaJK/OnBaIEVs/+BH4faa1a2tLeR+Jy15PKqDRQoNIJw==", - "license": "Apache-2.0", + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/functions": "0.11.8", - "@firebase/functions-types": "0.6.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "@firebase/app-compat": "0.x" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@firebase/functions-types": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.2.tgz", - "integrity": "sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/installations": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.9.tgz", - "integrity": "sha512-hlT7AwCiKghOX3XizLxXOsTFiFCQnp/oj86zp1UxwDGmyzsyoxtX+UIZyVyH/oBF5+XtblFG9KZzZQ/h+dpy+Q==", - "license": "Apache-2.0", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/util": "1.10.0", - "idb": "7.1.1", - "tslib": "^2.1.0" + "type-fest": "^0.20.2" }, - "peerDependencies": { - "@firebase/app": "0.x" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@firebase/installations-compat": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.9.tgz", - "integrity": "sha512-2lfdc6kPXR7WaL4FCQSQUhXcPbI7ol3wF+vkgtU25r77OxPf8F/VmswQ7sgIkBBWtymn5ZF20TIKtnOj9rjb6w==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/installations": "0.6.9", - "@firebase/installations-types": "0.5.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" }, - "peerDependencies": { - "@firebase/app-compat": "0.x" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@firebase/installations-types": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.2.tgz", - "integrity": "sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x" + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@firebase/logger": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", - "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } + "node_modules/@fastify/busboy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.0.0.tgz", + "integrity": "sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==", + "license": "MIT" }, - "node_modules/@firebase/messaging": { - "version": "0.12.11", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.11.tgz", - "integrity": "sha512-zn5zGhF46BmiZ7W9yAUoHlqzJGakmWn1FNp//roXHN62dgdEFIKfXY7IODA2iQiXpmUO3sBdI/Tf+Hsft1mVkw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/installations": "0.6.9", - "@firebase/messaging-interop-types": "0.2.2", - "@firebase/util": "1.10.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==", + "license": "Apache-2.0" }, - "node_modules/@firebase/messaging-compat": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.11.tgz", - "integrity": "sha512-2NCkfE1L9jSn5OC+2n5rGAz5BEAQreK2lQGdPYQEJlAbKB2efoF+2FdiQ+LD8SlioSXz66REfeaEdesoLPFQcw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/messaging": "0.12.11", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==", + "license": "Apache-2.0" }, - "node_modules/@firebase/messaging-interop-types": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.2.tgz", - "integrity": "sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA==", + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==", "license": "Apache-2.0" }, - "node_modules/@firebase/performance": { + "node_modules/@firebase/component": { "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.9.tgz", - "integrity": "sha512-PnVaak5sqfz5ivhua+HserxTJHtCar/7zM0flCX6NkzBNzJzyzlH4Hs94h2Il0LQB99roBqoE5QT1JqWqcLJHQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/installations": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/performance-compat": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.9.tgz", - "integrity": "sha512-dNl95IUnpsu3fAfYBZDCVhXNkASE0uo4HYaEPd2/PKscfTvsgqFAOxfAXzBEDOnynDWiaGUnb5M1O00JQ+3FXA==", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.9.tgz", + "integrity": "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/performance": "0.6.9", - "@firebase/performance-types": "0.2.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" } }, - "node_modules/@firebase/performance-types": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.2.tgz", - "integrity": "sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/remote-config": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.9.tgz", - "integrity": "sha512-EO1NLCWSPMHdDSRGwZ73kxEEcTopAxX1naqLJFNApp4hO8WfKfmEpmjxmP5TrrnypjIf2tUkYaKsfbEA7+AMmA==", + "node_modules/@firebase/database": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.8.tgz", + "integrity": "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==", "license": "Apache-2.0", "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", "@firebase/component": "0.6.9", - "@firebase/installations": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", + "faye-websocket": "0.11.4", "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" } }, - "node_modules/@firebase/remote-config-compat": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.9.tgz", - "integrity": "sha512-AxzGpWfWFYejH2twxfdOJt5Cfh/ATHONegTd/a0p5flEzsD5JsxXgfkFToop+mypEL3gNwawxrxlZddmDoNxyA==", + "node_modules/@firebase/database-compat": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.8.tgz", + "integrity": "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.9", + "@firebase/database": "1.0.8", + "@firebase/database-types": "1.0.5", "@firebase/logger": "0.4.2", - "@firebase/remote-config": "0.4.9", - "@firebase/remote-config-types": "0.3.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" } }, - "node_modules/@firebase/remote-config-types": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.2.tgz", - "integrity": "sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/storage": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.2.tgz", - "integrity": "sha512-fxuJnHshbhVwuJ4FuISLu+/76Aby2sh+44ztjF2ppoe0TELIDxPW6/r1KGlWYt//AD0IodDYYA8ZTN89q8YqUw==", + "node_modules/@firebase/database-types": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.5.tgz", + "integrity": "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0", - "undici": "6.19.7" - }, - "peerDependencies": { - "@firebase/app": "0.x" + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.10.0" } }, - "node_modules/@firebase/storage-compat": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.12.tgz", - "integrity": "sha512-hA4VWKyGU5bWOll+uwzzhEMMYGu9PlKQc1w4DWxB3aIErWYzonrZjF0icqNQZbwKNIdh8SHjZlFeB2w6OSsjfg==", + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/storage": "0.13.2", - "@firebase/storage-types": "0.8.2", - "@firebase/util": "1.10.0", "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/storage-types": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.2.tgz", - "integrity": "sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" } }, "node_modules/@firebase/util": { @@ -2770,32 +2822,6 @@ "tslib": "^2.1.0" } }, - "node_modules/@firebase/vertexai-preview": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@firebase/vertexai-preview/-/vertexai-preview-0.0.4.tgz", - "integrity": "sha512-EBSqyu9eg8frQlVU9/HjKtHN7odqbh9MtAcVz3WwHj4gLCLOoN9F/o+oxlq3CxvFrd3CNTZwu6d2mZtVlEInng==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.2", - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" - } - }, - "node_modules/@firebase/webchannel-wrapper": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.1.tgz", - "integrity": "sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ==", - "license": "Apache-2.0" - }, "node_modules/@floating-ui/core": { "version": "1.6.9", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", @@ -2941,24 +2967,12 @@ "supercluster": "^8.0.1" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.9.15", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", - "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.7.8", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, "node_modules/@grpc/proto-loader": { "version": "0.7.13", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", "license": "Apache-2.0", + "optional": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -4141,31 +4155,36 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "license": "BSD-3-Clause", + "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -4175,31 +4194,36 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@radix-ui/number": { "version": "1.1.0", @@ -6341,6 +6365,16 @@ "@types/node": "*" } }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -6405,12 +6439,6 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "license": "MIT" }, - "node_modules/@types/js-cookie": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", - "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -7744,6 +7772,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, "node_modules/array-includes": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", @@ -8147,6 +8182,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { @@ -8338,6 +8374,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -8431,6 +8468,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -8513,6 +8551,17 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "license": "MIT" }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8527,21 +8576,66 @@ "dev": true, "license": "MIT" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", "dev": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", "dev": true, "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, "engines": { - "node": ">= 0.6" + "node": ">=16" } }, "node_modules/convert-hrtime": { @@ -8612,6 +8706,7 @@ "version": "0.0.23", "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.23.tgz", "integrity": "sha512-/MCsQbuzTPA/ZTOjjyr2Na5o3lRpr8vd0MZE8tMP0OBNg/VrLxWHteVKalQ8KR+fBmUadbJLdoyEz9sT+q84qg==", + "dev": true, "license": "Apache-2.0", "dependencies": { "chalk": "^4.1.0", @@ -8641,6 +8736,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -8671,6 +8767,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -8741,6 +8838,19 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -8979,6 +9089,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -8993,6 +9104,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, "funding": [ { "type": "github", @@ -9005,6 +9117,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -9020,6 +9133,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -9041,6 +9155,19 @@ "tslib": "^2.0.3" } }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -9175,6 +9302,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -9183,6 +9311,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -10461,8 +10599,7 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { "version": "4.5.0", @@ -10566,41 +10703,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/firebase": { - "version": "10.13.2", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.13.2.tgz", - "integrity": "sha512-YeI+TO5rJsoyZsVFx9WiN5ibdVCIigYTWwldRTMfCzrSPrJFVGao4acYj3x0EYGKDIgSgEyVBayD5BffD4Eyow==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/analytics": "0.10.8", - "@firebase/analytics-compat": "0.2.14", - "@firebase/app": "0.10.11", - "@firebase/app-check": "0.8.8", - "@firebase/app-check-compat": "0.3.15", - "@firebase/app-compat": "0.2.41", - "@firebase/app-types": "0.9.2", - "@firebase/auth": "1.7.9", - "@firebase/auth-compat": "0.5.14", - "@firebase/database": "1.0.8", - "@firebase/database-compat": "1.0.8", - "@firebase/firestore": "4.7.2", - "@firebase/firestore-compat": "0.3.37", - "@firebase/functions": "0.11.8", - "@firebase/functions-compat": "0.3.14", - "@firebase/installations": "0.6.9", - "@firebase/installations-compat": "0.2.9", - "@firebase/messaging": "0.12.11", - "@firebase/messaging-compat": "0.2.11", - "@firebase/performance": "0.6.9", - "@firebase/performance-compat": "0.2.9", - "@firebase/remote-config": "0.4.9", - "@firebase/remote-config-compat": "0.2.9", - "@firebase/storage": "0.13.2", - "@firebase/storage-compat": "0.3.12", - "@firebase/util": "1.10.0", - "@firebase/vertexai-preview": "0.0.4" - } - }, "node_modules/firebase-admin": { "version": "12.5.0", "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.5.0.tgz", @@ -10955,6 +11057,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -11053,6 +11156,24 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -11125,6 +11246,22 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -11381,6 +11518,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -11461,6 +11599,22 @@ "node": ">=8.12.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -11474,12 +11628,6 @@ "node": ">=0.10.0" } }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "license": "ISC" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -11519,6 +11667,17 @@ "module-details-from-path": "^1.0.3" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -11547,6 +11706,16 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -11863,6 +12032,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -11973,6 +12152,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", @@ -12130,15 +12322,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12240,6 +12423,33 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -12477,6 +12687,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "devOptional": true, "license": "MIT" }, "node_modules/lodash.clonedeep": { @@ -12528,6 +12739,13 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -12535,17 +12753,53 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true }, "node_modules/loose-envify": { "version": "1.4.0", @@ -12663,6 +12917,19 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -12932,6 +13199,23 @@ } } }, + "node_modules/next-render-analyzer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/next-render-analyzer/-/next-render-analyzer-0.1.2.tgz", + "integrity": "sha512-irHP+fV4v+xggRSuZZcbTDxTJoZd/881vyTsuvJi5/ctVz2hpXwBJaJF7AdyP03loPPyIh42ie140+ZC/fG/5g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0" + }, + "bin": { + "next-render-analyzer": "dist/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -13084,6 +13368,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -13694,6 +13979,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, "license": "MIT" }, "node_modules/postcss-nested": { @@ -13948,6 +14234,7 @@ "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, "license": "BSD-3-Clause", + "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -14329,6 +14616,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14865,6 +15153,16 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stacktrace-parser": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", @@ -15265,6 +15563,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -15498,6 +15797,19 @@ "license": "MIT", "peer": true }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -15532,6 +15844,13 @@ "integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==", "license": "ISC" }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, "node_modules/time-span": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/time-span/-/time-span-4.0.0.tgz", @@ -15548,6 +15867,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -15852,15 +16181,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici": { - "version": "6.19.7", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.7.tgz", - "integrity": "sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -15911,6 +16231,19 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -16423,6 +16756,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -16500,6 +16834,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=10" @@ -16512,21 +16847,25 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -16545,6 +16884,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=12" diff --git a/package.json b/package.json index d332bd7d..909f6b19 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,14 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "typecheck": "tsc --noEmit", + "lint:all": "npm run lint && npm run format:check && npm run typecheck", + "fix:all": "npm run lint:fix && npm run format", + "prepare": "husky" }, "dependencies": { "@hookform/resolvers": "^5.1.1", @@ -24,18 +31,15 @@ "@tanstack/react-query": "^5.84.1", "@tanstack/react-query-devtools": "^5.84.1", "@tanstack/react-virtual": "^3.13.12", - "@types/js-cookie": "^3.0.6", "axios": "^1.6.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "critters": "^0.0.23", - "firebase": "^10.7.2", "firebase-admin": "^12.0.0", - "js-cookie": "^3.0.5", "linkify-react": "^4.3.2", "linkifyjs": "^4.3.2", "lucide-react": "^0.479.0", "next": "^14.2.35", + "next-render-analyzer": "^0.1.2", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.60.0", @@ -46,6 +50,8 @@ "zustand": "^5.0.7" }, "devDependencies": { + "@commitlint/cli": "^20.2.0", + "@commitlint/config-conventional": "^20.2.0", "@svgr/webpack": "^8.1.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^20.11.19", @@ -53,10 +59,12 @@ "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "autoprefixer": "^10.4.20", + "critters": "^0.0.23", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-next": "^14.2.13", "eslint-config-prettier": "^9.1.0", + "husky": "^9.1.7", "postcss": "^8.4.45", "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.10", diff --git a/sentry.client.config.ts b/sentry.client.config.ts index eeaeb9e6..24182437 100644 --- a/sentry.client.config.ts +++ b/sentry.client.config.ts @@ -20,10 +20,12 @@ if (process.env.NODE_ENV === "production") { replaysSessionSampleRate: 0.1, // 일반 세션의 10% replaysOnErrorSampleRate: 1.0, // 에러 발생 시 100% + // tracePropagationTargets는 최상위 옵션으로 설정 + tracePropagationTargets: ["solid-connection.com", /^https:\/\/(www\.)?solid[\-]?connection\.com/], + integrations: [ // Browser Tracing: 페이지 로드 및 네비게이션 성능 측정 Sentry.browserTracingIntegration({ - tracePropagationTargets: ["solid-connection.com", /^https:\/\/(www\.)?solid[\-]?connection\.com/], // Web Vitals 자동 수집 활성화 enableInp: true, // Interaction to Next Paint (INP) 측정 }), diff --git a/src/api/applications/client/queryKeys.ts b/src/api/applications/client/queryKeys.ts deleted file mode 100644 index b57885b2..00000000 --- a/src/api/applications/client/queryKeys.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - competitorsApplicationList = "competitorsApplicationList", -} diff --git a/src/api/applications/client/useGetApplicationsList.ts b/src/api/applications/client/useGetApplicationsList.ts deleted file mode 100644 index 8dd1a649..00000000 --- a/src/api/applications/client/useGetApplicationsList.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKeys"; - -import { ApplicationListResponse } from "@/types/application"; - -// UseQueryResult는 useQuery의 반환 타입을 명시할 때 유용합니다. -import { UseQueryOptions, UseQueryResult, useQuery } from "@tanstack/react-query"; - -export const getCompetitorsApplicationList = (): Promise> => - axiosInstance.get("/applications"); - -// 커스텀 훅의 props 타입 정의를 개선하여 queryKey와 queryFn을 제외시킵니다. -type UseGetCompetitorsApplicationListOptions = Omit< - UseQueryOptions< - AxiosResponse, // queryFn이 반환하는 원본 데이터 타입 - AxiosError<{ message: string }>, // 에러 타입 - ApplicationListResponse // select를 통해 최종적으로 반환될 데이터 타입 - >, - "queryKey" | "queryFn" // 훅 내부에서 지정하므로 props에서는 제외 ->; - -const useGetApplicationsList = ( - props?: UseGetCompetitorsApplicationListOptions, -): UseQueryResult> => { - // 반환 타입 명시 - return useQuery({ - queryKey: [QueryKeys.competitorsApplicationList], - queryFn: getCompetitorsApplicationList, - staleTime: 1000 * 60 * 5, - select: (response) => response.data, - ...props, - }); -}; - -export default useGetApplicationsList; diff --git a/src/api/applications/client/useGetCompetitorsApplicationList.ts b/src/api/applications/client/useGetCompetitorsApplicationList.ts deleted file mode 100644 index 885fc055..00000000 --- a/src/api/applications/client/useGetCompetitorsApplicationList.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKeys"; - -import { ApplicationListResponse } from "@/types/application"; - -// UseQueryResult는 useQuery의 반환 타입을 명시할 때 유용합니다. -import { UseQueryOptions, UseQueryResult, useQuery } from "@tanstack/react-query"; - -export const getCompetitorsApplicationList = (): Promise> => - axiosInstance.get("/applications/competitors"); - -// 커스텀 훅의 props 타입 정의를 개선하여 queryKey와 queryFn을 제외시킵니다. -type UseGetCompetitorsApplicationListOptions = Omit< - UseQueryOptions< - AxiosResponse, // queryFn이 반환하는 원본 데이터 타입 - AxiosError<{ message: string }>, // 에러 타입 - ApplicationListResponse // select를 통해 최종적으로 반환될 데이터 타입 - >, - "queryKey" | "queryFn" // 훅 내부에서 지정하므로 props에서는 제외 ->; - -const useGetCompetitorsApplicationList = ( - props?: UseGetCompetitorsApplicationListOptions, -): UseQueryResult> => { - // 반환 타입 명시 - return useQuery({ - queryKey: [QueryKeys.competitorsApplicationList], - queryFn: getCompetitorsApplicationList, - staleTime: 1000 * 60 * 5, - select: (response) => response.data, - ...props, - }); -}; - -export default useGetCompetitorsApplicationList; diff --git a/src/api/applications/client/usePostSubmitApplication.ts b/src/api/applications/client/usePostSubmitApplication.ts deleted file mode 100644 index 360ecf86..00000000 --- a/src/api/applications/client/usePostSubmitApplication.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useRouter } from "next/navigation"; - -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { UseMutationOptions, UseMutationResult, useMutation } from "@tanstack/react-query"; - -// API 함수 경로 -export interface UseSubmitApplicationResponse { - isSuccess: boolean; -} - -export interface UseSubmitApplicationRequest { - gpaScoreId: number; - languageTestScoreId: number; - universityChoiceRequest: { - firstChoiceUniversityId: number | null; - secondChoiceUniversityId: number | null; - thirdChoiceUniversityId: number | null; - }; -} - -export const postSubmitApplication = ( - request: UseSubmitApplicationRequest, -): Promise> => axiosInstance.post("/applications", request); - -const usePostSubmitApplication = ( - props?: UseMutationOptions< - AxiosResponse, // TData - AxiosError<{ message: string }>, // TError - UseSubmitApplicationRequest, // TVariables - unknown // TContext - >, -): UseMutationResult< - AxiosResponse, - AxiosError<{ message: string }>, - UseSubmitApplicationRequest, - unknown -> => { - return useMutation< - AxiosResponse, // TData: 성공 시 반환 타입 - AxiosError<{ message: string }>, // TError: 에러 타입 - UseSubmitApplicationRequest // TVariables: 요청 body 타입 - >({ - ...props, - // mutationFn: API 요청을 수행할 비동기 함수를 지정합니다. - mutationFn: (request: UseSubmitApplicationRequest) => postSubmitApplication(request), - - // onError: API 요청이 실패했을 때 실행할 콜백 함수입니다. - onError: (error) => { - const errorMessage = error?.response?.data?.message; - toast.error(errorMessage || "지원 중 오류가 발생했습니다. 다시 시도해주세요."); - }, - }); -}; - -export default usePostSubmitApplication; diff --git a/src/api/auth/client/usePostEmailSignUp.ts b/src/api/auth/client/usePostEmailSignUp.ts deleted file mode 100644 index 58e8b905..00000000 --- a/src/api/auth/client/usePostEmailSignUp.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { EmailSignUpRequest, EmailSignUpResponse } from "@/types/auth"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -/** - * @description 이메일 회원가입 API 함수 - * @param request - 이메일 회원가입 요청 데이터 - * @returns Promise - */ -const emailSignUp = async (request: EmailSignUpRequest): Promise => { - const response: AxiosResponse = await publicAxiosInstance.post("/auth/email/sign-up", request); - return response.data; -}; - -/** - * @description 이메일 회원가입을 위한 useMutation 커스텀 훅 - */ -const usePostEmailSignUp = () => { - return useMutation({ - mutationFn: emailSignUp, - onError: (error) => { - console.error("이메일 회원가입 실패:", error); - toast.error("회원가입에 실패했습니다."); - }, - }); -}; - -export default usePostEmailSignUp; diff --git a/src/api/auth/client/usePostSignUp.ts b/src/api/auth/client/usePostSignUp.ts deleted file mode 100644 index e39b5b1c..00000000 --- a/src/api/auth/client/usePostSignUp.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { SignUpRequest, SignUpResponse } from "@/types/auth"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -/** - * @description 회원가입 API 함수 - * @param request - 회원가입 요청 데이터 - * @returns Promise - */ -const signUp = async (signUpRequest: SignUpRequest): Promise => { - // 임시 성별, 생년월일 추가. API 변경 시 삭제 - const payload = { - ...signUpRequest, - birth: "2000-01-01", - gender: "PREFER_NOT_TO_SAY", - }; - - const response: AxiosResponse = await publicAxiosInstance.post("/auth/sign-up", payload); - return response.data; -}; - -/** - * @description 회원가입을 위한 useMutation 커스텀 훅 - */ -const usePostSignUp = () => { - return useMutation({ - mutationFn: signUp, - onError: (error) => { - console.error("회원가입 실패:", error); - toast.error("회원가입에 실패했습니다."); - }, - }); -}; - -export default usePostSignUp; diff --git a/src/api/auth/useLogin.ts b/src/api/auth/useLogin.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/api/boards/clients/QueryKeys.ts b/src/api/boards/clients/QueryKeys.ts deleted file mode 100644 index 87ea7028..00000000 --- a/src/api/boards/clients/QueryKeys.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - postList = "postList1", -} diff --git a/src/api/boards/clients/useGetPostList.ts b/src/api/boards/clients/useGetPostList.ts deleted file mode 100644 index 987782d0..00000000 --- a/src/api/boards/clients/useGetPostList.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./QueryKeys"; - -import { ListPost } from "@/types/community"; - -import { useQuery } from "@tanstack/react-query"; - -interface UseGetPostListProps { - boardCode: string; - category?: string | null; -} - -const getPostList = (boardCode: string, category: string | null = null): Promise> => { - // "전체"는 필터 없음을 의미하므로 파라미터에 포함하지 않음 - const params = category && category !== "전체" ? { category } : {}; - - return publicAxiosInstance.get(`/boards/${boardCode}`, { params }); -}; - -const useGetPostList = ({ boardCode, category = null }: UseGetPostListProps) => { - return useQuery({ - queryKey: [QueryKeys.postList, boardCode, category], - queryFn: () => getPostList(boardCode, category), - // HydrationBoundary로부터 자동으로 hydrate된 데이터 사용 - // staleTime을 무한으로 설정하여 불필요한 자동 refetch를 방지합니다. - staleTime: Infinity, - gcTime: 1000 * 60 * 30, // 예: 30분 - select: (response) => { - return [...response.data].sort((a, b) => { - return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); - }); - }, - }); -}; - -export default useGetPostList; diff --git a/src/api/chat/clients/queryKey.ts b/src/api/chat/clients/queryKey.ts deleted file mode 100644 index 526b36c3..00000000 --- a/src/api/chat/clients/queryKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum QueryKeys { - chatRooms = "chatRooms", - chatHistories = "chatHistories", - partnerInfo = "partnerInfo", -} diff --git a/src/api/chat/clients/useGetChatRooms.ts b/src/api/chat/clients/useGetChatRooms.ts deleted file mode 100644 index fbc73524..00000000 --- a/src/api/chat/clients/useGetChatRooms.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ChatRoom } from "@/types/chat"; - -import { useQuery } from "@tanstack/react-query"; - -export interface ChatRoomListResponse { - chatRooms: ChatRoom[]; -} - -const getChatRooms = async () => { - const res = await axiosInstance.get("/chats/rooms"); - return res.data; -}; -const useGetChatRooms = () => { - return useQuery({ - queryKey: [QueryKeys.chatRooms], - queryFn: getChatRooms, - staleTime: 1000 * 60 * 5, // 5분간 캐시 - select: (data) => data.chatRooms, - }); -}; -export default useGetChatRooms; diff --git a/src/api/chat/clients/useGetPartnerInfo.ts b/src/api/chat/clients/useGetPartnerInfo.ts deleted file mode 100644 index b926991c..00000000 --- a/src/api/chat/clients/useGetPartnerInfo.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ChatPartner } from "@/types/chat"; - -import { useQuery } from "@tanstack/react-query"; - -type ChatRoomListResponse = ChatPartner; - -const getPartnerInfo = async (roomId: number): Promise => { - const res = await axiosInstance.get(`/chats/rooms/${roomId}/partner`); - return res.data; -}; -const useGetPartnerInfo = (roomId: number) => { - return useQuery({ - queryKey: [QueryKeys.partnerInfo, roomId], - queryFn: () => getPartnerInfo(roomId), - staleTime: 1000 * 60 * 5, - }); -}; - -export default useGetPartnerInfo; diff --git a/src/api/chat/clients/usePutChatRead.ts b/src/api/chat/clients/usePutChatRead.ts deleted file mode 100644 index 57ed5183..00000000 --- a/src/api/chat/clients/usePutChatRead.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -const putChatRead = async (roomId: number): Promise => { - const response: AxiosResponse = await axiosInstance.put(`/chats/rooms/${roomId}/read`); - return response.data; -}; - -const usePutChatRead = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: putChatRead, - onSuccess: () => { - // 아티클 목록 쿼리를 무효화하여 새로 고침 - queryClient.invalidateQueries({ queryKey: [QueryKeys.chatRooms] }); - }, - onError: (error) => { - console.error("채팅방 진입 읽기 실패", error); - }, - }); -}; - -export default usePutChatRead; diff --git a/src/api/community/client/queryKey.ts b/src/api/community/client/queryKey.ts deleted file mode 100644 index 2e791de8..00000000 --- a/src/api/community/client/queryKey.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - posts = "posts", -} diff --git a/src/api/community/client/useCreateComment.ts b/src/api/community/client/useCreateComment.ts deleted file mode 100644 index 970dbf6f..00000000 --- a/src/api/community/client/useCreateComment.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { CommentCreateRequest, CommentIdResponse } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 댓글 생성 API 함수 - * @param request - 댓글 생성 요청 데이터 - * @returns Promise - */ -const createComment = async (request: CommentCreateRequest): Promise => { - const response: AxiosResponse = await axiosInstance.post(`/comments`, request); - return response.data; -}; - -/** - * @description 댓글 생성을 위한 useMutation 커스텀 훅 - */ -const useCreateComment = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: createComment, - onSuccess: (data, variables) => { - // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, variables.postId] }); - toast.success("댓글이 등록되었습니다."); - }, - onError: (error) => { - console.error("댓글 생성 실패:", error); - toast.error("댓글 등록에 실패했습니다."); - }, - }); -}; - -export default useCreateComment; diff --git a/src/api/community/client/useDeleteComment.ts b/src/api/community/client/useDeleteComment.ts deleted file mode 100644 index 5f7b69e8..00000000 --- a/src/api/community/client/useDeleteComment.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { CommentIdResponse } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface DeleteCommentRequest { - commentId: number; - postId: number; -} - -/** - * @description 댓글 삭제 API 함수 - * @param commentId - 삭제할 댓글의 ID - * @returns Promise - */ -const deleteComment = async (commentId: number): Promise => { - const response: AxiosResponse = await axiosInstance.delete(`/comments/${commentId}`); - return response.data; -}; - -/** - * @description 댓글 삭제를 위한 useMutation 커스텀 훅 - */ -const useDeleteComment = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ commentId }: DeleteCommentRequest) => deleteComment(commentId), - onSuccess: (data, variables) => { - // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, variables.postId] }); - toast.success("댓글이 삭제되었습니다."); - }, - onError: (error) => { - console.error("댓글 삭제 실패:", error); - toast.error("댓글 삭제에 실패했습니다."); - }, - }); -}; - -export default useDeleteComment; diff --git a/src/api/community/client/useDeleteLike.ts b/src/api/community/client/useDeleteLike.ts deleted file mode 100644 index 15ff0d87..00000000 --- a/src/api/community/client/useDeleteLike.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { PostLikeResponse } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 게시글 좋아요 취소 API 함수 - * @param postId - 좋아요를 취소할 게시글의 ID - * @returns Promise - */ -const deleteLike = async (postId: number): Promise => { - const response: AxiosResponse = await axiosInstance.delete(`/posts/${postId}/like`); - return response.data; -}; - -/** - * @description 게시글 좋아요 취소를 위한 useMutation 커스텀 훅 - */ -const useDeleteLike = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: deleteLike, - onSuccess: (data, postId) => { - // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, postId] }); - }, - onError: (error) => { - console.error("게시글 좋아요 취소 실패:", error); - toast.error("좋아요 취소 처리에 실패했습니다."); - }, - }); -}; - -export default useDeleteLike; diff --git a/src/api/community/client/useDeletePost.ts b/src/api/community/client/useDeletePost.ts deleted file mode 100644 index cbc93387..00000000 --- a/src/api/community/client/useDeletePost.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useRouter } from "next/navigation"; - -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "@/lib/zustand/useToastStore"; - -/** - * @description 게시글 삭제 API 응답 타입 - * @property {string} message - 성공 메시지 - * @property {number} postId - 삭제된 게시글 ID - */ -interface DeletePostResponse { - message: string; - postId: number; -} - -/** - * @description postId를 받아 해당 게시글을 삭제하는 API 함수 - * @param postId - 삭제할 게시글의 ID - * @returns Promise> - */ -export const deletePostApi = (postId: number): Promise> => { - return axiosInstance.delete(`/posts/${postId}`); -}; - -/** - * @description 게시글 삭제를 위한 useMutation 커스텀 훅 - */ -const useDeletePost = () => { - const router = useRouter(); - const queryClient = useQueryClient(); - - return useMutation({ - // mutation 실행 시 호출될 함수 - mutationFn: deletePostApi, - - // mutation 성공 시 실행될 콜백 - onSuccess: () => { - // 'posts' 쿼리 키를 가진 모든 쿼리를 무효화하여 - // 게시글 목록을 다시 불러오도록 합니다. - // ['posts', 'list'] 등 구체적인 키를 사용하셔도 좋습니다. - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts] }); - - toast.success("게시글이 성공적으로 삭제되었습니다."); - - // 게시글 목록 페이지 이동 - router.replace("/community/FREE"); - }, - - // mutation 실패 시 실행될 콜백 - onError: (error) => { - console.error("게시글 삭제 실패:", error); - toast.error("게시글 삭제에 실패했습니다. 잠시 후 다시 시도해주세요."); - }, - }); -}; - -export default useDeletePost; diff --git a/src/api/community/client/useGetPostDetail.ts b/src/api/community/client/useGetPostDetail.ts deleted file mode 100644 index 28abbc67..00000000 --- a/src/api/community/client/useGetPostDetail.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { Post } from "@/types/community"; - -import { useQuery } from "@tanstack/react-query"; - -/** - * @description 게시글 상세 조회 API 함수 - * @param postId - 조회할 게시글의 ID - * @returns Promise - */ -const getPostDetail = async (postId: number): Promise => { - const response: AxiosResponse = await axiosInstance.get(`/posts/${postId}`); - return response.data; -}; - -/** - * @description 게시글 상세 조회를 위한 useQuery 커스텀 훅 - */ -const useGetPostDetail = (postId: number) => { - return useQuery({ - queryKey: [QueryKeys.posts, postId], - queryFn: () => getPostDetail(postId), - enabled: !!postId, - }); -}; - -export default useGetPostDetail; diff --git a/src/api/community/client/usePostLike.ts b/src/api/community/client/usePostLike.ts deleted file mode 100644 index 4ff99d8b..00000000 --- a/src/api/community/client/usePostLike.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { PostLikeResponse } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 게시글 좋아요 API 함수 - * @param postId - 좋아요할 게시글의 ID - * @returns Promise - */ -const postLike = async (postId: number): Promise => { - const response: AxiosResponse = await axiosInstance.post(`/posts/${postId}/like`); - return response.data; -}; - -/** - * @description 게시글 좋아요를 위한 useMutation 커스텀 훅 - */ -const usePostLike = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: postLike, - onSuccess: (data, postId) => { - // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, postId] }); - }, - onError: (error) => { - console.error("게시글 좋아요 실패:", error); - toast.error("좋아요 처리에 실패했습니다."); - }, - }); -}; - -export default usePostLike; diff --git a/src/api/community/client/useUpdatePost.ts b/src/api/community/client/useUpdatePost.ts deleted file mode 100644 index f63880b8..00000000 --- a/src/api/community/client/useUpdatePost.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { PostIdResponse, PostUpdateRequest } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface UpdatePostRequest { - postId: number; - data: PostUpdateRequest; -} - -/** - * @description 게시글 수정 API 함수 - * @param postId - 수정할 게시글의 ID - * @param request - 게시글 수정 요청 데이터 - * @returns Promise - */ -const updatePost = async (postId: number, request: PostUpdateRequest): Promise => { - const convertedRequest: FormData = new FormData(); - convertedRequest.append( - "postUpdateRequest", - new Blob([JSON.stringify(request.postUpdateRequest)], { type: "application/json" }), - ); - request.file.forEach((file) => { - convertedRequest.append("file", file); - }); - - const response: AxiosResponse = await axiosInstance.patch(`/posts/${postId}`, convertedRequest, { - headers: { "Content-Type": "multipart/form-data" }, - }); - return response.data; -}; - -/** - * @description 게시글 수정을 위한 useMutation 커스텀 훅 - */ -const useUpdatePost = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ postId, data }: UpdatePostRequest) => updatePost(postId, data), - onSuccess: (result, variables) => { - // 해당 게시글 상세 쿼리와 목록 쿼리를 무효화 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, variables.postId] }); - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts] }); - toast.success("게시글이 수정되었습니다."); - }, - onError: (error) => { - console.error("게시글 수정 실패:", error); - toast.error("게시글 수정에 실패했습니다."); - }, - }); -}; - -export default useUpdatePost; diff --git a/src/api/file/client/useUploadProfileImagePublic.ts b/src/api/file/client/useUploadProfileImagePublic.ts deleted file mode 100644 index aa7cbb65..00000000 --- a/src/api/file/client/useUploadProfileImagePublic.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { FileResponse } from "@/types/file"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -/** - * @description 프로필 이미지 업로드 API 함수 (공개) - * @param file - 업로드할 파일 - * @returns Promise - */ -const uploadProfileImagePublic = async (file: File): Promise => { - const formData = new FormData(); - formData.append("file", file); - - const response: AxiosResponse = await publicAxiosInstance.post("/file/profile/pre", formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); - return response.data; -}; - -/** - * @description 프로필 이미지 업로드를 위한 useMutation 커스텀 훅 - */ -const useUploadProfileImagePublic = () => { - return useMutation({ - mutationFn: uploadProfileImagePublic, - onError: (error) => { - console.error("프로필 이미지 업로드 실패:", error); - toast.error("이미지 업로드에 실패했습니다."); - }, - }); -}; - -export default useUploadProfileImagePublic; diff --git a/src/api/mentee/client/queryKey.ts b/src/api/mentee/client/queryKey.ts deleted file mode 100644 index c57bd84a..00000000 --- a/src/api/mentee/client/queryKey.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - applyMentoringList = "applyMentoringList", -} diff --git a/src/api/mentee/client/useGetApplyMentoringList.ts b/src/api/mentee/client/useGetApplyMentoringList.ts deleted file mode 100644 index 153568c8..00000000 --- a/src/api/mentee/client/useGetApplyMentoringList.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { AxiosError } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentoringListItem } from "@/types/mentee"; -import { VerifyStatus } from "@/types/mentee"; - -import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import type { QueryFunctionContext } from "@tanstack/react-query"; - -interface UseGetApplyMentoringListResponse { - content: MentoringListItem[]; - nextPageNumber: number; -} -type UseGetApplyMentoringListRequest = VerifyStatus; - -const OFFSET = 3; // 기본 페이지 크기 - -const getApplyMentoringList = async ({ - queryKey, - pageParam, -}: QueryFunctionContext<[string, VerifyStatus], number>): Promise => { - const [, verifyStatus] = queryKey; - const res = await axiosInstance.get( - `/mentee/mentorings?verify-status=${verifyStatus}&size=${OFFSET}&page=${pageParam}`, - ); - return res.data; -}; - -const useGetApplyMentoringList = (verifyStatus: UseGetApplyMentoringListRequest) => { - return useInfiniteQuery< - UseGetApplyMentoringListResponse, - AxiosError, - MentoringListItem[], - [string, VerifyStatus], - number - >({ - queryKey: [QueryKeys.applyMentoringList, verifyStatus], - queryFn: getApplyMentoringList, - initialPageParam: 0, - getNextPageParam: (lastPage) => (lastPage.nextPageNumber === -1 ? undefined : lastPage.nextPageNumber), - staleTime: 1000 * 60 * 5, // 5분간 캐시 - select: (data) => data.pages.flatMap((p) => p.content), - }); -}; - -// 멘토링 리스트 프리페치용 훅 -export const usePrefetchApplyMentoringList = () => { - const queryClient = useQueryClient(); - - const prefetchMenteeMentoringList = (verifyStatus: UseGetApplyMentoringListRequest) => { - queryClient.prefetchInfiniteQuery({ - queryKey: [QueryKeys.applyMentoringList, verifyStatus], - queryFn: getApplyMentoringList, - initialPageParam: 0, - staleTime: 1000 * 60 * 5, - }); - }; - - return { prefetchMenteeMentoringList }; -}; - -export default useGetApplyMentoringList; diff --git a/src/api/mentee/client/usePatchMenteeCheckMentorings.ts b/src/api/mentee/client/usePatchMenteeCheckMentorings.ts deleted file mode 100644 index 537b2b63..00000000 --- a/src/api/mentee/client/usePatchMenteeCheckMentorings.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AxiosError } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { useMutation } from "@tanstack/react-query"; - -interface UsePatchMenteeCheckMentoringsRequest { - checkedMentoringIds: number[]; -} - -interface UsePatchMenteeCheckMentoringsResponse { - checkedMentoringIds: number[]; -} - -const patchMenteeCheckMentorings = async ( - body: UsePatchMenteeCheckMentoringsRequest, -): Promise => { - const res = await axiosInstance.patch("/mentee/mentorings/check", body); - return res.data; -}; - -const usePatchMenteeCheckMentorings = () => - useMutation({ - mutationFn: patchMenteeCheckMentorings, - }); - -export default usePatchMenteeCheckMentorings; diff --git a/src/api/mentee/client/usePostApplyMentoring.ts b/src/api/mentee/client/usePostApplyMentoring.ts deleted file mode 100644 index e4e2c28b..00000000 --- a/src/api/mentee/client/usePostApplyMentoring.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface UsePostApplyMentoringRequest { - mentorId: number; -} -interface UsePostApplyMentoringResponse { - mentoringId: number; -} - -const postApplyMentoring = async (body: UsePostApplyMentoringRequest): Promise => { - const res = await axiosInstance.post("/mentee/mentorings", body); - return res.data; -}; - -const usePostApplyMentoring = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: postApplyMentoring, - onSuccess: async () => { - // 멘토링 신청 후 멘토 목록을 새로고침 - await queryClient.invalidateQueries({ queryKey: [QueryKeys.applyMentoringList] }); - }, - onError: () => { - toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); - }, - }); -}; - -export default usePostApplyMentoring; diff --git a/src/api/mentor/client/queryKey.ts b/src/api/mentor/client/queryKey.ts deleted file mode 100644 index 345ca144..00000000 --- a/src/api/mentor/client/queryKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum QueryKeys { - mentoringNewCount = "mentoringNewCount", - mentoringList = "mentoringList", - myMentorProfile = "myMentorProfile", -} diff --git a/src/api/mentor/client/useGetMentorMyProfile.ts b/src/api/mentor/client/useGetMentorMyProfile.ts deleted file mode 100644 index b8598b02..00000000 --- a/src/api/mentor/client/useGetMentorMyProfile.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentorCardPreview } from "@/types/mentor"; - -// 학업 학기 (예: "2026-1") -import { useQuery } from "@tanstack/react-query"; - -type UseGetMyMentorProfileResponse = MentorCardPreview; - -const getMentorMyProfile = async (): Promise => { - const res = await axiosInstance.get("/mentor/my"); - return res.data; -}; - -const useGetMentorMyProfile = () => { - return useQuery({ - queryKey: [QueryKeys.myMentorProfile], - queryFn: getMentorMyProfile, - staleTime: 1000 * 60 * 5, // 5분간 캐시 - }); -}; - -export default useGetMentorMyProfile; diff --git a/src/api/mentor/client/useGetMentoringList.ts b/src/api/mentor/client/useGetMentoringList.ts deleted file mode 100644 index 88024b75..00000000 --- a/src/api/mentor/client/useGetMentoringList.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentoringItem } from "@/types/mentor"; - -import { useInfiniteQuery } from "@tanstack/react-query"; - -interface UseGetMentoringListResponse { - content: MentoringItem[]; - nextPageNumber: number; -} -interface UseGetMentoringListRequest { - size?: number; -} -const OFFSET = 5; // 페이지 오프셋 초기값 - -const getMentoringList = async (page: number, size: number = OFFSET): Promise => { - const endpoint = `/mentor/mentorings?size=${size}&page=${page}`; - const res = await axiosInstance.get(endpoint); - return res.data; -}; - -// 무한스크롤을 위한 useInfiniteQuery -const useGetMentoringList = ({ size = OFFSET }: UseGetMentoringListRequest) => - useInfiniteQuery({ - queryKey: [QueryKeys.mentoringList, size], - queryFn: ({ pageParam = 0 }) => getMentoringList(pageParam as number, size), - initialPageParam: 0, - getNextPageParam: (lastPage: UseGetMentoringListResponse) => { - // nextPageNumber가 -1이면 더 이상 페이지가 없음 - return lastPage.nextPageNumber !== -1 ? lastPage.nextPageNumber : undefined; - }, - refetchInterval: 1000 * 60 * 10, // ⏱️ 10분마다 자동 재요청 - staleTime: 1000 * 60 * 5, // fresh 상태 유지 - select: (data) => data.pages.flatMap((page) => page.content), // 모든 페이지의 content를 평 - }); - -export default useGetMentoringList; diff --git a/src/api/mentor/client/useGetMentoringUncheckedCount.ts b/src/api/mentor/client/useGetMentoringUncheckedCount.ts deleted file mode 100644 index 3204769d..00000000 --- a/src/api/mentor/client/useGetMentoringUncheckedCount.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useQuery } from "@tanstack/react-query"; - -interface GetMentoringNewCountResponse { - uncheckedCount: number; // 멘토링 신규 요청 수 -} - -const getMentoringUncheckedCount = async (): Promise => { - const endpoint = "/mentor/mentorings/check"; - const res = await axiosInstance.get(endpoint); - return res.data; -}; - -// ISR 의도(10분) 유지: staleTime 10분 -const useGetMentoringUncheckedCount = (isEnable: boolean) => - useQuery({ - queryKey: [QueryKeys.mentoringNewCount], - queryFn: getMentoringUncheckedCount, - enabled: isEnable, - refetchInterval: 1000 * 60 * 10, // ⏱️ 10분마다 자동 재요청 - staleTime: 1000 * 60 * 5, // fresh 상태 유지 - select: (data) => data.uncheckedCount, // 필요한 데이터만 반환 - }); - -export default useGetMentoringUncheckedCount; diff --git a/src/api/mentor/client/usePatchMentorCheckMentorings.ts b/src/api/mentor/client/usePatchMentorCheckMentorings.ts deleted file mode 100644 index 3eb2126a..00000000 --- a/src/api/mentor/client/usePatchMentorCheckMentorings.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface UsePatchMentorCheckMentoringsRequest { - checkedMentoringIds: number[]; -} -interface UsePatchMentorCheckMentoringsResponse { - checkedMentoringIds: number[]; // 체크된 멘토링 ID 배열 -} - -const patchMenotrCheck = async ( - body: UsePatchMentorCheckMentoringsRequest, -): Promise => { - const res = await axiosInstance.patch("/mentor/mentorings/check", body); - return res.data; -}; - -const usePatchMentorCheckMentorings = () => { - const queriesClient = useQueryClient(); - return useMutation({ - onSuccess: () => { - // 멘토링 체크 상태 변경 후 멘토링 목록 쿼리 무효화 - Promise.all([ - queriesClient.invalidateQueries({ queryKey: [QueryKeys.mentoringList] }), - queriesClient.invalidateQueries({ queryKey: [QueryKeys.mentoringNewCount] }), - ]); - }, - mutationFn: patchMenotrCheck, - }); -}; - -export default usePatchMentorCheckMentorings; diff --git a/src/api/mentor/client/usePostMentorApplication.ts b/src/api/mentor/client/usePostMentorApplication.ts deleted file mode 100644 index fea8ef24..00000000 --- a/src/api/mentor/client/usePostMentorApplication.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -export interface PostMentorApplicationRequest { - interestedCountries: string[]; // 관심 국가 목록 - country: string; // 수학 국가 - universityName: string; // 수학 학교 - studyStatus: "STUDYING" | "PLANNING" | "COMPLETED"; // 준비 단계 - verificationFile: File; // 증명서 파일 -} - -const postMentorApplication = async (body: PostMentorApplicationRequest): Promise => { - const formData = new FormData(); - - // JSON 데이터를 Blob으로 추가 - const applicationData = { - interestedCountries: body.interestedCountries, - country: body.country, - universityName: body.universityName, - studyStatus: body.studyStatus, - }; - - formData.append( - "mentorApplicationRequest", - new Blob([JSON.stringify(applicationData)], { type: "application/json" }), - ); - - // 파일 추가 - formData.append("file", body.verificationFile); - - const res = await axiosInstance.post("/mentor/verification", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }); - - return res.data; -}; - -const usePostMentorApplication = () => { - return useMutation({ - mutationFn: postMentorApplication, - onError: (error) => { - toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); - }, - }); -}; - -export default usePostMentorApplication; diff --git a/src/api/mentor/client/usePutMyMentorProfile.ts b/src/api/mentor/client/usePutMyMentorProfile.ts deleted file mode 100644 index 8de00810..00000000 --- a/src/api/mentor/client/usePutMyMentorProfile.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface ChannelPayload { - type: string; - url: string; -} - -export interface PutMyMentorProfileRequest { - channels: ChannelPayload[]; - passTip: string; - introduction: string; -} - -const putMyMentorProfile = async (body: PutMyMentorProfileRequest): Promise => { - const res = await axiosInstance.put("/mentor/my", body); - return res.data; -}; - -const usePutMyMentorProfile = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: putMyMentorProfile, - onSuccess: () => { - // 멘토 프로필 데이터를 stale로 만들어 다음 요청 시 새로운 데이터를 가져오도록 함 - queryClient.invalidateQueries({ - queryKey: [QueryKeys.myMentorProfile], - }); - }, - }); -}; - -export default usePutMyMentorProfile; diff --git a/src/api/mentors/client/queryKey.ts b/src/api/mentors/client/queryKey.ts deleted file mode 100644 index 39622908..00000000 --- a/src/api/mentors/client/queryKey.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum QueryKeys { - mentorDetail = "mentorDetail", - mentoringNewCount = "mentoringNewCount", - mentorList = "mentorList", - myMentorProfile = "myMentorProfile", - recommendedMentor = "recommendedMentor", - menteeMentoringList = "menteeMentoringList", -} diff --git a/src/api/mentors/client/useGetMentorDetail.ts b/src/api/mentors/client/useGetMentorDetail.ts deleted file mode 100644 index af4225a6..00000000 --- a/src/api/mentors/client/useGetMentorDetail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentorCardDetail } from "@/types/mentor"; - -import { useQuery } from "@tanstack/react-query"; - -type UseGetMentorDetailResponse = MentorCardDetail; - -const getMentorDetail = async ({ queryKey }: { queryKey: [string, number] }): Promise => { - const [, mentorId] = queryKey; - const res = await axiosInstance.get(`/mentors/${mentorId}`); - return res.data; -}; - -const useGetMentorDetail = (mentorId: number | null) => { - return useQuery({ - queryKey: [QueryKeys.mentorDetail, mentorId!], - queryFn: getMentorDetail, - enabled: mentorId !== null, - staleTime: 1000 * 60 * 5, // 5분간 캐시 - }); -}; - -export default useGetMentorDetail; diff --git a/src/api/mentors/client/useGetMentorList.ts b/src/api/mentors/client/useGetMentorList.ts deleted file mode 100644 index fcac4fbb..00000000 --- a/src/api/mentors/client/useGetMentorList.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentorCardDetail } from "@/types/mentor"; - -import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import type { QueryFunctionContext } from "@tanstack/react-query"; - -interface UseGetMentorListRequest { - region?: string; -} - -interface GetMentorListResponse { - /** 다음 페이지 번호. 다음 페이지가 없으면 -1 */ - nextPageNumber: number; - content: MentorCardDetail[]; -} - -const OFFSET = 10; // 기본 페이지 크기 - -const getMentorList = async ({ - queryKey, - pageParam, -}: QueryFunctionContext<[string, string], number>): Promise => { - const [, region] = queryKey; - const res = await axiosInstance.get( - `/mentors?region=${region}&page=${pageParam}&size=${OFFSET}`, - ); - return res.data; -}; - -const useGetMentorList = ({ region = "" }: UseGetMentorListRequest = {}) => - useInfiniteQuery({ - queryKey: [QueryKeys.mentorList, region], - queryFn: getMentorList, - initialPageParam: 0, - getNextPageParam: (lastPage) => (lastPage.nextPageNumber === -1 ? undefined : lastPage.nextPageNumber), - staleTime: 1000 * 60 * 5, - select: (data) => data.pages.flatMap((p) => p.content), - }); - -// 탭 프리페치용 훅 -export const usePrefetchMentorList = () => { - const queryClient = useQueryClient(); - - const prefetchMentorList = (region: string) => { - queryClient.prefetchInfiniteQuery({ - queryKey: [QueryKeys.mentorList, region], - queryFn: getMentorList, - initialPageParam: 0, - staleTime: 1000 * 60 * 5, - }); - }; - - return { prefetchMentorList }; -}; - -export default useGetMentorList; diff --git a/src/api/my/client/queryKey.ts b/src/api/my/client/queryKey.ts deleted file mode 100644 index 6dff8450..00000000 --- a/src/api/my/client/queryKey.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - myInfo = "myInfo", -} diff --git a/src/api/my/client/useGetMyInfo.ts b/src/api/my/client/useGetMyInfo.ts deleted file mode 100644 index 9f6acd36..00000000 --- a/src/api/my/client/useGetMyInfo.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { UserRole } from "@/types/mentor"; -import { BaseUserInfo } from "@/types/myInfo"; - -import { useMutationState, useQuery } from "@tanstack/react-query"; - -// --- 타입 정의 --- -export interface MenteeInfo extends BaseUserInfo { - role: UserRole.MENTEE; - interestedCountries: string[]; -} - -export interface MentorInfo extends BaseUserInfo { - role: UserRole.MENTOR; - attendedUniversity: string; -} - -export interface AdminInfo extends BaseUserInfo { - role: UserRole.ADMIN; - attendedUniversity: string; -} - -export type MyInfoResponse = MenteeInfo | MentorInfo | AdminInfo; - -// --- API 호출 함수 --- -const getMyInfo = async (): Promise => { - const response: AxiosResponse = await axiosInstance.get("/my"); - return response.data; -}; - -const useGetMyInfo = () => { - const queryResult = useQuery({ - queryKey: [QueryKeys.myInfo], - queryFn: getMyInfo, - // staleTime을 무한으로 설정하여 불필요한 자동 refetch를 방지합니다. - staleTime: Infinity, - gcTime: 1000 * 60 * 30, // 예: 30분 - }); - - const pendingMutations = useMutationState({ - filters: { - mutationKey: [QueryKeys.myInfo, "patch"], - status: "pending", - }, - select: (mutation) => { - return mutation.state.variables as Partial; - }, - }); - - const isOptimistic = pendingMutations.length > 0; - const pendingData = isOptimistic ? pendingMutations[0] : null; - - const displayData = isOptimistic ? { ...queryResult.data, ...pendingData } : queryResult.data; - - return { ...queryResult, data: displayData }; -}; - -export default useGetMyInfo; diff --git a/src/api/my/client/usePatchMyInfo.ts b/src/api/my/client/usePatchMyInfo.ts deleted file mode 100644 index abf9dc58..00000000 --- a/src/api/my/client/usePatchMyInfo.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AxiosError } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "@/lib/zustand/useToastStore"; - -export interface UseMyMentorProfileRequest { - nickname?: string; - file?: File; -} - -const patchMyMentorProfile = async (data: UseMyMentorProfileRequest): Promise => { - const formData = new FormData(); - if (data.nickname) { - formData.append("nickname", data.nickname); - } - if (data.file) { - formData.append("file", data.file); - } - - const res = await axiosInstance.patch("/my", formData); - - return res.data; -}; - -const usePatchMyInfo = () => { - const queryClient = useQueryClient(); - - return useMutation, UseMyMentorProfileRequest>({ - mutationKey: [QueryKeys.myInfo, "patch"], - mutationFn: patchMyMentorProfile, - onSettled: () => { - queryClient.invalidateQueries({ - queryKey: [QueryKeys.myInfo], - }); - }, - onSuccess: () => { - toast.success("프로필이 성공적으로 수정되었습니다."); - }, - onError: (error) => { - const errorMessage = error.response?.data?.message; - toast.error(errorMessage || "프로필 수정에 실패했습니다. 다시 시도해주세요."); - }, - }); -}; - -export default usePatchMyInfo; diff --git a/src/api/news/client/queryKey.ts b/src/api/news/client/queryKey.ts deleted file mode 100644 index 9065e83d..00000000 --- a/src/api/news/client/queryKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum QueryKeys { - articleList = "articleList", - postAddArticle = "postAddArticle", - putModifyArticle = "putModifyArticle", -} diff --git a/src/api/news/client/useDeleteArticleLike.ts b/src/api/news/client/useDeleteArticleLike.ts deleted file mode 100644 index cda01e76..00000000 --- a/src/api/news/client/useDeleteArticleLike.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { Article } from "@/types/news"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -// Article 타입에 isLiked, likeCount 속성이 포함되어 있어야 합니다. - -interface DeleteArticleLikeResponse { - isLiked: boolean; - likeCount: number; -} - -// 1. 롤백을 위한 context 타입을 정의합니다. -type ArticleLikeMutationContext = { - previousArticleList?: { newsResponseList: Article[] }; -}; - -const deleteArticleLike = async (articleId: number): Promise => { - const response: AxiosResponse = await axiosInstance.delete(`/news/${articleId}/like`); - return response.data; -}; - -// 2. 어떤 유저의 목록을 업데이트할지 식별하기 위해 userId를 props로 받습니다. -const useDeleteArticleLike = (userId: number | null) => { - const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; - - return useMutation< - DeleteArticleLikeResponse, - AxiosError<{ message: string }>, - number, // mutate 함수에 전달하는 변수 타입 (articleId) - ArticleLikeMutationContext - >({ - mutationFn: deleteArticleLike, - - // 3. onMutate: '좋아요 취소' 클릭 즉시 UI를 업데이트합니다. - onMutate: async (unlikedArticleId) => { - await queryClient.cancelQueries({ queryKey }); - - const previousArticleList = queryClient.getQueryData<{ newsResponseList: Article[] }>(queryKey); - - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { - if (!oldData) return { newsResponseList: [] }; - return { - newsResponseList: oldData.newsResponseList.map((article) => - article.id === unlikedArticleId - ? { - ...article, - isLiked: false, - likeCount: Math.max(0, (article.likeCount ?? 1) - 1), - } - : article, - ), - }; - }); - - return { previousArticleList }; - }, - - // 4. onError: 실패 시 이전 상태로 롤백합니다. - onError: (err, variables, context) => { - if (context?.previousArticleList) { - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, context.previousArticleList); - } - }, - }); -}; - -export default useDeleteArticleLike; diff --git a/src/api/news/client/useGetArticleList.ts b/src/api/news/client/useGetArticleList.ts deleted file mode 100644 index 77cf86cc..00000000 --- a/src/api/news/client/useGetArticleList.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { Article } from "@/types/news"; - -import { useQuery } from "@tanstack/react-query"; - -interface ArticleListResponse { - newsResponseList: Article[]; -} - -const getArticleList = async (userId: number): Promise => { - const response: AxiosResponse = await axiosInstance.get(`/news?author-id=${userId}`); - return response.data; -}; - -const useGetArticleList = (userId: number) => { - return useQuery({ - queryKey: [QueryKeys.articleList, userId], - queryFn: () => { - // enabled 옵션이 있더라도, 타입 가드를 추가하면 더 안전합니다. - if (userId === null) { - return Promise.reject(new Error("User ID is null")); - } - return getArticleList(userId); - }, - staleTime: 1000 * 60 * 10, // ⏱️ 10분 - - enabled: userId !== null, - // 서버 응답(ArticleListResponse)에서 실제 데이터 배열(Article[])만 선택합니다. - select: (data) => data.newsResponseList, - }); -}; - -export default useGetArticleList; diff --git a/src/api/news/client/usePostArticleLike.ts b/src/api/news/client/usePostArticleLike.ts deleted file mode 100644 index 5d452cce..00000000 --- a/src/api/news/client/usePostArticleLike.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { Article } from "@/types/news"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -// Article 타입 import - -interface PostArticleLikeResponse { - isLiked: boolean; - likeCount: number; -} - -// 1. 롤백을 위한 context 타입을 정의합니다. -type ArticleLikeMutationContext = { - previousArticleList?: Article[]; -}; - -const postArticleLike = async (articleId: number): Promise => { - const response: AxiosResponse = await axiosInstance.post(`/news/${articleId}/like`); - return response.data; -}; - -// 2. 어떤 유저의 목록을 업데이트할지 식별하기 위해 userId를 props로 받습니다. -const usePostArticleLike = (userId: number | null) => { - const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; - - return useMutation< - PostArticleLikeResponse, - AxiosError<{ message: string }>, - number, // mutate 함수에 전달하는 변수 타입 (articleId) - ArticleLikeMutationContext - >({ - mutationFn: postArticleLike, - - // 3. onMutate: '좋아요' 클릭 즉시 UI를 업데이트합니다. - onMutate: async (likedArticleId) => { - await queryClient.cancelQueries({ queryKey }); - - const previousArticleList = queryClient.getQueryData(queryKey); - - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { - if (!oldData) return { newsResponseList: [] }; - return { - newsResponseList: oldData.newsResponseList.map((article) => - article.id === likedArticleId - ? { - ...article, - isLiked: true, // '좋아요' 상태를 true로 변경 - likeCount: (article.likeCount ?? 0) + 1, // '좋아요' 수를 1 증가 - } - : article, - ), - }; - }); - - return { previousArticleList }; - }, - - // 4. onError: 실패 시 이전 상태로 롤백합니다. - onError: (err, variables, context) => { - if (context?.previousArticleList) { - queryClient.setQueryData(queryKey, context.previousArticleList); - } - }, - }); -}; - -export default usePostArticleLike; diff --git a/src/api/reports/client/usePostReport.ts b/src/api/reports/client/usePostReport.ts deleted file mode 100644 index f1531cc5..00000000 --- a/src/api/reports/client/usePostReport.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useRouter } from "next/navigation"; - -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { ReportType } from "@/types/reports"; - -import { useMutation } from "@tanstack/react-query"; -import { toast } from "@/lib/zustand/useToastStore"; - -interface UsePostReportsRequest { - targetType: "POST"; // 지금은 게시글 신고 기능만 존재 - targetId: number; // 신고하려는 리소스의 ID - reportType: ReportType; // Docs 참고 -} - -const postReports = async (body: UsePostReportsRequest): Promise => { - const response: AxiosResponse = await axiosInstance.post(`/reports`, body); - return response.data; -}; - -const usePostReports = () => { - const router = useRouter(); - return useMutation({ - mutationFn: postReports, - onSuccess: () => { - toast.success("신고가 성공적으로 등록되었습니다."); - router.back(); // 신고 후 뒤로 이동 - }, - onError: (error) => { - toast.error("신고 등록에 실패했습니다. 잠시 후 다시 시도해주세요."); - }, - }); -}; - -export default usePostReports; diff --git a/src/api/score/client/queryKey.ts b/src/api/score/client/queryKey.ts deleted file mode 100644 index c7281e50..00000000 --- a/src/api/score/client/queryKey.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum QueryKeys { - myGpaScore = "myGpaScore", - myLanguageTestScore = "myLanguageTestScore", -} diff --git a/src/api/score/client/useGetMyGpaScore.ts b/src/api/score/client/useGetMyGpaScore.ts deleted file mode 100644 index 63d22266..00000000 --- a/src/api/score/client/useGetMyGpaScore.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { GpaScore } from "@/types/score"; - -import { useQuery } from "@tanstack/react-query"; - -interface UseMyGpaScoreResponse { - gpaScoreStatusResponseList: GpaScore[]; -} - -export const getMyGpaScore = (): Promise> => axiosInstance.get("/scores/gpas"); - -const useGetMyGpaScore = () => { - return useQuery({ - queryKey: [QueryKeys.myGpaScore], - queryFn: getMyGpaScore, - staleTime: Infinity, // 5분간 캐시 - select: (data) => data.data.gpaScoreStatusResponseList, - }); -}; - -export default useGetMyGpaScore; diff --git a/src/api/score/client/useGetMyLanguageTestScore.ts b/src/api/score/client/useGetMyLanguageTestScore.ts deleted file mode 100644 index b44cd762..00000000 --- a/src/api/score/client/useGetMyLanguageTestScore.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { LanguageTestScore } from "@/types/score"; - -import { useQuery } from "@tanstack/react-query"; - -interface UseGetMyLanguageTestScoreResponse { - languageTestScoreStatusResponseList: LanguageTestScore[]; -} - -export const getMyLanguageTestScore = (): Promise> => - axiosInstance.get("/scores/language-tests"); - -const useGetMyLanguageTestScore = () => { - return useQuery({ - queryKey: [QueryKeys.myLanguageTestScore], - queryFn: getMyLanguageTestScore, - staleTime: Infinity, - select: (data) => data.data.languageTestScoreStatusResponseList, - }); -}; - -export default useGetMyLanguageTestScore; diff --git a/src/api/score/client/usePostGpaScore.ts b/src/api/score/client/usePostGpaScore.ts deleted file mode 100644 index 1f9dca47..00000000 --- a/src/api/score/client/usePostGpaScore.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "@/lib/zustand/useToastStore"; - -interface UsePostGpaScoreRequest { - gpaScoreRequest: { - gpa: number; - gpaCriteria: number; - issueDate: string; // yyyy-MM-dd - }; - file: Blob; -} - -export const postGpaScore = (request: UsePostGpaScoreRequest): Promise> => { - const convertedRequest: FormData = new FormData(); - convertedRequest.append( - "gpaScoreRequest", - new Blob([JSON.stringify(request.gpaScoreRequest)], { type: "application/json" }), - ); - convertedRequest.append("file", request.file); - return axiosInstance.post("/scores/gpas", convertedRequest); -}; - -export const usePostGpaScore = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (request: UsePostGpaScoreRequest) => postGpaScore(request), - - onSuccess: () => { - toast.success("학점 정보가 성공적으로 제출되었습니다."); - queryClient.invalidateQueries({ queryKey: [QueryKeys.myGpaScore] }); - }, - - onError: (error) => { - console.error("학점 제출 중 오류 발생:", error); - toast.error("오류가 발생했습니다. 다시 시도해주세요."); - }, - }); -}; diff --git a/src/api/score/client/usePostLanguageTestScore.ts b/src/api/score/client/usePostLanguageTestScore.ts deleted file mode 100644 index ee513ca7..00000000 --- a/src/api/score/client/usePostLanguageTestScore.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useRouter } from "next/navigation"; - -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -// 예시: 성공 후 페이지 이동 -import { QueryKeys } from "./queryKey"; - -import { LanguageTestEnum } from "@/types/score"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "@/lib/zustand/useToastStore"; - -// QueryKeys가 정의된 경로로 수정해주세요. - -interface UsePostLanguageTestScoreRequest { - languageTestScoreRequest: { - languageTestType: LanguageTestEnum; - languageTestScore: string; - issueDate: string; // yyyy-MM-dd - }; - file: File; -} - -export const postLanguageTestScore = (request: UsePostLanguageTestScoreRequest): Promise> => { - const convertedRequest: FormData = new FormData(); - convertedRequest.append( - "languageTestScoreRequest", - new Blob([JSON.stringify(request.languageTestScoreRequest)], { type: "application/json" }), - ); - convertedRequest.append("file", request.file); - return axiosInstance.post("/scores/language-tests", convertedRequest); -}; -/** - * 공인 어학 점수를 제출(POST)하기 위한 useMutation 훅입니다. - */ -export const usePostLanguageTestScore = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (request: UsePostLanguageTestScoreRequest) => postLanguageTestScore(request), - - onSuccess: () => { - toast.success("어학 성적이 성공적으로 제출되었습니다."); - queryClient.invalidateQueries({ queryKey: [QueryKeys.myLanguageTestScore] }); - }, - - onError: (error) => { - console.error("어학 성적 제출 중 오류 발생:", error); - toast.error("오류가 발생했습니다. 다시 시도해주세요."); - }, - }); -}; diff --git a/src/api/university/client/queryKey.ts b/src/api/university/client/queryKey.ts deleted file mode 100644 index 78511836..00000000 --- a/src/api/university/client/queryKey.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum QueryKeys { - recommendedUniversity = "recommendedUniversity", - univApplyInfosLike = "univApplyInfosLike", - universitySearchText = "universitySearchText", - universitySearchFilter = "universitySearchFilter", -} diff --git a/src/api/university/client/useDeleteUniversityFavorite.ts b/src/api/university/client/useDeleteUniversityFavorite.ts deleted file mode 100644 index 9413afa2..00000000 --- a/src/api/university/client/useDeleteUniversityFavorite.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -const deleteUniversityFavorite = (universityInfoForApplyId: number): Promise => - axiosInstance.delete(`/univ-apply-infos/${universityInfoForApplyId}/like`); - -const useDeleteUniversityFavorite = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: deleteUniversityFavorite, - onSuccess: () => { - // 위시리스트 관련 쿼리를 무효화하여 데이터를 다시 불러옵니다. - queryClient.invalidateQueries({ queryKey: [QueryKeys.univApplyInfosLike] }); - }, - }); -}; - -export default useDeleteUniversityFavorite; diff --git a/src/api/university/client/useGetMyWishUniversity.ts b/src/api/university/client/useGetMyWishUniversity.ts deleted file mode 100644 index 0b2c74cc..00000000 --- a/src/api/university/client/useGetMyWishUniversity.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ListUniversity } from "@/types/university"; - -import { useQuery } from "@tanstack/react-query"; - -export const getMyWishUniversity = (): Promise> => - axiosInstance.get("/univ-apply-infos/like"); - -const useGetMyWishUniversity = (enabled: boolean = true) => { - return useQuery({ - queryKey: [QueryKeys.univApplyInfosLike], - queryFn: () => getMyWishUniversity(), - staleTime: 1000 * 60 * 5, - select: (data) => data.data, - enabled, - }); -}; - -export default useGetMyWishUniversity; diff --git a/src/api/university/client/useGetRecommendedUniversity.ts b/src/api/university/client/useGetRecommendedUniversity.ts deleted file mode 100644 index 8e2833e4..00000000 --- a/src/api/university/client/useGetRecommendedUniversity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ListUniversity } from "@/types/university"; - -import { getAccessTokenWithReissue } from "@/lib/zustand/useAuthStore"; -import { useQuery } from "@tanstack/react-query"; - -type UseGetRecommendedUniversityResponse = { recommendedUniversities: ListUniversity[] }; -type UseGetRecommendedUniversityRequest = { - isLogin: boolean; -}; - -const getRecommendedUniversity = async (isLogin: boolean): Promise => { - const endpoint = "/univ-apply-infos/recommend"; - - let instance = publicAxiosInstance; - if (isLogin) { - const isLoginState = await getAccessTokenWithReissue(); - instance = isLoginState ? axiosInstance : publicAxiosInstance; - } - const res = await instance.get(endpoint); - return res.data; -}; - -const useGetRecommendedUniversity = ({ isLogin }: UseGetRecommendedUniversityRequest) => - useQuery({ - queryKey: [QueryKeys.recommendedUniversity, isLogin], - queryFn: () => getRecommendedUniversity(isLogin), - staleTime: 1000 * 60 * 5, - select: (data) => data.recommendedUniversities, // 필요한 데이터만 반환 - }); - -export default useGetRecommendedUniversity; diff --git a/src/api/university/client/useGetUniversityDetail.ts b/src/api/university/client/useGetUniversityDetail.ts deleted file mode 100644 index 57faab21..00000000 --- a/src/api/university/client/useGetUniversityDetail.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { University } from "@/types/university"; - -import { useQuery } from "@tanstack/react-query"; - -/** - * @description 대학 상세 조회 API 함수 (공개) - * @param universityInfoForApplyId - 대학 ID - * @returns Promise - */ -const getUniversityDetail = async (universityInfoForApplyId: number): Promise => { - const response: AxiosResponse = await publicAxiosInstance.get( - `/univ-apply-infos/${universityInfoForApplyId}`, - ); - return response.data; -}; - -/** - * @description 대학 상세 조회를 위한 useQuery 커스텀 훅 - */ -const useGetUniversityDetail = (universityInfoForApplyId: number) => { - return useQuery({ - queryKey: [QueryKeys.universityDetail, universityInfoForApplyId], - queryFn: () => getUniversityDetail(universityInfoForApplyId), - enabled: !!universityInfoForApplyId, - }); -}; - -export default useGetUniversityDetail; diff --git a/src/api/university/client/useGetUniversitySearchByFilter.ts b/src/api/university/client/useGetUniversitySearchByFilter.ts deleted file mode 100644 index 67dbfbb1..00000000 --- a/src/api/university/client/useGetUniversitySearchByFilter.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { CountryCode, LanguageTestType, ListUniversity } from "@/types/university"; - -import { useQuery } from "@tanstack/react-query"; - -// --- 타입 정의 --- -export interface UniversitySearchFilterParams { - languageTestType?: LanguageTestType; - testScore?: number; - countryCode?: CountryCode[]; -} - -interface UniversitySearchFilterResponse { - univApplyInfoPreviews: ListUniversity[]; -} - -// --- API 호출 함수 --- -const getUniversitySearchByFilter = async ( - filters: UniversitySearchFilterParams, -): Promise => { - const params = new URLSearchParams(); - - if (filters.languageTestType) { - params.append("languageTestType", filters.languageTestType); - } - if (filters.testScore !== undefined) { - params.append("testScore", String(filters.testScore)); - } - if (filters.countryCode) { - filters.countryCode.forEach((code) => { - params.append("countryCode", code); - }); - } - - const response: AxiosResponse = await publicAxiosInstance.get( - `/univ-apply-infos/search/filter?${params.toString()}`, - ); - return response.data; -}; - -// --- 커스텀 훅 --- -const useGetUniversitySearchByFilter = (filters: UniversitySearchFilterParams) => { - return useQuery({ - queryKey: [QueryKeys.universitySearchFilter, filters], - queryFn: () => getUniversitySearchByFilter(filters), - enabled: Object.values(filters).some((value) => { - if (Array.isArray(value)) return value.length > 0; - return value !== undefined && value !== ""; - }), - select: (data) => data.univApplyInfoPreviews, - }); -}; - -export default useGetUniversitySearchByFilter; diff --git a/src/api/university/client/useGetUniversitySearchByText.ts b/src/api/university/client/useGetUniversitySearchByText.ts deleted file mode 100644 index f63c0579..00000000 --- a/src/api/university/client/useGetUniversitySearchByText.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useMemo } from "react"; - -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ListUniversity } from "@/types/university"; - -import { useQuery } from "@tanstack/react-query"; - -// --- 타입 정의 --- -interface UniversitySearchTextResponse { - univApplyInfoPreviews: ListUniversity[]; -} - -// --- API 호출 함수 --- -const getAllUniversitiesApi = async (): Promise => { - const response: AxiosResponse = await publicAxiosInstance.get( - "/univ-apply-infos/search/text", - { - params: { value: "" }, // 항상 빈 값으로 호출 - }, - ); - return response.data; -}; - -const useUniversitySearch = (searchValue: string) => { - // 1. 모든 대학 데이터를 한 번만 가져와 'Infinity' 캐시로 저장합니다. - const { - data: allUniversities, // 모든 대학 목록 - isLoading, - isError, - error, - } = useQuery({ - queryKey: [QueryKeys.universitySearchText], // "모든 대학"을 위한 고유 키 - queryFn: getAllUniversitiesApi, - staleTime: Infinity, // 한번 가져오면 절대 다시 요청하지 않음 - gcTime: Infinity, // 캐시가 절대 삭제되지 않음 (선택 사항) - select: (data) => data.univApplyInfoPreviews, - }); - - // 2. 검색어가 변경될 때만 캐시된 데이터를 필터링합니다. - const filteredUniversities = useMemo(() => { - const normalizedSearchValue = searchValue.trim().toLowerCase(); - - if (!normalizedSearchValue) { - return allUniversities; // 검색어가 없으면 전체 목록 반환 - } - - // allUniversities가 아직 로드되지 않았으면 빈 배열 반환 - if (!allUniversities) { - return []; - } - - // 대학 이름(koreanName)에 검색어가 포함되어 있는지 확인하여 필터링 - return allUniversities.filter((university) => university.koreanName.toLowerCase().includes(normalizedSearchValue)); - }, [allUniversities, searchValue]); // allUniversities나 searchValue가 바뀔 때만 재계산 - - return { - data: filteredUniversities, // 필터링된 결과 - isLoading, // 초기 데이터 로딩 상태 - isError, - error, - totalCount: allUniversities?.length || 0, // 전체 대학 수 (필요시 사용) - }; -}; -export default useUniversitySearch; diff --git a/src/api/university/client/usePostUniversityFavorite.ts b/src/api/university/client/usePostUniversityFavorite.ts deleted file mode 100644 index 106cbb10..00000000 --- a/src/api/university/client/usePostUniversityFavorite.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; -import { createMutationErrorHandler } from "@/utils/errorHandler"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 위시리스트 학교 추가 API 응답 타입 - * @property {number} universityInfoForApplyId - 추가된 학교 정보 ID - * @property {string} message - 성공 메시지 - */ -interface UniversityFavoriteResponse { - universityInfoForApplyId: number; - message: string; -} - -/** - * @description 위시리스트에 학교를 추가하는 API 함수 - * @param universityInfoForApplyId - 추가할 학교 정보의 ID - * @returns Promise> - */ -export const postUniversityFavoriteApi = ( - universityInfoForApplyId: number, -): Promise> => - axiosInstance.post(`/univ-apply-infos/${universityInfoForApplyId}/like`); - -/** - * @description 위시리스트 학교 추가를 위한 useMutation 커스텀 훅 - */ -const usePostUniversityFavorite = () => { - const queryClient = useQueryClient(); - - return useMutation({ - // mutation 실행 시 호출될 함수 - mutationFn: postUniversityFavoriteApi, - - // mutation 성공 시 실행될 콜백 - onSuccess: () => { - // 위시리스트 관련 쿼리를 무효화하여 데이터를 다시 불러옵니다. - queryClient.invalidateQueries({ queryKey: [QueryKeys.univApplyInfosLike] }); - }, - - // mutation 실패 시 실행될 콜백 - onError: createMutationErrorHandler("위시리스트 추가에 실패했습니다."), - }); -}; - -export default usePostUniversityFavorite; diff --git a/src/apis/Admin/api.ts b/src/apis/Admin/api.ts new file mode 100644 index 00000000..74b67368 --- /dev/null +++ b/src/apis/Admin/api.ts @@ -0,0 +1,176 @@ +import { axiosInstance } from "@/utils/axiosInstance"; + +export interface VerifyLanguageTestResponse { + id: number; + languageTestType: string; + languageTestScore: string; + verifyStatus: string; + rejectedReason: null; +} + +export type VerifyLanguageTestRequest = Record; + +export interface LanguageTestListResponseSort { + sorted: boolean; + unsorted: boolean; + empty: boolean; +} + +export interface LanguageTestListResponsePageable { + pageNumber: number; + pageSize: number; + sort: LanguageTestListResponsePageableSort; + offset: number; + paged: boolean; + unpaged: boolean; +} + +export interface LanguageTestListResponsePageableSort { + sorted: boolean; + unsorted: boolean; + empty: boolean; +} + +export interface LanguageTestListResponseContentItem { + languageTestScoreStatusResponse: LanguageTestListResponseContentItemLanguageTestScoreStatusResponse; + siteUserResponse: LanguageTestListResponseContentItemSiteUserResponse; +} + +export interface LanguageTestListResponseContentItemSiteUserResponse { + id: number; + nickname: string; + profileImageUrl: string; +} + +export interface LanguageTestListResponseContentItemLanguageTestScoreStatusResponse { + id: number; + languageTestResponse: LanguageTestListResponseContentItemLanguageTestScoreStatusResponseLanguageTestResponse; + verifyStatus: string; + rejectedReason: null; + createdAt: string; + updatedAt: string; +} + +export interface LanguageTestListResponseContentItemLanguageTestScoreStatusResponseLanguageTestResponse { + languageTestType: string; + languageTestScore: string; + languageTestReportUrl: string; +} + +export interface LanguageTestListResponse { + content: LanguageTestListResponseContentItem[]; + pageable: LanguageTestListResponsePageable; + totalElements: number; + totalPages: number; + last: boolean; + size: number; + number: number; + sort: LanguageTestListResponseSort; + numberOfElements: number; + first: boolean; + empty: boolean; +} + +export interface VerifyGpaResponse { + id: number; + gpa: number; + gpaCriteria: number; + verifyStatus: string; + rejectedReason: null; +} + +export type VerifyGpaRequest = Record; + +export interface GpaListResponseSort { + sorted: boolean; + unsorted: boolean; + empty: boolean; +} + +export interface GpaListResponsePageable { + pageNumber: number; + pageSize: number; + sort: GpaListResponsePageableSort; + offset: number; + paged: boolean; + unpaged: boolean; +} + +export interface GpaListResponsePageableSort { + sorted: boolean; + unsorted: boolean; + empty: boolean; +} + +export interface GpaListResponseContentItem { + gpaScoreStatusResponse: GpaListResponseContentItemGpaScoreStatusResponse; + siteUserResponse: GpaListResponseContentItemSiteUserResponse; +} + +export interface GpaListResponseContentItemSiteUserResponse { + id: number; + nickname: string; + profileImageUrl: string; +} + +export interface GpaListResponseContentItemGpaScoreStatusResponse { + id: number; + gpaResponse: GpaListResponseContentItemGpaScoreStatusResponseGpaResponse; + verifyStatus: string; + rejectedReason: null; + createdAt: string; + updatedAt: string; +} + +export interface GpaListResponseContentItemGpaScoreStatusResponseGpaResponse { + gpa: number; + gpaCriteria: number; + gpaReportUrl: string; +} + +export interface GpaListResponse { + content: GpaListResponseContentItem[]; + pageable: GpaListResponsePageable; + totalElements: number; + totalPages: number; + last: boolean; + size: number; + number: number; + sort: GpaListResponseSort; + numberOfElements: number; + first: boolean; + empty: boolean; +} + +export const adminApi = { + putVerifyLanguageTest: async (params: { + languageTestScoreId: string | number; + data?: VerifyLanguageTestRequest; + }): Promise => { + const res = await axiosInstance.put( + `/admin/scores/language-tests/${params.languageTestScoreId}`, + params?.data, + ); + return res.data; + }, + + getLanguageTestList: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/admin/scores/language-tests?page=1&size=10`, { + params: params?.params, + }); + return res.data; + }, + + putVerifyGpa: async (params: { + gpaScoreId: string | number; + data?: VerifyGpaRequest; + }): Promise => { + const res = await axiosInstance.put(`/admin/scores/gpas/${params.gpaScoreId}`, params?.data); + return res.data; + }, + + getGpaList: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/admin/scores/gpas`, { params: params?.params }); + return res.data; + }, +}; diff --git a/src/apis/Admin/getGpaList.ts b/src/apis/Admin/getGpaList.ts new file mode 100644 index 00000000..cacb3db3 --- /dev/null +++ b/src/apis/Admin/getGpaList.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { GpaListResponse, adminApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetGpaList = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.Admin.gpaList, params], + queryFn: () => adminApi.getGpaList(params ? { params } : {}), + }); +}; + +export default useGetGpaList; diff --git a/src/apis/Admin/getLanguageTestList.ts b/src/apis/Admin/getLanguageTestList.ts new file mode 100644 index 00000000..e30603a4 --- /dev/null +++ b/src/apis/Admin/getLanguageTestList.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { LanguageTestListResponse, adminApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetLanguageTestList = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.Admin.languageTestList, params], + queryFn: () => adminApi.getLanguageTestList(params ? { params } : {}), + }); +}; + +export default useGetLanguageTestList; diff --git a/src/apis/Admin/index.ts b/src/apis/Admin/index.ts new file mode 100644 index 00000000..f7d22c2a --- /dev/null +++ b/src/apis/Admin/index.ts @@ -0,0 +1,5 @@ +export { adminApi } from "./api"; +export { default as getGpaList } from "./getGpaList"; +export { default as getLanguageTestList } from "./getLanguageTestList"; +export { default as putVerifyGpa } from "./putVerifyGpa"; +export { default as putVerifyLanguageTest } from "./putVerifyLanguageTest"; diff --git a/src/apis/Admin/putVerifyGpa.ts b/src/apis/Admin/putVerifyGpa.ts new file mode 100644 index 00000000..4bd564f4 --- /dev/null +++ b/src/apis/Admin/putVerifyGpa.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { VerifyGpaRequest, VerifyGpaResponse, adminApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePutVerifyGpa = () => { + return useMutation({ + mutationFn: (variables) => adminApi.putVerifyGpa(variables), + }); +}; + +export default usePutVerifyGpa; diff --git a/src/apis/Admin/putVerifyLanguageTest.ts b/src/apis/Admin/putVerifyLanguageTest.ts new file mode 100644 index 00000000..f83b3e6b --- /dev/null +++ b/src/apis/Admin/putVerifyLanguageTest.ts @@ -0,0 +1,17 @@ +import { AxiosError } from "axios"; + +import { VerifyLanguageTestRequest, VerifyLanguageTestResponse, adminApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePutVerifyLanguageTest = () => { + return useMutation< + VerifyLanguageTestResponse, + AxiosError, + { languageTestScoreId: string | number; data: VerifyLanguageTestRequest } + >({ + mutationFn: (variables) => adminApi.putVerifyLanguageTest(variables), + }); +}; + +export default usePutVerifyLanguageTest; diff --git a/src/apis/Auth/api.ts b/src/apis/Auth/api.ts new file mode 100644 index 00000000..1dadaf97 --- /dev/null +++ b/src/apis/Auth/api.ts @@ -0,0 +1,145 @@ +import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; + +export type SignOutResponse = Record; + +export type SignOutRequest = Record; + +// Apple Auth Types +export interface RegisteredAppleAuthResponse { + isRegistered: true; + accessToken: string; + refreshToken: string; +} + +export interface UnregisteredAppleAuthResponse { + isRegistered: false; + nickname: null; + email: string; + profileImageUrl: null; + signUpToken: string; +} + +export type AppleAuthResponse = RegisteredAppleAuthResponse | UnregisteredAppleAuthResponse; + +export interface AppleAuthRequest { + code: string; +} + +export interface RefreshTokenResponse { + accessToken: string; +} + +export type RefreshTokenRequest = Record; + +export interface EmailLoginResponse { + accessToken: string; + refreshToken: string; +} + +export interface EmailLoginRequest { + email: string; + password: string; +} + +export interface EmailVerificationResponse { + signUpToken: string; +} + +export interface EmailVerificationRequest { + email: string; + verificationCode: string; +} + +// Kakao Auth Types +export interface RegisteredKakaoAuthResponse { + isRegistered: true; + accessToken: string; + refreshToken: string; +} + +export interface UnregisteredKakaoAuthResponse { + isRegistered: false; + nickname: string; + email: string; + profileImageUrl: string; + signUpToken: string; +} + +export type KakaoAuthResponse = RegisteredKakaoAuthResponse | UnregisteredKakaoAuthResponse; + +export interface KakaoAuthRequest { + code: string; +} + +export type AccountResponse = void; + +export interface SignUpResponse { + accessToken: string; + refreshToken: string; +} + +export interface SignUpRequest { + signUpToken: string; + nickname: string; + profileImageUrl: string; + preparationStatus: string; + interestedRegions: string[]; + interestedCountries: string[]; +} + +export interface EmailSignUpRequest { + email: string; + password: string; +} + +export interface EmailSignUpResponse { + signUpToken: string; +} + +export const authApi = { + postSignOut: async (): Promise => { + const res = await axiosInstance.post(`/auth/sign-out`); + return res.data; + }, + + postAppleAuth: async (data: AppleAuthRequest): Promise => { + const res = await publicAxiosInstance.post(`/auth/apple`, data); + return res.data; + }, + + postRefreshToken: async (): Promise => { + const res = await publicAxiosInstance.post(`/auth/reissue`); + return res.data; + }, + + postEmailLogin: async (data: EmailLoginRequest): Promise => { + const res = await publicAxiosInstance.post(`/auth/email/sign-in`, data); + return res.data; + }, + + postEmailSignUp: async (data: EmailSignUpRequest): Promise => { + const res = await publicAxiosInstance.post(`/auth/email/sign-up`, data); + return res.data; + }, + + postKakaoAuth: async (data: KakaoAuthRequest): Promise => { + const res = await publicAxiosInstance.post(`/auth/kakao`, data); + return res.data; + }, + + deleteAccount: async (): Promise => { + const res = await axiosInstance.delete(`/auth/quit`); + return res.data; + }, + + postSignUp: async (data: SignUpRequest): Promise => { + // 임시 성별, 생년월일 추가. API 변경 시 삭제 + const payload = { + ...data, + birth: "2000-01-01", + gender: "PREFER_NOT_TO_SAY", + }; + const res = await publicAxiosInstance.post(`/auth/sign-up`, payload); + return res.data; + }, +}; diff --git a/src/api/auth/client/useDeleteUserAccount.ts b/src/apis/Auth/deleteAccount.ts similarity index 56% rename from src/api/auth/client/useDeleteUserAccount.ts rename to src/apis/Auth/deleteAccount.ts index 0d0fa96b..b9274190 100644 --- a/src/api/auth/client/useDeleteUserAccount.ts +++ b/src/apis/Auth/deleteAccount.ts @@ -1,24 +1,25 @@ import { useRouter } from "next/navigation"; -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; +import { AccountResponse, authApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -export const deleteUserAccount = (): Promise> => axiosInstance.delete("/auth/quit"); - +/** + * @description 회원탈퇴를 위한 useMutation 커스텀 훅 + */ const useDeleteUserAccount = () => { const router = useRouter(); const { clearAccessToken } = useAuthStore(); - const queryClient = useQueryClient(); // 쿼리 캐시 관리를 위해 클라이언트 인스턴스를 가져옵니다. + const queryClient = useQueryClient(); - return useMutation({ - mutationFn: deleteUserAccount, + return useMutation({ + mutationFn: () => authApi.deleteAccount(), onMutate: () => { - // 낙관적 업데이트: 로그아웃 요청이 시작되면 바로 로그인 상태를 false로 변경합니다. + // 낙관적 업데이트: 요청이 시작되면 바로 홈으로 이동 router.replace("/"); }, onSuccess: () => { @@ -26,7 +27,7 @@ const useDeleteUserAccount = () => { clearAccessToken(); queryClient.clear(); }, - onError: (error) => { + onError: () => { toast.error("회원탈퇴에 실패했습니다. 잠시 후 다시 시도해주세요."); }, }); diff --git a/src/apis/Auth/index.ts b/src/apis/Auth/index.ts new file mode 100644 index 00000000..da505d67 --- /dev/null +++ b/src/apis/Auth/index.ts @@ -0,0 +1,25 @@ +export { authApi } from "./api"; +export type { + KakaoAuthRequest, + KakaoAuthResponse, + AppleAuthRequest, + AppleAuthResponse, + EmailLoginRequest, + EmailLoginResponse, + SignUpRequest, + SignUpResponse, + EmailSignUpRequest, + EmailSignUpResponse, +} from "./api"; + +// Client-side hooks +export { default as useDeleteUserAccount } from "./deleteAccount"; +export { default as usePostAppleAuth } from "./postAppleAuth"; +export { default as usePostEmailAuth } from "./postEmailLogin"; +export { default as usePostEmailSignUp } from "./postEmailVerification"; +export { default as usePostKakaoAuth } from "./postKakaoAuth"; +export { default as usePostLogout } from "./postSignOut"; +export { default as usePostSignUp } from "./postSignUp"; + +// Server-side functions +export { postReissueToken } from "./server"; diff --git a/src/api/auth/client/usePostAppleAuth.ts b/src/apis/Auth/postAppleAuth.ts similarity index 62% rename from src/api/auth/client/usePostAppleAuth.ts rename to src/apis/Auth/postAppleAuth.ts index 5079cf25..2ef03e0a 100644 --- a/src/api/auth/client/usePostAppleAuth.ts +++ b/src/apis/Auth/postAppleAuth.ts @@ -1,47 +1,25 @@ import { useRouter, useSearchParams } from "next/navigation"; -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { publicAxiosInstance } from "@/utils/axiosInstance"; import { validateSafeRedirect } from "@/utils/authUtils"; +import { AppleAuthRequest, AppleAuthResponse, authApi } from "./api"; + import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation } from "@tanstack/react-query"; -// Apple -export interface RegisteredAppleAuthResponse { - isRegistered: true; - accessToken: string; - refreshToken: string; -} - -export interface UnregisteredAppleAuthResponse { - isRegistered: false; - nickname: null; - email: string; - profileImageUrl: null; - signUpToken: string; -} - -interface AppleAuthRequest { - code: string; -} - -type AppleAuthResponse = RegisteredAppleAuthResponse | UnregisteredAppleAuthResponse; - -const postAppleAuth = ({ code }: AppleAuthRequest): Promise> => - publicAxiosInstance.post("/auth/apple", { code }); - +/** + * @description 애플 로그인을 위한 useMutation 커스텀 훅 + */ const usePostAppleAuth = () => { const router = useRouter(); const searchParams = useSearchParams(); - return useMutation({ - mutationFn: postAppleAuth, - onSuccess: (response) => { - const { data } = response; - + return useMutation({ + mutationFn: (data) => authApi.postAppleAuth(data), + onSuccess: (data) => { if (data.isRegistered) { // 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장 // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 diff --git a/src/api/auth/client/usePostEmailAuth.ts b/src/apis/Auth/postEmailLogin.ts similarity index 64% rename from src/api/auth/client/usePostEmailAuth.ts rename to src/apis/Auth/postEmailLogin.ts index 3b4d1e16..af5d54ce 100644 --- a/src/api/auth/client/usePostEmailAuth.ts +++ b/src/apis/Auth/postEmailLogin.ts @@ -1,36 +1,27 @@ import { useRouter, useSearchParams } from "next/navigation"; -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { publicAxiosInstance } from "@/utils/axiosInstance"; import { validateSafeRedirect } from "@/utils/authUtils"; +import { EmailLoginRequest, EmailLoginResponse, authApi } from "./api"; + import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation } from "@tanstack/react-query"; -interface UsePostEmailSignInResponse { - accessToken: string; - refreshToken: string; -} - -interface LoginRequest { - email: string; - password: string; -} - -const postEmailAuth = ({ email, password }: LoginRequest): Promise> => - publicAxiosInstance.post("/auth/email/sign-in", { email, password }); - +/** + * @description 이메일 로그인을 위한 useMutation 커스텀 훅 + */ const usePostEmailAuth = () => { const { setAccessToken } = useAuthStore(); const searchParams = useSearchParams(); const router = useRouter(); - return useMutation({ - mutationFn: postEmailAuth, + return useMutation({ + mutationFn: (data) => authApi.postEmailLogin(data), onSuccess: (data) => { - const { accessToken } = data.data; + const { accessToken } = data; // Zustand persist가 자동으로 localStorage에 저장 // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 @@ -45,4 +36,5 @@ const usePostEmailAuth = () => { }, }); }; + export default usePostEmailAuth; diff --git a/src/apis/Auth/postEmailVerification.ts b/src/apis/Auth/postEmailVerification.ts new file mode 100644 index 00000000..c94bc09d --- /dev/null +++ b/src/apis/Auth/postEmailVerification.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { EmailSignUpRequest, EmailSignUpResponse, authApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 이메일 회원가입을 위한 useMutation 커스텀 훅 + */ +const usePostEmailSignUp = () => { + return useMutation({ + mutationFn: (data) => authApi.postEmailSignUp(data), + onError: (error) => { + console.error("이메일 회원가입 실패:", error); + toast.error("회원가입에 실패했습니다."); + }, + }); +}; + +export default usePostEmailSignUp; diff --git a/src/api/auth/client/usePostKakaoAuth.ts b/src/apis/Auth/postKakaoAuth.ts similarity index 64% rename from src/api/auth/client/usePostKakaoAuth.ts rename to src/apis/Auth/postKakaoAuth.ts index 32804801..9179848b 100644 --- a/src/api/auth/client/usePostKakaoAuth.ts +++ b/src/apis/Auth/postKakaoAuth.ts @@ -1,48 +1,26 @@ import { useRouter, useSearchParams } from "next/navigation"; -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { publicAxiosInstance } from "@/utils/axiosInstance"; import { validateSafeRedirect } from "@/utils/authUtils"; +import { KakaoAuthRequest, KakaoAuthResponse, authApi } from "./api"; + import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation } from "@tanstack/react-query"; -// Kakao -interface RegisteredKakaoAuthReponse { - isRegistered: true; - accessToken: string; - refreshToken: string; -} - -interface UnregisteredKakaoAuthReponse { - isRegistered: false; - nickname: string; - email: string; - profileImageUrl: string; - signUpToken: string; -} - -interface KakaoAuthRequest { - code: string; -} - -const postKakaoAuth = ({ - code, -}: KakaoAuthRequest): Promise> => - publicAxiosInstance.post("/auth/kakao", { code }); - +/** + * @description 카카오 로그인을 위한 useMutation 커스텀 훅 + */ const usePostKakaoAuth = () => { const { setAccessToken } = useAuthStore(); const router = useRouter(); const searchParams = useSearchParams(); - return useMutation({ - mutationFn: postKakaoAuth, - onSuccess: (response) => { - const { data } = response; - + return useMutation({ + mutationFn: (data) => authApi.postKakaoAuth(data), + onSuccess: (data) => { if (data.isRegistered) { // 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장 // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 diff --git a/src/apis/Auth/postRefreshToken.ts b/src/apis/Auth/postRefreshToken.ts new file mode 100644 index 00000000..95140426 --- /dev/null +++ b/src/apis/Auth/postRefreshToken.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { RefreshTokenRequest, RefreshTokenResponse, authApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostRefreshToken = () => { + return useMutation({ + mutationFn: () => authApi.postRefreshToken(), + }); +}; + +export default usePostRefreshToken; diff --git a/src/api/auth/client/usePostLogout.ts b/src/apis/Auth/postSignOut.ts similarity index 56% rename from src/api/auth/client/usePostLogout.ts rename to src/apis/Auth/postSignOut.ts index ccfba24e..1c477418 100644 --- a/src/api/auth/client/usePostLogout.ts +++ b/src/apis/Auth/postSignOut.ts @@ -1,18 +1,19 @@ -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; +import { SignOutResponse, authApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -export const postLogout = (): Promise> => axiosInstance.post("/auth/sign-out"); - +/** + * @description 로그아웃을 위한 useMutation 커스텀 훅 + */ const usePostLogout = () => { const { clearAccessToken } = useAuthStore(); - const queryClient = useQueryClient(); // 쿼리 캐시 관리를 위해 클라이언트 인스턴스를 가져옵니다. + const queryClient = useQueryClient(); - return useMutation({ - mutationFn: postLogout, + return useMutation({ + mutationFn: () => authApi.postSignOut(), onSuccess: () => { // Zustand persist가 자동으로 localStorage에서 제거 clearAccessToken(); diff --git a/src/apis/Auth/postSignUp.ts b/src/apis/Auth/postSignUp.ts new file mode 100644 index 00000000..cbfe6aae --- /dev/null +++ b/src/apis/Auth/postSignUp.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { SignUpRequest, SignUpResponse, authApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 회원가입을 위한 useMutation 커스텀 훅 + */ +const usePostSignUp = () => { + return useMutation({ + mutationFn: (data) => authApi.postSignUp(data), + onError: (error) => { + console.error("회원가입 실패:", error); + toast.error("회원가입에 실패했습니다."); + }, + }); +}; + +export default usePostSignUp; diff --git a/src/apis/Auth/server/index.ts b/src/apis/Auth/server/index.ts new file mode 100644 index 00000000..09d60f03 --- /dev/null +++ b/src/apis/Auth/server/index.ts @@ -0,0 +1 @@ +export { default as postReissueToken } from "./postReissueToken"; diff --git a/src/api/auth/server/postReissueToken.ts b/src/apis/Auth/server/postReissueToken.ts similarity index 87% rename from src/api/auth/server/postReissueToken.ts rename to src/apis/Auth/server/postReissueToken.ts index 3bc130d2..51c60ed1 100644 --- a/src/api/auth/server/postReissueToken.ts +++ b/src/apis/Auth/server/postReissueToken.ts @@ -2,6 +2,10 @@ import { publicAxiosInstance } from "@/utils/axiosInstance"; import useAuthStore from "@/lib/zustand/useAuthStore"; +/** + * @description 토큰 재발급 서버사이드 함수 + * axiosInstance의 interceptor에서 사용됨 + */ const postReissueToken = async (): Promise => { try { const response = await publicAxiosInstance.post<{ accessToken: string }>("/auth/reissue"); @@ -21,4 +25,5 @@ const postReissueToken = async (): Promise => { throw error; } }; + export default postReissueToken; diff --git a/src/apis/MyPage/api.ts b/src/apis/MyPage/api.ts new file mode 100644 index 00000000..2e3cb6bd --- /dev/null +++ b/src/apis/MyPage/api.ts @@ -0,0 +1,70 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { UserRole } from "@/types/mentor"; +import { BaseUserInfo } from "@/types/myInfo"; + +// --- 타입 정의 --- +export interface MenteeInfo extends BaseUserInfo { + role: UserRole.MENTEE; + interestedCountries: string[]; +} + +export interface MentorInfo extends BaseUserInfo { + role: UserRole.MENTOR; + attendedUniversity: string; +} + +export interface AdminInfo extends BaseUserInfo { + role: UserRole.ADMIN; + attendedUniversity: string; +} + +export type MyInfoResponse = MenteeInfo | MentorInfo | AdminInfo; + +export type InterestedRegionCountryResponse = void; + +export type InterestedRegionCountryRequest = string[]; + +export interface ProfilePatchRequest { + nickname?: string; + file?: File; +} + +export interface PasswordPatchRequest { + currentPassword: string; + newPassword: string; + newPasswordConfirmation: string; +} + +export const myPageApi = { + getProfile: async (): Promise => { + const response: AxiosResponse = await axiosInstance.get("/my"); + return response.data; + }, + + patchProfile: async (data: ProfilePatchRequest): Promise => { + const formData = new FormData(); + if (data.nickname) { + formData.append("nickname", data.nickname); + } + if (data.file) { + formData.append("file", data.file); + } + const res = await axiosInstance.patch("/my", formData); + return res.data; + }, + + patchPassword: async (data: PasswordPatchRequest): Promise => { + const res = await axiosInstance.patch("/my/password", data); + return res.data; + }, + + patchInterestedRegionCountry: async ( + data: InterestedRegionCountryRequest, + ): Promise => { + const res = await axiosInstance.patch(`/my/interested-location`, data); + return res.data; + }, +}; diff --git a/src/apis/MyPage/getProfile.ts b/src/apis/MyPage/getProfile.ts new file mode 100644 index 00000000..b25e2092 --- /dev/null +++ b/src/apis/MyPage/getProfile.ts @@ -0,0 +1,39 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { MyInfoResponse, myPageApi } from "./api"; + +import { UseQueryResult, useMutationState, useQuery } from "@tanstack/react-query"; + +type UseGetMyInfoResult = Omit, "data"> & { + data: MyInfoResponse | undefined; +}; + +const useGetMyInfo = (): UseGetMyInfoResult => { + const queryResult = useQuery({ + queryKey: [QueryKeys.MyPage.profile], + queryFn: () => myPageApi.getProfile(), + // staleTime을 무한으로 설정하여 불필요한 자동 refetch를 방지합니다. + staleTime: Infinity, + gcTime: 1000 * 60 * 30, // 예: 30분 + }); + + const pendingMutations = useMutationState({ + filters: { + mutationKey: [QueryKeys.MyPage.profile, "patch"], + status: "pending", + }, + select: (mutation) => { + return mutation.state.variables as Partial; + }, + }); + + const isOptimistic = pendingMutations.length > 0; + const pendingData = isOptimistic ? pendingMutations[0] : null; + + const displayData = isOptimistic && queryResult.data ? { ...queryResult.data, ...pendingData } : queryResult.data; + + return { ...queryResult, data: displayData as MyInfoResponse | undefined }; +}; + +export default useGetMyInfo; diff --git a/src/apis/MyPage/index.ts b/src/apis/MyPage/index.ts new file mode 100644 index 00000000..d25fcb4d --- /dev/null +++ b/src/apis/MyPage/index.ts @@ -0,0 +1,13 @@ +export { + myPageApi, + type MyInfoResponse, + type MenteeInfo, + type MentorInfo, + type AdminInfo, + type ProfilePatchRequest, + type PasswordPatchRequest, +} from "./api"; +export { default as useGetMyInfo } from "./getProfile"; +export { default as usePatchMyInfo } from "./patchProfile"; +export { default as usePatchMyPassword } from "./patchPassword"; +export { default as usePatchInterestedRegionCountry } from "./patchInterestedRegionCountry"; diff --git a/src/apis/MyPage/patchInterestedRegionCountry.ts b/src/apis/MyPage/patchInterestedRegionCountry.ts new file mode 100644 index 00000000..19fdef47 --- /dev/null +++ b/src/apis/MyPage/patchInterestedRegionCountry.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { InterestedRegionCountryRequest, InterestedRegionCountryResponse, myPageApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePatchInterestedRegionCountry = () => { + return useMutation({ + mutationFn: (data) => myPageApi.patchInterestedRegionCountry(data), + }); +}; + +export default usePatchInterestedRegionCountry; diff --git a/src/api/my/client/usePatchMyPassword.ts b/src/apis/MyPage/patchPassword.ts similarity index 53% rename from src/api/my/client/usePatchMyPassword.ts rename to src/apis/MyPage/patchPassword.ts index eddda64c..b9c1f96a 100644 --- a/src/api/my/client/usePatchMyPassword.ts +++ b/src/apis/MyPage/patchPassword.ts @@ -2,37 +2,25 @@ import { useRouter } from "next/navigation"; import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; +import { QueryKeys } from "../queryKeys"; +import { PasswordPatchRequest, myPageApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "@/lib/zustand/useToastStore"; - -export interface UseMyMentorProfileRequest { - currentPassword: string; - newPassword: string; - newPasswordConfirmation: string; -} - -const patchMyPassword = async (data: UseMyMentorProfileRequest): Promise => { - const res = await axiosInstance.patch("/my/password", data, {}); - return res.data; -}; +import { useMutation, useQueryClient } from "@tanstack/react-query"; const usePatchMyPassword = () => { const queryClient = useQueryClient(); const router = useRouter(); const { clearAccessToken } = useAuthStore(); - return useMutation, UseMyMentorProfileRequest>({ - mutationKey: [QueryKeys.myInfo, "password", "patch"], - mutationFn: patchMyPassword, + return useMutation, PasswordPatchRequest>({ + mutationKey: [QueryKeys.MyPage.password, "patch"], + mutationFn: (data) => myPageApi.patchPassword(data), onSuccess: () => { clearAccessToken(); queryClient.clear(); - toast.success("프로필이 성공적으로 수정되었습니다."); + toast.success("비밀번호가 성공적으로 변경되었습니다."); router.replace("/"); }, onError: (error) => { diff --git a/src/apis/MyPage/patchProfile.ts b/src/apis/MyPage/patchProfile.ts new file mode 100644 index 00000000..8e8bcb16 --- /dev/null +++ b/src/apis/MyPage/patchProfile.ts @@ -0,0 +1,30 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { ProfilePatchRequest, myPageApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +const usePatchMyInfo = () => { + const queryClient = useQueryClient(); + + return useMutation, ProfilePatchRequest>({ + mutationKey: [QueryKeys.MyPage.profile, "patch"], + mutationFn: (data) => myPageApi.patchProfile(data), + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: [QueryKeys.MyPage.profile], + }); + }, + onSuccess: () => { + toast.success("프로필이 성공적으로 수정되었습니다."); + }, + onError: (error) => { + const errorMessage = error.response?.data?.message; + toast.error(errorMessage || "프로필 수정에 실패했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePatchMyInfo; diff --git a/src/apis/Scores/api.ts b/src/apis/Scores/api.ts new file mode 100644 index 00000000..0ccd1d05 --- /dev/null +++ b/src/apis/Scores/api.ts @@ -0,0 +1,81 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { GpaScore, LanguageTestEnum, LanguageTestScore } from "@/types/score"; + +// ====== Query Keys ====== +export const ScoresQueryKeys = { + myGpaScore: "myGpaScore", + myLanguageTestScore: "myLanguageTestScore", +} as const; + +// ====== Types ====== +export interface UseMyGpaScoreResponse { + gpaScoreStatusResponseList: GpaScore[]; +} + +export interface UseGetMyLanguageTestScoreResponse { + languageTestScoreStatusResponseList: LanguageTestScore[]; +} + +export interface UsePostGpaScoreRequest { + gpaScoreRequest: { + gpa: number; + gpaCriteria: number; + issueDate: string; // yyyy-MM-dd + }; + file: Blob; +} + +export interface UsePostLanguageTestScoreRequest { + languageTestScoreRequest: { + languageTestType: LanguageTestEnum; + languageTestScore: string; + issueDate: string; // yyyy-MM-dd + }; + file: File; +} + +// ====== API Functions ====== +export const scoresApi = { + /** + * 내 학점 점수 조회 + */ + getMyGpaScore: async (): Promise> => { + return axiosInstance.get("/scores/gpas"); + }, + + /** + * 내 어학 점수 조회 + */ + getMyLanguageTestScore: async (): Promise> => { + return axiosInstance.get("/scores/language-tests"); + }, + + /** + * 학점 점수 제출 + */ + postGpaScore: async (request: UsePostGpaScoreRequest): Promise> => { + const formData = new FormData(); + formData.append( + "gpaScoreRequest", + new Blob([JSON.stringify(request.gpaScoreRequest)], { type: "application/json" }), + ); + formData.append("file", request.file); + return axiosInstance.post("/scores/gpas", formData); + }, + + /** + * 어학 점수 제출 + */ + postLanguageTestScore: async (request: UsePostLanguageTestScoreRequest): Promise> => { + const formData = new FormData(); + formData.append( + "languageTestScoreRequest", + new Blob([JSON.stringify(request.languageTestScoreRequest)], { type: "application/json" }), + ); + formData.append("file", request.file); + return axiosInstance.post("/scores/language-tests", formData); + }, +}; diff --git a/src/apis/Scores/getGpaList.ts b/src/apis/Scores/getGpaList.ts new file mode 100644 index 00000000..a2344ba2 --- /dev/null +++ b/src/apis/Scores/getGpaList.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { ScoresQueryKeys, scoresApi } from "./api"; + +import { GpaScore } from "@/types/score"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 내 학점 점수 조회 훅 + */ +const useGetMyGpaScore = () => { + return useQuery({ + queryKey: [ScoresQueryKeys.myGpaScore], + queryFn: scoresApi.getMyGpaScore, + staleTime: Infinity, + select: (data) => data.data.gpaScoreStatusResponseList, + }); +}; + +export default useGetMyGpaScore; diff --git a/src/apis/Scores/getLanguageTestList.ts b/src/apis/Scores/getLanguageTestList.ts new file mode 100644 index 00000000..642ec2b2 --- /dev/null +++ b/src/apis/Scores/getLanguageTestList.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { ScoresQueryKeys, scoresApi } from "./api"; + +import { LanguageTestScore } from "@/types/score"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 내 어학 점수 조회 훅 + */ +const useGetMyLanguageTestScore = () => { + return useQuery({ + queryKey: [ScoresQueryKeys.myLanguageTestScore], + queryFn: scoresApi.getMyLanguageTestScore, + staleTime: Infinity, + select: (data) => data.data.languageTestScoreStatusResponseList, + }); +}; + +export default useGetMyLanguageTestScore; diff --git a/src/apis/Scores/index.ts b/src/apis/Scores/index.ts new file mode 100644 index 00000000..82bc201a --- /dev/null +++ b/src/apis/Scores/index.ts @@ -0,0 +1,12 @@ +export { scoresApi, ScoresQueryKeys } from "./api"; +export type { + UseMyGpaScoreResponse, + UseGetMyLanguageTestScoreResponse, + UsePostGpaScoreRequest, + UsePostLanguageTestScoreRequest, +} from "./api"; + +export { default as useGetMyGpaScore } from "./getGpaList"; +export { default as useGetMyLanguageTestScore } from "./getLanguageTestList"; +export { default as usePostGpaScore } from "./postCreateGpa"; +export { default as usePostLanguageTestScore } from "./postCreateLanguageTest"; diff --git a/src/apis/Scores/postCreateGpa.ts b/src/apis/Scores/postCreateGpa.ts new file mode 100644 index 00000000..a3252781 --- /dev/null +++ b/src/apis/Scores/postCreateGpa.ts @@ -0,0 +1,29 @@ +import { AxiosError } from "axios"; + +import { ScoresQueryKeys, UsePostGpaScoreRequest, scoresApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 학점 점수 제출 훅 + */ +export const usePostGpaScore = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (request: UsePostGpaScoreRequest) => scoresApi.postGpaScore(request), + + onSuccess: () => { + toast.success("학점 정보가 성공적으로 제출되었습니다."); + queryClient.invalidateQueries({ queryKey: [ScoresQueryKeys.myGpaScore] }); + }, + + onError: (error) => { + console.error("학점 제출 중 오류 발생:", error); + toast.error("오류가 발생했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostGpaScore; diff --git a/src/apis/Scores/postCreateLanguageTest.ts b/src/apis/Scores/postCreateLanguageTest.ts new file mode 100644 index 00000000..bc8527ff --- /dev/null +++ b/src/apis/Scores/postCreateLanguageTest.ts @@ -0,0 +1,29 @@ +import { AxiosError } from "axios"; + +import { ScoresQueryKeys, UsePostLanguageTestScoreRequest, scoresApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 어학 점수 제출 훅 + */ +export const usePostLanguageTestScore = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (request: UsePostLanguageTestScoreRequest) => scoresApi.postLanguageTestScore(request), + + onSuccess: () => { + toast.success("어학 성적이 성공적으로 제출되었습니다."); + queryClient.invalidateQueries({ queryKey: [ScoresQueryKeys.myLanguageTestScore] }); + }, + + onError: (error) => { + console.error("어학 성적 제출 중 오류 발생:", error); + toast.error("오류가 발생했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostLanguageTestScore; diff --git a/src/apis/applications/api.ts b/src/apis/applications/api.ts new file mode 100644 index 00000000..f41225aa --- /dev/null +++ b/src/apis/applications/api.ts @@ -0,0 +1,60 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { ApplicationListResponse } from "@/types/application"; + +// ====== Query Keys ====== +export const ApplicationsQueryKeys = { + competitorsApplicationList: "competitorsApplicationList", +} as const; + +// ====== Types ====== +export interface UseSubmitApplicationResponse { + isSuccess: boolean; +} + +export interface UseSubmitApplicationRequest { + gpaScoreId: number; + languageTestScoreId: number; + universityChoiceRequest: { + firstChoiceUniversityId: number | null; + secondChoiceUniversityId: number | null; + thirdChoiceUniversityId: number | null; + }; +} + +export interface CompetitorsResponse { + competitors: Array<{ + id: number; + name: string; + score: number; + }>; +} + +// ====== API Functions ====== +export const applicationsApi = { + /** + * 지원 목록 조회 + */ + getApplicationsList: async (): Promise> => { + return axiosInstance.get("/applications"); + }, + + /** + * 지원 제출 + */ + postSubmitApplication: async ( + request: UseSubmitApplicationRequest, + ): Promise> => { + return axiosInstance.post("/applications", request); + }, + + /** + * 경쟁자 목록 조회 + */ + getCompetitors: async (config?: { params?: Record }): Promise => { + const res = await axiosInstance.get("/applications/competitors", config); + return res.data; + }, +}; diff --git a/src/apis/applications/getApplicants.ts b/src/apis/applications/getApplicants.ts new file mode 100644 index 00000000..6523baa8 --- /dev/null +++ b/src/apis/applications/getApplicants.ts @@ -0,0 +1,29 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { ApplicationsQueryKeys, applicationsApi } from "./api"; + +import { ApplicationListResponse } from "@/types/application"; + +import { UseQueryOptions, UseQueryResult, useQuery } from "@tanstack/react-query"; + +type UseGetApplicationsListOptions = Omit< + UseQueryOptions, AxiosError<{ message: string }>, ApplicationListResponse>, + "queryKey" | "queryFn" +>; + +/** + * @description 지원 목록 조회 훅 + */ +const useGetApplicationsList = ( + props?: UseGetApplicationsListOptions, +): UseQueryResult> => { + return useQuery({ + queryKey: [ApplicationsQueryKeys.competitorsApplicationList], + queryFn: applicationsApi.getApplicationsList, + staleTime: 1000 * 60 * 5, // 5분간 캐시 + select: (response) => response.data, + ...props, + }); +}; + +export default useGetApplicationsList; diff --git a/src/apis/applications/getCompetitors.ts b/src/apis/applications/getCompetitors.ts new file mode 100644 index 00000000..c0696094 --- /dev/null +++ b/src/apis/applications/getCompetitors.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { CompetitorsResponse, applicationsApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetCompetitors = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.applications.competitors, params], + queryFn: () => applicationsApi.getCompetitors(params ? { params } : {}), + }); +}; + +export default useGetCompetitors; diff --git a/src/apis/applications/index.ts b/src/apis/applications/index.ts new file mode 100644 index 00000000..a618c45e --- /dev/null +++ b/src/apis/applications/index.ts @@ -0,0 +1,5 @@ +export { applicationsApi, ApplicationsQueryKeys } from "./api"; +export type { UseSubmitApplicationResponse, UseSubmitApplicationRequest } from "./api"; +export { default as useGetApplicationsList } from "./getApplicants"; +export { default as useGetCompetitors } from "./getCompetitors"; +export { default as usePostSubmitApplication } from "./postSubmitApplication"; diff --git a/src/apis/applications/postSubmitApplication.ts b/src/apis/applications/postSubmitApplication.ts new file mode 100644 index 00000000..45c47bd5 --- /dev/null +++ b/src/apis/applications/postSubmitApplication.ts @@ -0,0 +1,38 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { UseSubmitApplicationRequest, UseSubmitApplicationResponse, applicationsApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { UseMutationOptions, UseMutationResult, useMutation } from "@tanstack/react-query"; + +/** + * @description 지원 제출 훅 + */ +const usePostSubmitApplication = ( + props?: UseMutationOptions< + AxiosResponse, + AxiosError<{ message: string }>, + UseSubmitApplicationRequest, + unknown + >, +): UseMutationResult< + AxiosResponse, + AxiosError<{ message: string }>, + UseSubmitApplicationRequest, + unknown +> => { + return useMutation< + AxiosResponse, + AxiosError<{ message: string }>, + UseSubmitApplicationRequest + >({ + ...props, + mutationFn: applicationsApi.postSubmitApplication, + onError: (error) => { + const errorMessage = error?.response?.data?.message; + toast.error(errorMessage || "지원 중 오류가 발생했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostSubmitApplication; diff --git a/src/apis/chat/api.ts b/src/apis/chat/api.ts new file mode 100644 index 00000000..df2d984b --- /dev/null +++ b/src/apis/chat/api.ts @@ -0,0 +1,57 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { ChatMessage, ChatPartner, ChatRoom } from "@/types/chat"; + +// QueryKeys for chat domain +export const ChatQueryKeys = { + chatRooms: "chatRooms", + chatHistories: "chatHistories", + partnerInfo: "partnerInfo", +} as const; + +// Re-export types from @/types/chat +export type { ChatMessage, ChatRoom, ChatPartner }; + +export interface ChatHistoriesResponse { + nextPageNumber: number; // 다음 페이지가 없다면 -1 + content: ChatMessage[]; +} + +export interface ChatRoomListResponse { + chatRooms: ChatRoom[]; +} + +interface GetChatHistoriesParams { + roomId: number; + size?: number; + page?: number; +} + +export const chatApi = { + getChatHistories: async ({ roomId, size = 20, page = 0 }: GetChatHistoriesParams): Promise => { + const res = await axiosInstance.get(`/chats/rooms/${roomId}`, { + params: { + size, + page, + }, + }); + return res.data; + }, + + getChatRooms: async (): Promise => { + const res = await axiosInstance.get("/chats/rooms"); + return res.data; + }, + + putReadChatRoom: async (roomId: number): Promise => { + const response: AxiosResponse = await axiosInstance.put(`/chats/rooms/${roomId}/read`); + return response.data; + }, + + getChatPartner: async (roomId: number): Promise => { + const res = await axiosInstance.get(`/chats/rooms/${roomId}/partner`); + return res.data; + }, +}; diff --git a/src/api/chat/clients/useGetChatHistories.ts b/src/apis/chat/getChatMessages.ts similarity index 55% rename from src/api/chat/clients/useGetChatHistories.ts rename to src/apis/chat/getChatMessages.ts index 91916b08..9c0adf12 100644 --- a/src/api/chat/clients/useGetChatHistories.ts +++ b/src/apis/chat/getChatMessages.ts @@ -1,38 +1,12 @@ import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ChatMessage } from "@/types/chat"; +import { ChatHistoriesResponse, ChatMessage, ChatQueryKeys, chatApi } from "./api"; import { useInfiniteQuery } from "@tanstack/react-query"; -export interface ChatHistoriesResponse { - nextPageNumber: number; // 다음 페이지가 없다면 -1 - content: ChatMessage[]; -} - -interface GetChatHistoriesParams { - roomId: number; - size?: number; - page?: number; -} - -const getChatHistories = async ({ - roomId, - size = 20, - page = 0, -}: GetChatHistoriesParams): Promise => { - const res = await axiosInstance.get(`/chats/rooms/${roomId}`, { - params: { - size, - page, - }, - }); - return res.data; -}; - +/** + * @description 채팅 히스토리를 무한 스크롤로 가져오는 훅 + */ const useGetChatHistories = (roomId: number, size: number = 20) => { return useInfiniteQuery< ChatHistoriesResponse, @@ -45,8 +19,8 @@ const useGetChatHistories = (roomId: number, size: number = 20) => { [string, number], number >({ - queryKey: [QueryKeys.chatHistories, roomId], - queryFn: ({ pageParam = 0 }: { pageParam?: number }) => getChatHistories({ roomId, size, page: pageParam }), + queryKey: [ChatQueryKeys.chatHistories, roomId], + queryFn: ({ pageParam = 0 }: { pageParam?: number }) => chatApi.getChatHistories({ roomId, size, page: pageParam }), initialPageParam: 0, getNextPageParam: (lastPage: ChatHistoriesResponse) => { // nextPageNumber가 -1이면 더 이상 페이지가 없음 diff --git a/src/apis/chat/getChatPartner.ts b/src/apis/chat/getChatPartner.ts new file mode 100644 index 00000000..0f871aa8 --- /dev/null +++ b/src/apis/chat/getChatPartner.ts @@ -0,0 +1,19 @@ +import { AxiosError } from "axios"; + +import { ChatPartner, ChatQueryKeys, chatApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 채팅 상대방 정보를 가져오는 훅 + */ +const useGetPartnerInfo = (roomId: number) => { + return useQuery({ + queryKey: [ChatQueryKeys.partnerInfo, roomId], + queryFn: () => chatApi.getChatPartner(roomId), + staleTime: 1000 * 60 * 5, + enabled: !!roomId, + }); +}; + +export default useGetPartnerInfo; diff --git a/src/apis/chat/getChatRooms.ts b/src/apis/chat/getChatRooms.ts new file mode 100644 index 00000000..362583c4 --- /dev/null +++ b/src/apis/chat/getChatRooms.ts @@ -0,0 +1,19 @@ +import { AxiosError } from "axios"; + +import { ChatQueryKeys, ChatRoom, ChatRoomListResponse, chatApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 채팅방 목록을 가져오는 훅 + */ +const useGetChatRooms = () => { + return useQuery({ + queryKey: [ChatQueryKeys.chatRooms], + queryFn: chatApi.getChatRooms, + staleTime: 1000 * 60 * 5, // 5분간 캐시 + select: (data) => data.chatRooms, + }); +}; + +export default useGetChatRooms; diff --git a/src/apis/chat/index.ts b/src/apis/chat/index.ts new file mode 100644 index 00000000..8774747a --- /dev/null +++ b/src/apis/chat/index.ts @@ -0,0 +1,6 @@ +export { chatApi, ChatQueryKeys } from "./api"; +export type { ChatHistoriesResponse, ChatRoomListResponse, ChatMessage, ChatRoom, ChatPartner } from "./api"; +export { default as useGetChatHistories } from "./getChatMessages"; +export { default as useGetPartnerInfo } from "./getChatPartner"; +export { default as useGetChatRooms } from "./getChatRooms"; +export { default as usePutChatRead } from "./putReadChatRoom"; diff --git a/src/apis/chat/putReadChatRoom.ts b/src/apis/chat/putReadChatRoom.ts new file mode 100644 index 00000000..6d955204 --- /dev/null +++ b/src/apis/chat/putReadChatRoom.ts @@ -0,0 +1,25 @@ +import { AxiosError } from "axios"; + +import { ChatQueryKeys, chatApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 채팅방 읽음 처리 훅 + */ +const usePutChatRead = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: chatApi.putReadChatRoom, + onSuccess: () => { + // 채팅방 목록 쿼리를 무효화하여 새로 고침 + queryClient.invalidateQueries({ queryKey: [ChatQueryKeys.chatRooms] }); + }, + onError: (error) => { + console.error("채팅방 진입 읽기 실패", error); + }, + }); +}; + +export default usePutChatRead; diff --git a/src/apis/community/api.ts b/src/apis/community/api.ts new file mode 100644 index 00000000..f57a3851 --- /dev/null +++ b/src/apis/community/api.ts @@ -0,0 +1,154 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; + +import { + CommentCreateRequest, + CommentIdResponse, + ListPost, + Post, + PostCreateRequest, + PostIdResponse, + PostLikeResponse, + PostUpdateRequest, +} from "@/types/community"; + +// QueryKeys for community domain +export const CommunityQueryKeys = { + posts: "posts", + postList: "postList1", // 기존 api/boards와 동일한 키 유지 +} as const; + +export interface BoardListResponse { + 0: string; + 1: string; + 2: string; + 3: string; +} + +export interface BoardResponseItem { + id: number; + title: string; + content: string; + likeCount: number; + commentCount: number; + createdAt: string; + updatedAt: string; + postCategory: string; + postThumbnailUrl: null | string; +} + +export interface BoardResponse { + 0: BoardResponseItem[]; + 1: BoardResponseItem[]; + 2: BoardResponseItem[]; + 3: BoardResponseItem[]; +} + +// Delete response types +export interface DeletePostResponse { + message: string; + postId: number; +} + +// Re-export types from @/types/community for convenience +export type { + Post, + PostCreateRequest, + PostIdResponse, + PostUpdateRequest, + PostLikeResponse, + CommentCreateRequest, + CommentIdResponse, + ListPost, +}; + +export const communityApi = { + /** + * 게시글 목록 조회 (클라이언트) + */ + getPostList: (boardCode: string, category: string | null = null): Promise> => { + const params = category && category !== "전체" ? { category } : {}; + return publicAxiosInstance.get(`/boards/${boardCode}`, { params }); + }, + + getBoardList: async (params?: Record): Promise => { + const res = await axiosInstance.get(`/boards`, { params }); + return res.data; + }, + + getBoard: async (boardCode: string, params?: Record): Promise => { + const res = await axiosInstance.get(`/boards/${boardCode}`, { params }); + return res.data; + }, + + getPostDetail: async (postId: number): Promise => { + const response: AxiosResponse = await axiosInstance.get(`/posts/${postId}`); + return response.data; + }, + + createPost: async (request: PostCreateRequest): Promise => { + const convertedRequest: FormData = new FormData(); + convertedRequest.append( + "postCreateRequest", + new Blob([JSON.stringify(request.postCreateRequest)], { type: "application/json" }), + ); + request.file.forEach((file) => { + convertedRequest.append("file", file); + }); + + const response: AxiosResponse = await axiosInstance.post(`/posts`, convertedRequest, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + return { + ...response.data, + boardCode: request.postCreateRequest.boardCode, + }; + }, + + updatePost: async (postId: number, request: PostUpdateRequest): Promise => { + const convertedRequest: FormData = new FormData(); + convertedRequest.append( + "postUpdateRequest", + new Blob([JSON.stringify(request.postUpdateRequest)], { type: "application/json" }), + ); + request.file.forEach((file) => { + convertedRequest.append("file", file); + }); + + const response: AxiosResponse = await axiosInstance.patch(`/posts/${postId}`, convertedRequest, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return response.data; + }, + + deletePost: async (postId: number): Promise> => { + return axiosInstance.delete(`/posts/${postId}`); + }, + + likePost: async (postId: number): Promise => { + const response: AxiosResponse = await axiosInstance.post(`/posts/${postId}/like`); + return response.data; + }, + + unlikePost: async (postId: number): Promise => { + const response: AxiosResponse = await axiosInstance.delete(`/posts/${postId}/like`); + return response.data; + }, + + createComment: async (request: CommentCreateRequest): Promise => { + const response: AxiosResponse = await axiosInstance.post(`/comments`, request); + return response.data; + }, + + deleteComment: async (commentId: number): Promise => { + const response: AxiosResponse = await axiosInstance.delete(`/comments/${commentId}`); + return response.data; + }, + + updateComment: async (commentId: number, data: { content: string }): Promise => { + const res = await axiosInstance.patch(`/comments/${commentId}`, data); + return res.data; + }, +}; diff --git a/src/apis/community/deleteComment.ts b/src/apis/community/deleteComment.ts new file mode 100644 index 00000000..25bba468 --- /dev/null +++ b/src/apis/community/deleteComment.ts @@ -0,0 +1,33 @@ +import { AxiosError } from "axios"; + +import { CommentIdResponse, CommunityQueryKeys, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +interface DeleteCommentRequest { + commentId: number; + postId: number; +} + +/** + * @description 댓글 삭제를 위한 useMutation 커스텀 훅 + */ +const useDeleteComment = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ commentId }) => communityApi.deleteComment(commentId), + onSuccess: (data, variables) => { + // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); + toast.success("댓글이 삭제되었습니다."); + }, + onError: (error) => { + console.error("댓글 삭제 실패:", error); + toast.error("댓글 삭제에 실패했습니다."); + }, + }); +}; + +export default useDeleteComment; diff --git a/src/apis/community/deleteLikePost.ts b/src/apis/community/deleteLikePost.ts new file mode 100644 index 00000000..295fb6da --- /dev/null +++ b/src/apis/community/deleteLikePost.ts @@ -0,0 +1,27 @@ +import { AxiosError } from "axios"; + +import { CommunityQueryKeys, PostLikeResponse, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 게시글 좋아요 취소를 위한 useMutation 커스텀 훅 + */ +const useDeleteLike = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: communityApi.unlikePost, + onSuccess: (data, postId) => { + // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, postId] }); + }, + onError: (error) => { + console.error("게시글 좋아요 취소 실패:", error); + toast.error("좋아요 취소 처리에 실패했습니다."); + }, + }); +}; + +export default useDeleteLike; diff --git a/src/apis/community/deletePost.ts b/src/apis/community/deletePost.ts new file mode 100644 index 00000000..c1883dd7 --- /dev/null +++ b/src/apis/community/deletePost.ts @@ -0,0 +1,36 @@ +import { useRouter } from "next/navigation"; + +import { AxiosError, AxiosResponse } from "axios"; + +import { CommunityQueryKeys, DeletePostResponse, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 게시글 삭제를 위한 useMutation 커스텀 훅 + */ +const useDeletePost = () => { + const router = useRouter(); + const queryClient = useQueryClient(); + + return useMutation, AxiosError, number>({ + mutationFn: communityApi.deletePost, + onSuccess: () => { + // 'posts' 쿼리 키를 가진 모든 쿼리를 무효화하여 + // 게시글 목록을 다시 불러오도록 합니다. + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] }); + + toast.success("게시글이 성공적으로 삭제되었습니다."); + + // 게시글 목록 페이지 이동 + router.replace("/community/FREE"); + }, + onError: (error) => { + console.error("게시글 삭제 실패:", error); + toast.error("게시글 삭제에 실패했습니다. 잠시 후 다시 시도해주세요."); + }, + }); +}; + +export default useDeletePost; diff --git a/src/apis/community/getBoard.ts b/src/apis/community/getBoard.ts new file mode 100644 index 00000000..27670287 --- /dev/null +++ b/src/apis/community/getBoard.ts @@ -0,0 +1,16 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { BoardResponse, communityApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetBoard = (boardCode: string | number, params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.community.board, boardCode, params], + queryFn: () => communityApi.getBoard(boardCode as string, params), + enabled: !!boardCode, + }); +}; + +export default useGetBoard; diff --git a/src/apis/community/getBoardList.ts b/src/apis/community/getBoardList.ts new file mode 100644 index 00000000..80bbbe69 --- /dev/null +++ b/src/apis/community/getBoardList.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { BoardListResponse, communityApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetBoardList = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.community.boardList, params], + queryFn: () => communityApi.getBoardList(params ? { params } : {}), + }); +}; + +export default useGetBoardList; diff --git a/src/apis/community/getPostDetail.ts b/src/apis/community/getPostDetail.ts new file mode 100644 index 00000000..034c33af --- /dev/null +++ b/src/apis/community/getPostDetail.ts @@ -0,0 +1,18 @@ +import { AxiosError } from "axios"; + +import { CommunityQueryKeys, Post, communityApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 게시글 상세 조회를 위한 useQuery 커스텀 훅 + */ +const useGetPostDetail = (postId: number) => { + return useQuery({ + queryKey: [CommunityQueryKeys.posts, postId], + queryFn: () => communityApi.getPostDetail(postId), + enabled: !!postId, + }); +}; + +export default useGetPostDetail; diff --git a/src/apis/community/getPostList.ts b/src/apis/community/getPostList.ts new file mode 100644 index 00000000..ca8341f7 --- /dev/null +++ b/src/apis/community/getPostList.ts @@ -0,0 +1,31 @@ +import { AxiosResponse } from "axios"; + +import { CommunityQueryKeys, communityApi } from "./api"; + +import { ListPost } from "@/types/community"; + +import { useQuery } from "@tanstack/react-query"; + +interface UseGetPostListProps { + boardCode: string; + category?: string | null; +} + +/** + * @description 게시글 목록 조회 훅 + */ +const useGetPostList = ({ boardCode, category = null }: UseGetPostListProps) => { + return useQuery({ + queryKey: [CommunityQueryKeys.postList, boardCode, category], + queryFn: () => communityApi.getPostList(boardCode, category), + staleTime: Infinity, + gcTime: 1000 * 60 * 30, // 30분 + select: (response) => { + return [...response.data].sort((a, b) => { + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + }); + }, + }); +}; + +export default useGetPostList; diff --git a/src/apis/community/index.ts b/src/apis/community/index.ts new file mode 100644 index 00000000..f847cdc8 --- /dev/null +++ b/src/apis/community/index.ts @@ -0,0 +1,26 @@ +export { communityApi, CommunityQueryKeys } from "./api"; +export type { + Post, + PostCreateRequest, + PostIdResponse, + PostUpdateRequest, + PostLikeResponse, + CommentCreateRequest, + CommentIdResponse, + ListPost, +} from "./api"; +export { default as useDeleteComment } from "./deleteComment"; +export { default as useDeleteLike } from "./deleteLikePost"; +export { default as useDeletePost } from "./deletePost"; +export { default as useGetBoard } from "./getBoard"; +export { default as useGetBoardList } from "./getBoardList"; +export { default as useGetPostDetail } from "./getPostDetail"; +export { default as useGetPostList } from "./getPostList"; +export { default as usePatchUpdateComment } from "./patchUpdateComment"; +export { default as useUpdatePost } from "./patchUpdatePost"; +export { default as useCreateComment } from "./postCreateComment"; +export { default as useCreatePost } from "./postCreatePost"; +export { default as usePostLike } from "./postLikePost"; + +// Server-side functions +export { getPostListServer } from "./server"; diff --git a/src/apis/community/patchUpdateComment.ts b/src/apis/community/patchUpdateComment.ts new file mode 100644 index 00000000..c3416453 --- /dev/null +++ b/src/apis/community/patchUpdateComment.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { CommentIdResponse, communityApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePatchUpdateComment = () => { + return useMutation({ + mutationFn: ({ commentId, content }) => communityApi.updateComment(commentId, { content }), + }); +}; + +export default usePatchUpdateComment; diff --git a/src/apis/community/patchUpdatePost.ts b/src/apis/community/patchUpdatePost.ts new file mode 100644 index 00000000..8923406c --- /dev/null +++ b/src/apis/community/patchUpdatePost.ts @@ -0,0 +1,34 @@ +import { AxiosError } from "axios"; + +import { CommunityQueryKeys, PostIdResponse, PostUpdateRequest, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +interface UpdatePostVariables { + postId: number; + data: PostUpdateRequest; +} + +/** + * @description 게시글 수정을 위한 useMutation 커스텀 훅 + */ +const useUpdatePost = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ postId, data }) => communityApi.updatePost(postId, data), + onSuccess: (result, variables) => { + // 해당 게시글 상세 쿼리와 목록 쿼리를 무효화 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] }); + toast.success("게시글이 수정되었습니다."); + }, + onError: (error) => { + console.error("게시글 수정 실패:", error); + toast.error("게시글 수정에 실패했습니다."); + }, + }); +}; + +export default useUpdatePost; diff --git a/src/apis/community/postCreateComment.ts b/src/apis/community/postCreateComment.ts new file mode 100644 index 00000000..97a28f49 --- /dev/null +++ b/src/apis/community/postCreateComment.ts @@ -0,0 +1,28 @@ +import { AxiosError } from "axios"; + +import { CommentCreateRequest, CommentIdResponse, CommunityQueryKeys, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 댓글 생성을 위한 useMutation 커스텀 훅 + */ +const useCreateComment = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: communityApi.createComment, + onSuccess: (data, variables) => { + // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); + toast.success("댓글이 등록되었습니다."); + }, + onError: (error) => { + console.error("댓글 생성 실패:", error); + toast.error("댓글 등록에 실패했습니다."); + }, + }); +}; + +export default useCreateComment; diff --git a/src/api/community/client/useCreatePost.ts b/src/apis/community/postCreatePost.ts similarity index 57% rename from src/api/community/client/useCreatePost.ts rename to src/apis/community/postCreatePost.ts index 694b4f87..446bd9ee 100644 --- a/src/api/community/client/useCreatePost.ts +++ b/src/apis/community/postCreatePost.ts @@ -1,42 +1,11 @@ -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { PostCreateRequest, PostIdResponse } from "@/types/community"; +import { CommunityQueryKeys, PostCreateRequest, PostIdResponse, communityApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -/** - * @description 게시글 생성 API 함수 - * @param request - 게시글 생성 요청 데이터 - * @returns Promise - */ -const createPost = async ( - request: PostCreateRequest -): Promise => { - const convertedRequest: FormData = new FormData(); - convertedRequest.append( - "postCreateRequest", - new Blob([JSON.stringify(request.postCreateRequest)], { type: "application/json" }) - ); - request.file.forEach((file) => { - convertedRequest.append("file", file); - }); - - const response: AxiosResponse = await axiosInstance.post(`/posts`, convertedRequest, { - headers: { "Content-Type": "multipart/form-data" }, - }); - - return { - ...response.data, - boardCode: request.postCreateRequest.boardCode, - }; -}; - /** * @description ISR 페이지를 revalidate하는 함수 * @param boardCode - 게시판 코드 @@ -69,17 +38,17 @@ const useCreatePost = () => { const queryClient = useQueryClient(); const { accessToken } = useAuthStore(); - return useMutation({ - mutationFn: createPost, + return useMutation({ + mutationFn: communityApi.createPost, onSuccess: async (data) => { // 게시글 목록 쿼리를 무효화하여 최신 목록 반영 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts] }); - + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] }); + // ISR 페이지 revalidate (사용자 인증 토큰 사용) if (accessToken) { await revalidateCommunityPage(data.boardCode, accessToken); } - + toast.success("게시글이 등록되었습니다."); }, onError: (error) => { diff --git a/src/apis/community/postLikePost.ts b/src/apis/community/postLikePost.ts new file mode 100644 index 00000000..2591446d --- /dev/null +++ b/src/apis/community/postLikePost.ts @@ -0,0 +1,27 @@ +import { AxiosError } from "axios"; + +import { CommunityQueryKeys, PostLikeResponse, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 게시글 좋아요를 위한 useMutation 커스텀 훅 + */ +const usePostLike = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: communityApi.likePost, + onSuccess: (data, postId) => { + // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, postId] }); + }, + onError: (error) => { + console.error("게시글 좋아요 실패:", error); + toast.error("좋아요 처리에 실패했습니다."); + }, + }); +}; + +export default usePostLike; diff --git a/src/api/boards/server/getPostList.ts b/src/apis/community/server.ts similarity index 74% rename from src/api/boards/server/getPostList.ts rename to src/apis/community/server.ts index 9d555d6c..91da59e3 100644 --- a/src/api/boards/server/getPostList.ts +++ b/src/apis/community/server.ts @@ -15,13 +15,12 @@ interface GetPostListParams { * @param revalidate - ISR revalidate 시간(초) 또는 false (무한 캐시) * @returns Promise> */ -export const getPostList = async ({ +export const getPostListServer = async ({ boardCode, category = null, - revalidate = false, // 기본값: 자동 재생성 비활성화 (수동 revalidate만) + revalidate = false, }: GetPostListParams): Promise> => { const params = new URLSearchParams(); - // "전체"는 필터 없음을 의미하므로 파라미터에 포함하지 않음 if (category && category !== "전체") { params.append("category", category); } @@ -32,9 +31,8 @@ export const getPostList = async ({ return serverFetch(url, { method: "GET", next: { - revalidate, - tags: [`posts-${boardCode}`], // 태그 기반 revalidation 지원 (글 작성 시만 revalidate) + ...(revalidate !== false && { revalidate }), + tags: [`posts-${boardCode}`], }, }); }; - diff --git a/src/apis/image-upload/api.ts b/src/apis/image-upload/api.ts new file mode 100644 index 00000000..f93f1490 --- /dev/null +++ b/src/apis/image-upload/api.ts @@ -0,0 +1,83 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; + +import { FileResponse } from "@/types/file"; + +// ====== Types ====== +export type SlackNotificationResponse = void; +export type SlackNotificationRequest = Record; + +export interface UploadLanguageTestReportResponse { + fileUrl: string; +} + +export interface UploadProfileImageResponse { + fileUrl: string; +} + +export interface UploadGpaReportResponse { + fileUrl: string; +} + +// ====== API Functions ====== +export const imageUploadApi = { + /** + * 슬랙 알림 전송 + */ + postSlackNotification: async (params: { data?: SlackNotificationRequest }): Promise => { + const res = await axiosInstance.post( + `https://hooks.slack.com/services/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`, + params?.data, + ); + return res.data; + }, + + /** + * 어학 성적 증명서 업로드 + */ + postUploadLanguageTestReport: async (file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const res = await axiosInstance.post(`/file/language-test`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data; + }, + + /** + * 프로필 이미지 업로드 (로그인 후) + */ + postUploadProfileImage: async (file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const res = await axiosInstance.post(`/file/profile/post`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data; + }, + + /** + * 프로필 이미지 업로드 (회원가입 전, 공개 API) + */ + postUploadProfileImageBeforeSignup: async (file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const response: AxiosResponse = await publicAxiosInstance.post("/file/profile/pre", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return response.data; + }, + + /** + * 학점 증명서 업로드 + */ + postUploadGpaReport: async (file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const res = await axiosInstance.post(`/file/gpa`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data; + }, +}; diff --git a/src/apis/image-upload/index.ts b/src/apis/image-upload/index.ts new file mode 100644 index 00000000..83a2df4e --- /dev/null +++ b/src/apis/image-upload/index.ts @@ -0,0 +1,8 @@ +export { imageUploadApi } from "./api"; +export type { UploadLanguageTestReportResponse, UploadProfileImageResponse, UploadGpaReportResponse } from "./api"; + +export { default as useSlackNotification } from "./postSlackNotification"; +export { default as useUploadGpaReport } from "./postUploadGpaReport"; +export { default as useUploadLanguageTestReport } from "./postUploadLanguageTestReport"; +export { default as useUploadProfileImage } from "./postUploadProfileImage"; +export { default as useUploadProfileImagePublic } from "./postUploadProfileImageBeforeSignup"; diff --git a/src/apis/image-upload/postSlackNotification.ts b/src/apis/image-upload/postSlackNotification.ts new file mode 100644 index 00000000..c5ea6962 --- /dev/null +++ b/src/apis/image-upload/postSlackNotification.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { SlackNotificationRequest, SlackNotificationResponse, imageUploadApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostSlackNotification = () => { + return useMutation({ + mutationFn: (data) => imageUploadApi.postSlackNotification({ data }), + }); +}; + +export default usePostSlackNotification; diff --git a/src/apis/image-upload/postUploadGpaReport.ts b/src/apis/image-upload/postUploadGpaReport.ts new file mode 100644 index 00000000..f8ffa4c8 --- /dev/null +++ b/src/apis/image-upload/postUploadGpaReport.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { UploadGpaReportResponse, imageUploadApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostUploadGpaReport = () => { + return useMutation({ + mutationFn: (file) => imageUploadApi.postUploadGpaReport(file), + }); +}; + +export default usePostUploadGpaReport; diff --git a/src/apis/image-upload/postUploadLanguageTestReport.ts b/src/apis/image-upload/postUploadLanguageTestReport.ts new file mode 100644 index 00000000..27002711 --- /dev/null +++ b/src/apis/image-upload/postUploadLanguageTestReport.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { UploadLanguageTestReportResponse, imageUploadApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostUploadLanguageTestReport = () => { + return useMutation({ + mutationFn: (file) => imageUploadApi.postUploadLanguageTestReport(file), + }); +}; + +export default usePostUploadLanguageTestReport; diff --git a/src/apis/image-upload/postUploadProfileImage.ts b/src/apis/image-upload/postUploadProfileImage.ts new file mode 100644 index 00000000..06a34553 --- /dev/null +++ b/src/apis/image-upload/postUploadProfileImage.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { UploadProfileImageResponse, imageUploadApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostUploadProfileImage = () => { + return useMutation({ + mutationFn: (file) => imageUploadApi.postUploadProfileImage(file), + }); +}; + +export default usePostUploadProfileImage; diff --git a/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts b/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts new file mode 100644 index 00000000..3e7bfc30 --- /dev/null +++ b/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts @@ -0,0 +1,23 @@ +import { AxiosError } from "axios"; + +import { imageUploadApi } from "./api"; + +import { FileResponse } from "@/types/file"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 프로필 이미지 업로드를 위한 useMutation 커스텀 훅 (회원가입 전 공개 API) + */ +const useUploadProfileImagePublic = () => { + return useMutation({ + mutationFn: imageUploadApi.postUploadProfileImageBeforeSignup, + onError: (error) => { + console.error("프로필 이미지 업로드 실패:", error); + toast.error("이미지 업로드에 실패했습니다."); + }, + }); +}; + +export default useUploadProfileImagePublic; diff --git a/src/apis/kakao-api/api.ts b/src/apis/kakao-api/api.ts new file mode 100644 index 00000000..54b9cfac --- /dev/null +++ b/src/apis/kakao-api/api.ts @@ -0,0 +1,34 @@ +import { axiosInstance } from "@/utils/axiosInstance"; + +export type KakaoUserIdsResponse = void; + +export type KakaoUnlinkResponse = void; + +export type KakaoUnlinkRequest = Record; + +export type KakaoInfoResponse = void; + +export const kakaoApiApi = { + getKakaoUserIds: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`https://kapi.kakao.com/v1/user/ids?order=dsc`, { + params: params?.params, + }); + return res.data; + }, + + postKakaoUnlink: async (params: { data?: KakaoUnlinkRequest }): Promise => { + const res = await axiosInstance.post( + `https://kapi.kakao.com/v1/user/unlink?target_id_type=user_id&target_id=3715136239`, + params?.data, + ); + return res.data; + }, + + getKakaoInfo: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get( + `https://kapi.kakao.com/v2/user/me?property_keys=["kakao_account.email"]&target_id_type=user_id&target_id=3715136239`, + { params: params?.params }, + ); + return res.data; + }, +}; diff --git a/src/apis/kakao-api/getKakaoInfo.ts b/src/apis/kakao-api/getKakaoInfo.ts new file mode 100644 index 00000000..53a4da37 --- /dev/null +++ b/src/apis/kakao-api/getKakaoInfo.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { KakaoInfoResponse, kakaoApiApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetKakaoInfo = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys["kakao-api"].kakaoInfo, params], + queryFn: () => kakaoApiApi.getKakaoInfo(params ? { params } : {}), + }); +}; + +export default useGetKakaoInfo; diff --git a/src/apis/kakao-api/getKakaoUserIds.ts b/src/apis/kakao-api/getKakaoUserIds.ts new file mode 100644 index 00000000..a6c1e6bd --- /dev/null +++ b/src/apis/kakao-api/getKakaoUserIds.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { KakaoUserIdsResponse, kakaoApiApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetKakaoUserIds = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys["kakao-api"].kakaoUserIds, params], + queryFn: () => kakaoApiApi.getKakaoUserIds(params ? { params } : {}), + }); +}; + +export default useGetKakaoUserIds; diff --git a/src/apis/kakao-api/index.ts b/src/apis/kakao-api/index.ts new file mode 100644 index 00000000..0acb2db7 --- /dev/null +++ b/src/apis/kakao-api/index.ts @@ -0,0 +1,4 @@ +export { kakaoApiApi } from "./api"; +export { default as getKakaoInfo } from "./getKakaoInfo"; +export { default as getKakaoUserIds } from "./getKakaoUserIds"; +export { default as postKakaoUnlink } from "./postKakaoUnlink"; diff --git a/src/apis/kakao-api/postKakaoUnlink.ts b/src/apis/kakao-api/postKakaoUnlink.ts new file mode 100644 index 00000000..268eae6c --- /dev/null +++ b/src/apis/kakao-api/postKakaoUnlink.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { KakaoUnlinkRequest, KakaoUnlinkResponse, kakaoApiApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostKakaoUnlink = () => { + return useMutation({ + mutationFn: (data) => kakaoApiApi.postKakaoUnlink({ data }), + }); +}; + +export default usePostKakaoUnlink; diff --git a/src/apis/mentor/api.ts b/src/apis/mentor/api.ts new file mode 100644 index 00000000..6a8ee150 --- /dev/null +++ b/src/apis/mentor/api.ts @@ -0,0 +1,194 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { MentoringListItem, VerifyStatus } from "@/types/mentee"; +import { MentorCardDetail, MentorCardPreview, MentoringApprovalStatus, MentoringItem } from "@/types/mentor"; + +// QueryKeys for mentor domain +export const MentorQueryKeys = { + myMentorProfile: "myMentorProfile", + mentoringList: "mentoringList", + mentoringNewCount: "mentoringNewCount", + applyMentoringList: "applyMentoringList", + mentorList: "mentorList", + mentorDetail: "mentorDetail", +} as const; + +// Re-export types +export type { MentorCardPreview, MentorCardDetail, MentoringItem }; +export type { MentoringListItem, VerifyStatus }; +export { MentoringApprovalStatus }; + +// Response types +export interface MentoringListResponse { + content: MentoringItem[]; + nextPageNumber: number; +} + +export interface GetMentoringNewCountResponse { + uncheckedCount: number; +} + +export interface ApplyMentoringListResponse { + content: MentoringListItem[]; + nextPageNumber: number; +} + +export interface MentorListResponse { + nextPageNumber: number; + content: MentorCardDetail[]; +} + +export interface MatchedMentorsResponse { + content: MentorCardDetail[]; + nextPageNumber: number; + totalElements: number; +} + +export interface PatchApprovalStatusRequest { + status: MentoringApprovalStatus; + mentoringId: number; +} + +export interface PatchApprovalStatusResponse { + mentoringId: number; + chatRoomId: number; +} + +export interface PatchCheckMentoringsRequest { + checkedMentoringIds: number[]; +} + +export interface PatchCheckMentoringsResponse { + checkedMentoringIds: number[]; +} + +export interface PostApplyMentoringRequest { + mentorId: number; +} + +export interface PostApplyMentoringResponse { + mentoringId: number; +} + +export interface PostMentorApplicationRequest { + interestedCountries: string[]; + country: string; + universityName: string; + studyStatus: "STUDYING" | "PLANNING" | "COMPLETED"; + verificationFile: File; +} + +export interface PutMyMentorProfileRequest { + channels: { type: string; url: string }[]; + passTip: string; + introduction: string; +} + +const OFFSET = 5; +const MENTORS_OFFSET = 10; +const MENTEE_OFFSET = 3; + +export const mentorApi = { + // === Mentor (멘토) APIs === + getMentorMyProfile: async (): Promise => { + const res = await axiosInstance.get("/mentor/my"); + return res.data; + }, + + getMentoringList: async (page: number, size: number = OFFSET): Promise => { + const endpoint = `/mentor/mentorings?size=${size}&page=${page}`; + const res = await axiosInstance.get(endpoint); + return res.data; + }, + + getMentoringUncheckedCount: async (): Promise => { + const endpoint = "/mentor/mentorings/check"; + const res = await axiosInstance.get(endpoint); + return res.data; + }, + + patchApprovalStatus: async (props: PatchApprovalStatusRequest): Promise => { + const { status, mentoringId } = props; + const res = await axiosInstance.patch(`/mentor/mentorings/${mentoringId}`, { + status, + }); + return res.data; + }, + + patchMentorCheckMentorings: async (body: PatchCheckMentoringsRequest): Promise => { + const res = await axiosInstance.patch("/mentor/mentorings/check", body); + return res.data; + }, + + postMentorApplication: async (body: PostMentorApplicationRequest): Promise => { + const formData = new FormData(); + const applicationData = { + interestedCountries: body.interestedCountries, + country: body.country, + universityName: body.universityName, + studyStatus: body.studyStatus, + }; + formData.append( + "mentorApplicationRequest", + new Blob([JSON.stringify(applicationData)], { type: "application/json" }), + ); + formData.append("file", body.verificationFile); + const res = await axiosInstance.post("/mentor/verification", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data; + }, + + putMyMentorProfile: async (body: PutMyMentorProfileRequest): Promise => { + const res = await axiosInstance.put("/mentor/my", body); + return res.data; + }, + + // === Mentee (멘티) APIs === + getApplyMentoringList: async ( + verifyStatus: VerifyStatus, + page: number, + size: number = MENTEE_OFFSET, + ): Promise => { + const res = await axiosInstance.get( + `/mentee/mentorings?verify-status=${verifyStatus}&size=${size}&page=${page}`, + ); + return res.data; + }, + + patchMenteeCheckMentorings: async (body: PatchCheckMentoringsRequest): Promise => { + const res = await axiosInstance.patch("/mentee/mentorings/check", body); + return res.data; + }, + + postApplyMentoring: async (body: PostApplyMentoringRequest): Promise => { + const res = await axiosInstance.post("/mentee/mentorings", body); + return res.data; + }, + + // === Mentors (멘토 목록) APIs === + getMentorList: async (region: string, page: number, size: number = MENTORS_OFFSET): Promise => { + const res = await axiosInstance.get(`/mentors?region=${region}&page=${page}&size=${size}`); + return res.data; + }, + + getMentorDetail: async (mentorId: number): Promise => { + const res = await axiosInstance.get(`/mentors/${mentorId}`); + return res.data; + }, + + getMatchedMentors: async (params: { + defaultSize: string | number; + defaultPage: string | number; + params?: Record; + }): Promise => { + const { defaultSize, defaultPage, params: queryParams } = params; + const res = await axiosInstance.get( + `/mentors/matched?size=${defaultSize}&page=${defaultPage}`, + { params: queryParams }, + ); + return res.data; + }, +}; diff --git a/src/apis/mentor/getAppliedMentorings.ts b/src/apis/mentor/getAppliedMentorings.ts new file mode 100644 index 00000000..2d6d516f --- /dev/null +++ b/src/apis/mentor/getAppliedMentorings.ts @@ -0,0 +1,38 @@ +import { AxiosError } from "axios"; + +import { ApplyMentoringListResponse, MentorQueryKeys, MentoringListItem, VerifyStatus, mentorApi } from "./api"; + +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; +import type { QueryFunctionContext } from "@tanstack/react-query"; + +/** + * @description 신청한 멘토링 목록 조회 훅 (무한 스크롤) + */ +const useGetApplyMentoringList = (verifyStatus: VerifyStatus) => { + return useInfiniteQuery({ + queryKey: [MentorQueryKeys.applyMentoringList, verifyStatus], + queryFn: ({ pageParam = 0 }) => mentorApi.getApplyMentoringList(verifyStatus, pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => (lastPage.nextPageNumber === -1 ? undefined : lastPage.nextPageNumber), + staleTime: 1000 * 60 * 5, // 5분간 캐시 + select: (data) => data.pages.flatMap((p) => p.content), + }); +}; + +// 멘토링 리스트 프리페치용 훅 +export const usePrefetchApplyMentoringList = () => { + const queryClient = useQueryClient(); + + const prefetchList = (verifyStatus: VerifyStatus) => { + queryClient.prefetchInfiniteQuery({ + queryKey: [MentorQueryKeys.applyMentoringList, verifyStatus], + queryFn: ({ pageParam = 0 }) => mentorApi.getApplyMentoringList(verifyStatus, pageParam as number), + initialPageParam: 0, + staleTime: 1000 * 60 * 5, + }); + }; + + return { prefetchList }; +}; + +export default useGetApplyMentoringList; diff --git a/src/apis/mentor/getMatchedMentors.ts b/src/apis/mentor/getMatchedMentors.ts new file mode 100644 index 00000000..46402b2d --- /dev/null +++ b/src/apis/mentor/getMatchedMentors.ts @@ -0,0 +1,20 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { MatchedMentorsResponse, mentorApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetMatchedMentors = ( + defaultSize: string | number, + defaultPage: string | number, + params?: Record, +) => { + return useQuery({ + queryKey: [QueryKeys.mentor.matchedMentors, defaultSize, defaultPage, params], + queryFn: () => mentorApi.getMatchedMentors({ defaultSize, defaultPage, params }), + enabled: !!defaultSize && !!defaultPage, + }); +}; + +export default useGetMatchedMentors; diff --git a/src/apis/mentor/getMentorDetail.ts b/src/apis/mentor/getMentorDetail.ts new file mode 100644 index 00000000..6a689866 --- /dev/null +++ b/src/apis/mentor/getMentorDetail.ts @@ -0,0 +1,19 @@ +import { AxiosError } from "axios"; + +import { MentorCardDetail, MentorQueryKeys, mentorApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 멘토 상세 조회 훅 + */ +const useGetMentorDetail = (mentorId: number | null) => { + return useQuery({ + queryKey: [MentorQueryKeys.mentorDetail, mentorId!], + queryFn: () => mentorApi.getMentorDetail(mentorId!), + enabled: mentorId !== null, + staleTime: 1000 * 60 * 5, // 5분간 캐시 + }); +}; + +export default useGetMentorDetail; diff --git a/src/apis/mentor/getMentorList.ts b/src/apis/mentor/getMentorList.ts new file mode 100644 index 00000000..13f64f6c --- /dev/null +++ b/src/apis/mentor/getMentorList.ts @@ -0,0 +1,42 @@ +import { AxiosError } from "axios"; + +import { MentorCardDetail, MentorListResponse, MentorQueryKeys, mentorApi } from "./api"; + +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; +import type { QueryFunctionContext } from "@tanstack/react-query"; + +interface UseGetMentorListRequest { + region?: string; +} + +/** + * @description 멘토 목록 조회 훅 (무한 스크롤) + */ +const useGetMentorList = ({ region = "" }: UseGetMentorListRequest = {}) => { + return useInfiniteQuery({ + queryKey: [MentorQueryKeys.mentorList, region], + queryFn: ({ pageParam = 0 }) => mentorApi.getMentorList(region, pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => (lastPage.nextPageNumber === -1 ? undefined : lastPage.nextPageNumber), + staleTime: 1000 * 60 * 5, + select: (data) => data.pages.flatMap((p) => p.content), + }); +}; + +// 탭 프리페치용 훅 +export const usePrefetchMentorList = () => { + const queryClient = useQueryClient(); + + const prefetchMentorList = (region: string) => { + queryClient.prefetchInfiniteQuery({ + queryKey: [MentorQueryKeys.mentorList, region], + queryFn: ({ pageParam = 0 }) => mentorApi.getMentorList(region, pageParam as number), + initialPageParam: 0, + staleTime: 1000 * 60 * 5, + }); + }; + + return { prefetchMentorList }; +}; + +export default useGetMentorList; diff --git a/src/apis/mentor/getMyMentorPage.ts b/src/apis/mentor/getMyMentorPage.ts new file mode 100644 index 00000000..09d2508c --- /dev/null +++ b/src/apis/mentor/getMyMentorPage.ts @@ -0,0 +1,18 @@ +import { AxiosError } from "axios"; + +import { MentorCardPreview, MentorQueryKeys, mentorApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 멘토 마이 프로필 조회 훅 + */ +const useGetMentorMyProfile = () => { + return useQuery({ + queryKey: [MentorQueryKeys.myMentorProfile], + queryFn: mentorApi.getMentorMyProfile, + staleTime: 1000 * 60 * 5, // 5분간 캐시 + }); +}; + +export default useGetMentorMyProfile; diff --git a/src/apis/mentor/getReceivedMentorings.ts b/src/apis/mentor/getReceivedMentorings.ts new file mode 100644 index 00000000..a193248b --- /dev/null +++ b/src/apis/mentor/getReceivedMentorings.ts @@ -0,0 +1,26 @@ +import { AxiosError } from "axios"; + +import { MentorQueryKeys, MentoringItem, MentoringListResponse, mentorApi } from "./api"; + +import { useInfiniteQuery } from "@tanstack/react-query"; + +const OFFSET = 5; + +/** + * @description 받은 멘토링 목록 조회 훅 (무한 스크롤) + */ +const useGetMentoringList = ({ size = OFFSET }: { size?: number } = {}) => { + return useInfiniteQuery({ + queryKey: [MentorQueryKeys.mentoringList, size], + queryFn: ({ pageParam = 0 }) => mentorApi.getMentoringList(pageParam, size), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + return lastPage.nextPageNumber !== -1 ? lastPage.nextPageNumber : undefined; + }, + refetchInterval: 1000 * 60 * 10, // ⏱️ 10분마다 자동 재요청 + staleTime: 1000 * 60 * 5, // fresh 상태 유지 + select: (data) => data.pages.flatMap((page) => page.content), + }); +}; + +export default useGetMentoringList; diff --git a/src/apis/mentor/getUnconfirmedMentoringCount.ts b/src/apis/mentor/getUnconfirmedMentoringCount.ts new file mode 100644 index 00000000..9a1d4cc5 --- /dev/null +++ b/src/apis/mentor/getUnconfirmedMentoringCount.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { GetMentoringNewCountResponse, MentorQueryKeys, mentorApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 미확인 멘토링 수 조회 훅 + */ +const useGetMentoringUncheckedCount = (isEnable: boolean) => { + return useQuery({ + queryKey: [MentorQueryKeys.mentoringNewCount], + queryFn: mentorApi.getMentoringUncheckedCount, + enabled: isEnable, + refetchInterval: 1000 * 60 * 10, // ⏱️ 10분마다 자동 재요청 + staleTime: 1000 * 60 * 5, // fresh 상태 유지 + select: (data) => data.uncheckedCount, + }); +}; + +export default useGetMentoringUncheckedCount; diff --git a/src/apis/mentor/index.ts b/src/apis/mentor/index.ts new file mode 100644 index 00000000..cd1e34f2 --- /dev/null +++ b/src/apis/mentor/index.ts @@ -0,0 +1,29 @@ +export { mentorApi, MentorQueryKeys } from "./api"; +export type { + MentorCardPreview, + MentorCardDetail, + MentoringItem, + MentoringApprovalStatus, + MentoringListItem, + VerifyStatus, + PutMyMentorProfileRequest, + PostMentorApplicationRequest, +} from "./api"; + +// Mentor (멘토) hooks +export { default as useGetMentorMyProfile } from "./getMyMentorPage"; +export { default as useGetMentoringList } from "./getReceivedMentorings"; +export { default as useGetMentoringUncheckedCount } from "./getUnconfirmedMentoringCount"; +export { default as usePatchApprovalStatus } from "./patchMentoringStatus"; +export { default as usePatchMentorCheckMentorings } from "./patchConfirmMentoring"; +export { default as usePostMentorApplication } from "./postMentorApplication"; +export { default as usePutMyMentorProfile } from "./putUpdateMyMentorPage"; + +// Mentee (멘티) hooks +export { default as useGetApplyMentoringList, usePrefetchApplyMentoringList } from "./getAppliedMentorings"; +export { default as usePatchMenteeCheckMentorings } from "./patchMenteeCheckMentorings"; +export { default as usePostApplyMentoring } from "./postApplyMentoring"; + +// Mentors (멘토 목록) hooks +export { default as useGetMentorList, usePrefetchMentorList } from "./getMentorList"; +export { default as useGetMentorDetail } from "./getMentorDetail"; diff --git a/src/apis/mentor/patchConfirmMentoring.ts b/src/apis/mentor/patchConfirmMentoring.ts new file mode 100644 index 00000000..f61c2875 --- /dev/null +++ b/src/apis/mentor/patchConfirmMentoring.ts @@ -0,0 +1,24 @@ +import { AxiosError } from "axios"; + +import { MentorQueryKeys, PatchCheckMentoringsRequest, PatchCheckMentoringsResponse, mentorApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 멘토 멘토링 확인 처리 훅 + */ +const usePatchMentorCheckMentorings = () => { + const queriesClient = useQueryClient(); + return useMutation({ + onSuccess: () => { + // 멘토링 체크 상태 변경 후 멘토링 목록 쿼리 무효화 + Promise.all([ + queriesClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringList] }), + queriesClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringNewCount] }), + ]); + }, + mutationFn: mentorApi.patchMentorCheckMentorings, + }); +}; + +export default usePatchMentorCheckMentorings; diff --git a/src/apis/mentor/patchMenteeCheckMentorings.ts b/src/apis/mentor/patchMenteeCheckMentorings.ts new file mode 100644 index 00000000..4308b11c --- /dev/null +++ b/src/apis/mentor/patchMenteeCheckMentorings.ts @@ -0,0 +1,16 @@ +import { AxiosError } from "axios"; + +import { PatchCheckMentoringsRequest, PatchCheckMentoringsResponse, mentorApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 멘티 멘토링 확인 처리 훅 + */ +const usePatchMenteeCheckMentorings = () => { + return useMutation({ + mutationFn: mentorApi.patchMenteeCheckMentorings, + }); +}; + +export default usePatchMenteeCheckMentorings; diff --git a/src/api/mentor/client/usePatchApprovalStatus.ts b/src/apis/mentor/patchMentoringStatus.ts similarity index 64% rename from src/api/mentor/client/usePatchApprovalStatus.ts rename to src/apis/mentor/patchMentoringStatus.ts index 50bb68c4..1594dbd7 100644 --- a/src/api/mentor/client/usePatchApprovalStatus.ts +++ b/src/apis/mentor/patchMentoringStatus.ts @@ -1,44 +1,34 @@ import { useRouter } from "next/navigation"; -import { axiosInstance } from "@/utils/axiosInstance"; +import { AxiosError } from "axios"; -import { QueryKeys } from "./queryKey"; - -import { MentoringApprovalStatus } from "@/types/mentor"; +import { + MentorQueryKeys, + MentoringApprovalStatus, + PatchApprovalStatusRequest, + PatchApprovalStatusResponse, + mentorApi, +} from "./api"; import { customAlert } from "@/lib/zustand/useAlertModalStore"; import { customConfirm } from "@/lib/zustand/useConfirmModalStore"; import { IconSmile, IconUnSmile } from "@/public/svgs/mentor"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -interface UsePatchApprovalStatusRequest { - status: MentoringApprovalStatus; - mentoringId: number; // 멘토링 ID -} -interface UsePatchApprovalStatusResponse { - mentoringId: number; // 멘토링 ID - chatRoomId: number; // 채팅방 ID -} - -const patchApprovalStatus = async (props: UsePatchApprovalStatusRequest): Promise => { - const { status, mentoringId } = props; - const res = await axiosInstance.patch(`/mentor/mentorings/${mentoringId}`, { - status, - }); - return res.data; -}; - +/** + * @description 멘토링 승인/거절 훅 + */ const usePatchApprovalStatus = () => { const router = useRouter(); const queryClient = useQueryClient(); - return useMutation({ - mutationFn: patchApprovalStatus, + return useMutation({ + mutationFn: mentorApi.patchApprovalStatus, onSuccess: async (data, variables) => { // 멘토링 상태 변경 후 쿼리 무효화 await Promise.all([ - queryClient.invalidateQueries({ queryKey: [QueryKeys.mentoringList] }), - queryClient.invalidateQueries({ queryKey: [QueryKeys.mentoringNewCount] }), + queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringList] }), + queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringNewCount] }), ]); if (variables.status === MentoringApprovalStatus.REJECTED) { diff --git a/src/apis/mentor/postApplyMentoring.ts b/src/apis/mentor/postApplyMentoring.ts new file mode 100644 index 00000000..7d70022e --- /dev/null +++ b/src/apis/mentor/postApplyMentoring.ts @@ -0,0 +1,25 @@ +import { AxiosError } from "axios"; + +import { MentorQueryKeys, PostApplyMentoringRequest, PostApplyMentoringResponse, mentorApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 멘토링 신청 훅 + */ +const usePostApplyMentoring = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: mentorApi.postApplyMentoring, + onSuccess: async () => { + // 멘토링 신청 후 멘토 목록을 새로고침 + await queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.applyMentoringList] }); + }, + onError: () => { + toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostApplyMentoring; diff --git a/src/apis/mentor/postMentorApplication.ts b/src/apis/mentor/postMentorApplication.ts new file mode 100644 index 00000000..2ef9acf3 --- /dev/null +++ b/src/apis/mentor/postMentorApplication.ts @@ -0,0 +1,20 @@ +import { AxiosError } from "axios"; + +import { PostMentorApplicationRequest, mentorApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 멘토 신청 훅 + */ +const usePostMentorApplication = () => { + return useMutation({ + mutationFn: mentorApi.postMentorApplication, + onError: (error) => { + toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostMentorApplication; diff --git a/src/apis/mentor/putUpdateMyMentorPage.ts b/src/apis/mentor/putUpdateMyMentorPage.ts new file mode 100644 index 00000000..23164a8a --- /dev/null +++ b/src/apis/mentor/putUpdateMyMentorPage.ts @@ -0,0 +1,24 @@ +import { AxiosError } from "axios"; + +import { MentorQueryKeys, PutMyMentorProfileRequest, mentorApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 내 멘토 프로필 수정 훅 + */ +const usePutMyMentorProfile = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: mentorApi.putMyMentorProfile, + onSuccess: () => { + // 멘토 프로필 데이터를 stale로 만들어 다음 요청 시 새로운 데이터를 가져오도록 함 + queryClient.invalidateQueries({ + queryKey: [MentorQueryKeys.myMentorProfile], + }); + }, + }); +}; + +export default usePutMyMentorProfile; diff --git a/src/apis/news/api.ts b/src/apis/news/api.ts new file mode 100644 index 00000000..8266572f --- /dev/null +++ b/src/apis/news/api.ts @@ -0,0 +1,109 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { ArticleFormData } from "@/components/mentor/ArticleBottomSheetModal/lib/schema"; + +import { Article } from "@/types/news"; + +// ====== Query Keys ====== +export const NewsQueryKeys = { + articleList: "articleList", + postAddArticle: "postAddArticle", + putModifyArticle: "putModifyArticle", +} as const; + +// ====== Types ====== +export interface ArticleListResponse { + newsResponseList: Article[]; +} + +export interface PostArticleLikeResponse { + isLiked: boolean; + likeCount: number; +} + +export interface DeleteArticleLikeResponse { + isLiked: boolean; + likeCount: number; +} + +export type UsePostAddArticleRequest = ArticleFormData; + +export type UsePutModifyArticleRequest = { + body: ArticleFormData & { isImageDeleted?: boolean }; + articleId: number; +}; + +// ====== API Functions ====== +export const newsApi = { + /** + * 아티클 목록 조회 + */ + getArticleList: async (userId: number): Promise => { + const response: AxiosResponse = await axiosInstance.get(`/news?author-id=${userId}`); + return response.data; + }, + + /** + * 아티클 추가 + */ + postAddArticle: async (body: UsePostAddArticleRequest): Promise
=> { + const newsCreateRequest = { + title: body.title, + description: body.description, + url: body.url || "", + }; + + const formData = new FormData(); + formData.append("newsCreateRequest", new Blob([JSON.stringify(newsCreateRequest)], { type: "application/json" })); + if (body.file) { + formData.append("file", body.file); + } + const response: AxiosResponse
= await axiosInstance.post("/news", formData); + return response.data; + }, + + /** + * 아티클 수정 + */ + putModifyArticle: async (props: UsePutModifyArticleRequest): Promise
=> { + const { body, articleId } = props; + const newsUpdateRequest = { + title: body.title, + description: body.description, + url: body.url || "", + resetToDefaultImage: body.isImageDeleted === true, + }; + const formData = new FormData(); + formData.append("newsUpdateRequest", new Blob([JSON.stringify(newsUpdateRequest)], { type: "application/json" })); + if (body.file) formData.append("file", body.file); + + const response: AxiosResponse
= await axiosInstance.put(`/news/${articleId}`, formData); + return response.data; + }, + + /** + * 아티클 삭제 + */ + deleteArticle: async (articleId: number): Promise => { + const response: AxiosResponse = await axiosInstance.delete(`/news/${articleId}`); + return response.data; + }, + + /** + * 아티클 좋아요 + */ + postArticleLike: async (articleId: number): Promise => { + const response: AxiosResponse = await axiosInstance.post(`/news/${articleId}/like`); + return response.data; + }, + + /** + * 아티클 좋아요 취소 + */ + deleteArticleLike: async (articleId: number): Promise => { + const response: AxiosResponse = await axiosInstance.delete(`/news/${articleId}/like`); + return response.data; + }, +}; diff --git a/src/apis/news/deleteLikeNews.ts b/src/apis/news/deleteLikeNews.ts new file mode 100644 index 00000000..c6a58371 --- /dev/null +++ b/src/apis/news/deleteLikeNews.ts @@ -0,0 +1,54 @@ +import { AxiosError } from "axios"; + +import { ArticleListResponse, DeleteArticleLikeResponse, NewsQueryKeys, newsApi } from "./api"; + +import { Article } from "@/types/news"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +type ArticleLikeMutationContext = { + previousArticleList?: ArticleListResponse; +}; + +/** + * @description 아티클 좋아요 취소 훅 + */ +const useDeleteArticleLike = (userId: number | null) => { + const queryClient = useQueryClient(); + const queryKey = [NewsQueryKeys.articleList, userId]; + + return useMutation, number, ArticleLikeMutationContext>({ + mutationFn: newsApi.deleteArticleLike, + + onMutate: async (unlikedArticleId) => { + await queryClient.cancelQueries({ queryKey }); + + const previousArticleList = queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, (oldData) => { + if (!oldData) return { newsResponseList: [] }; + return { + newsResponseList: oldData.newsResponseList.map((article) => + article.id === unlikedArticleId + ? { + ...article, + isLiked: false, + likeCount: Math.max(0, (article.likeCount ?? 1) - 1), + } + : article, + ), + }; + }); + + return { previousArticleList }; + }, + + onError: (err, variables, context) => { + if (context?.previousArticleList) { + queryClient.setQueryData(queryKey, context.previousArticleList); + } + }, + }); +}; + +export default useDeleteArticleLike; diff --git a/src/api/news/client/useDeleteArticle.ts b/src/apis/news/deleteNews.ts similarity index 55% rename from src/api/news/client/useDeleteArticle.ts rename to src/apis/news/deleteNews.ts index b787291f..104bf8b6 100644 --- a/src/api/news/client/useDeleteArticle.ts +++ b/src/apis/news/deleteNews.ts @@ -1,45 +1,32 @@ -import { AxiosError, AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; +import { ArticleListResponse, NewsQueryKeys, newsApi } from "./api"; import { Article } from "@/types/news"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; -// Article 타입 import 필요 - -// 1. 롤백을 위해 이전 데이터를 저장할 context 타입을 정의합니다. type ArticleDeleteMutationContext = { previousArticleList?: Article[]; }; -const deleteArticle = async (articleId: number): Promise => { - const response: AxiosResponse = await axiosInstance.delete(`/news/${articleId}`); - return response.data; -}; - -// 2. 어떤 유저의 목록을 업데이트할지 식별하기 위해 userId를 props로 받습니다. +/** + * @description 아티클 삭제 훅 + */ const useDeleteArticle = (userId: number | null) => { const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; + const queryKey = [NewsQueryKeys.articleList, userId]; - return useMutation< - void, // 성공 시 반환 타입 - AxiosError<{ message: string }>, // 에러 타입 - number, // mutate 함수에 전달하는 변수 타입 (articleId) - ArticleDeleteMutationContext // context 타입 - >({ - mutationFn: deleteArticle, + return useMutation, number, ArticleDeleteMutationContext>({ + mutationFn: newsApi.deleteArticle, onMutate: async (deletedArticleId) => { await queryClient.cancelQueries({ queryKey }); const previousArticleList = queryClient.getQueryData(queryKey); - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { + queryClient.setQueryData(queryKey, (oldData) => { if (!oldData) return { newsResponseList: [] }; return { newsResponseList: oldData.newsResponseList.filter((article) => article.id !== deletedArticleId), diff --git a/src/apis/news/getNewsList.ts b/src/apis/news/getNewsList.ts new file mode 100644 index 00000000..88a8b149 --- /dev/null +++ b/src/apis/news/getNewsList.ts @@ -0,0 +1,27 @@ +import { AxiosError } from "axios"; + +import { ArticleListResponse, NewsQueryKeys, newsApi } from "./api"; + +import { Article } from "@/types/news"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 아티클 목록 조회 훅 + */ +const useGetArticleList = (userId: number) => { + return useQuery({ + queryKey: [NewsQueryKeys.articleList, userId], + queryFn: () => { + if (userId === null) { + return Promise.reject(new Error("User ID is null")); + } + return newsApi.getArticleList(userId); + }, + staleTime: 1000 * 60 * 10, // 10분 + enabled: userId !== null && userId !== 0, + select: (data) => data.newsResponseList, + }); +}; + +export default useGetArticleList; diff --git a/src/apis/news/index.ts b/src/apis/news/index.ts new file mode 100644 index 00000000..b4c7f58c --- /dev/null +++ b/src/apis/news/index.ts @@ -0,0 +1,16 @@ +export { newsApi, NewsQueryKeys } from "./api"; +export type { + ArticleListResponse, + PostArticleLikeResponse, + DeleteArticleLikeResponse, + UsePostAddArticleRequest, + UsePutModifyArticleRequest, +} from "./api"; + +// News (아티클) hooks +export { default as useGetArticleList } from "./getNewsList"; +export { default as usePostAddArticle } from "./postCreateNews"; +export { default as usePutModifyArticle } from "./putUpdateNews"; +export { default as useDeleteArticle } from "./deleteNews"; +export { default as usePostArticleLike } from "./postLikeNews"; +export { default as useDeleteArticleLike } from "./deleteLikeNews"; diff --git a/src/api/news/client/usePostAddArticle.ts b/src/apis/news/postCreateNews.ts similarity index 60% rename from src/api/news/client/usePostAddArticle.ts rename to src/apis/news/postCreateNews.ts index 96d0ceb3..1489c37a 100644 --- a/src/api/news/client/usePostAddArticle.ts +++ b/src/apis/news/postCreateNews.ts @@ -1,51 +1,32 @@ -import { AxiosError, AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { ArticleFormData } from "@/components/mentor/ArticleBottomSheetModal/lib/schema"; - -import { QueryKeys } from "./queryKey"; +import { ArticleListResponse, NewsQueryKeys, UsePostAddArticleRequest, newsApi } from "./api"; import { Article } from "@/types/news"; +import { toast } from "@/lib/zustand/useToastStore"; import ArticleThumbUrlPng from "@/public/images/article-thumb.png"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "@/lib/zustand/useToastStore"; type ArticleMutationContext = { - previousArticleContainer?: { newsResponseList: Article[] }; -}; - -type UsePostAddArticleRequest = ArticleFormData; - -const postAddArticle = async (body: UsePostAddArticleRequest): Promise
=> { - const newsCreateRequest = { - title: body.title, - description: body.description, - url: body.url || "", - }; - - const formData = new FormData(); - formData.append("newsCreateRequest", new Blob([JSON.stringify(newsCreateRequest)], { type: "application/json" })); - if (body.file) { - formData.append("file", body.file); - } - const response: AxiosResponse
= await axiosInstance.post("/news", formData); - return response.data; + previousArticleContainer?: ArticleListResponse; }; +/** + * @description 아티클 추가 훅 + */ const usePostAddArticle = (userId: number | null) => { const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; + const queryKey = [NewsQueryKeys.articleList, userId]; return useMutation, UsePostAddArticleRequest, ArticleMutationContext>({ - mutationFn: postAddArticle, + mutationFn: newsApi.postAddArticle, onMutate: async (newArticle) => { await queryClient.cancelQueries({ queryKey }); - const previousArticleContainer = queryClient.getQueryData<{ newsResponseList: Article[] }>(queryKey); + const previousArticleContainer = queryClient.getQueryData(queryKey); - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { + queryClient.setQueryData(queryKey, (oldData) => { if (!oldData) return { newsResponseList: [] }; const optimisticArticle: Article = { diff --git a/src/apis/news/postLikeNews.ts b/src/apis/news/postLikeNews.ts new file mode 100644 index 00000000..d71a7e1d --- /dev/null +++ b/src/apis/news/postLikeNews.ts @@ -0,0 +1,54 @@ +import { AxiosError } from "axios"; + +import { ArticleListResponse, NewsQueryKeys, PostArticleLikeResponse, newsApi } from "./api"; + +import { Article } from "@/types/news"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +type ArticleLikeMutationContext = { + previousArticleList?: Article[]; +}; + +/** + * @description 아티클 좋아요 훅 + */ +const usePostArticleLike = (userId: number | null) => { + const queryClient = useQueryClient(); + const queryKey = [NewsQueryKeys.articleList, userId]; + + return useMutation, number, ArticleLikeMutationContext>({ + mutationFn: newsApi.postArticleLike, + + onMutate: async (likedArticleId) => { + await queryClient.cancelQueries({ queryKey }); + + const previousArticleList = queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, (oldData) => { + if (!oldData) return { newsResponseList: [] }; + return { + newsResponseList: oldData.newsResponseList.map((article) => + article.id === likedArticleId + ? { + ...article, + isLiked: true, + likeCount: (article.likeCount ?? 0) + 1, + } + : article, + ), + }; + }); + + return { previousArticleList }; + }, + + onError: (err, variables, context) => { + if (context?.previousArticleList) { + queryClient.setQueryData(queryKey, context.previousArticleList); + } + }, + }); +}; + +export default usePostArticleLike; diff --git a/src/api/news/client/usePutModifyArticle.ts b/src/apis/news/putUpdateNews.ts similarity index 60% rename from src/api/news/client/usePutModifyArticle.ts rename to src/apis/news/putUpdateNews.ts index 92d00ce5..d82ad79a 100644 --- a/src/api/news/client/usePutModifyArticle.ts +++ b/src/apis/news/putUpdateNews.ts @@ -1,51 +1,30 @@ -import { AxiosError, AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { ArticleFormData } from "@/components/mentor/ArticleBottomSheetModal/lib/schema"; - -import { QueryKeys } from "./queryKey"; +import { ArticleListResponse, NewsQueryKeys, UsePutModifyArticleRequest, newsApi } from "./api"; import { Article } from "@/types/news"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -type UsePutModifyArticleRequest = { - body: ArticleFormData & { isImageDeleted?: boolean }; - articleId: number; -}; type ArticleMutationContext = { previousArticleList?: Article[]; }; -const putModifyArticle = async (props: UsePutModifyArticleRequest): Promise
=> { - const { body, articleId } = props; - const newsUpdateRequest = { - title: body.title, - description: body.description, - url: body.url || "", - resetToDefaultImage: body.isImageDeleted === true, - }; - const formData = new FormData(); - formData.append("newsUpdateRequest", new Blob([JSON.stringify(newsUpdateRequest)], { type: "application/json" })); - if (body.file) formData.append("file", body.file); - - const response: AxiosResponse
= await axiosInstance.put(`/news/${articleId}`, formData); - return response.data; -}; - +/** + * @description 아티클 수정 훅 + */ const usePutModifyArticle = (userId: number | null) => { const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; + const queryKey = [NewsQueryKeys.articleList, userId]; return useMutation, UsePutModifyArticleRequest, ArticleMutationContext>({ - mutationFn: putModifyArticle, + mutationFn: newsApi.putModifyArticle, onMutate: async (variables) => { await queryClient.cancelQueries({ queryKey }); const previousArticleList = queryClient.getQueryData(queryKey); - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { + queryClient.setQueryData(queryKey, (oldData) => { if (!oldData) return { newsResponseList: [] }; return { diff --git a/src/apis/queryKeys.ts b/src/apis/queryKeys.ts new file mode 100644 index 00000000..3963ad5a --- /dev/null +++ b/src/apis/queryKeys.ts @@ -0,0 +1,132 @@ +/** + * React Query Keys + * Bruno 폴더 구조를 기반으로 자동 생성됨 + */ + +export const QueryKeys = { + Auth: { + folder: "Auth.folder" as const, + signOut: "Auth.signOut" as const, + appleAuth: "Auth.appleAuth" as const, + refreshToken: "Auth.refreshToken" as const, + emailLogin: "Auth.emailLogin" as const, + emailVerification: "Auth.emailVerification" as const, + kakaoAuth: "Auth.kakaoAuth" as const, + account: "Auth.account" as const, + signUp: "Auth.signUp" as const, + }, + news: { + folder: "news.folder" as const, + newsList: "news.newsList" as const, + news: "news.news" as const, + updateNews: "news.updateNews" as const, + likeNews: "news.likeNews" as const, + createNews: "news.createNews" as const, + }, + reports: { + folder: "reports.folder" as const, + report: "reports.report" as const, + }, + chat: { + folder: "chat.folder" as const, + chatMessages: "chat.chatMessages" as const, + chatRooms: "chat.chatRooms" as const, + readChatRoom: "chat.readChatRoom" as const, + chatPartner: "chat.chatPartner" as const, + }, + universities: { + folder: "universities.folder" as const, + recommendedUniversities: "universities.recommendedUniversities" as const, + wishList: "universities.wishList" as const, + wish: "universities.wish" as const, + addWish: "universities.addWish" as const, + isWish: "universities.isWish" as const, + universityDetail: "universities.universityDetail" as const, + searchText: "universities.searchText" as const, + searchFilter: "universities.searchFilter" as const, + byRegionCountry: "universities.byRegionCountry" as const, + }, + MyPage: { + folder: "MyPage.folder" as const, + interestedRegionCountry: "MyPage.interestedRegionCountry" as const, + profile: "MyPage.profile" as const, + password: "MyPage.password" as const, + }, + applications: { + folder: "applications.folder" as const, + competitors: "applications.competitors" as const, + submitApplication: "applications.submitApplication" as const, + applicants: "applications.applicants" as const, + }, + community: { + folder: "community.folder" as const, + boardList: "community.boardList" as const, + board: "community.board" as const, + comment: "community.comment" as const, + updateComment: "community.updateComment" as const, + createComment: "community.createComment" as const, + post: "community.post" as const, + updatePost: "community.updatePost" as const, + createPost: "community.createPost" as const, + postDetail: "community.postDetail" as const, + likePost: "community.likePost" as const, + }, + Scores: { + folder: "Scores.folder" as const, + createLanguageTest: "Scores.createLanguageTest" as const, + languageTestList: "Scores.languageTestList" as const, + createGpa: "Scores.createGpa" as const, + gpaList: "Scores.gpaList" as const, + }, + Admin: { + folder: "Admin.folder" as const, + verifyLanguageTest: "Admin.verifyLanguageTest" as const, + languageTestList: "Admin.languageTestList" as const, + verifyGpa: "Admin.verifyGpa" as const, + gpaList: "Admin.gpaList" as const, + }, + users: { + folder: "users.folder" as const, + nicknameExists: "users.nicknameExists" as const, + blockUser: "users.blockUser" as const, + unblockUser: "users.unblockUser" as const, + blockedUsers: "users.blockedUsers" as const, + }, + mentor: { + folder: "mentor.folder" as const, + matchedMentors: "mentor.matchedMentors" as const, + applyMentoring: "mentor.applyMentoring" as const, + confirmMentoring: "mentor.confirmMentoring" as const, + appliedMentorings: "mentor.appliedMentorings" as const, + mentorList: "mentor.mentorList" as const, + mentorDetail: "mentor.mentorDetail" as const, + myMentorPage: "mentor.myMentorPage" as const, + updateMyMentorPage: "mentor.updateMyMentorPage" as const, + mentoringStatus: "mentor.mentoringStatus" as const, + receivedMentorings: "mentor.receivedMentorings" as const, + unconfirmedMentoringCount: "mentor.unconfirmedMentoringCount" as const, + }, + "kakao-api": { + folder: "kakao-api.folder" as const, + kakaoUserIds: "kakao-api.kakaoUserIds" as const, + kakaoUnlink: "kakao-api.kakaoUnlink" as const, + kakaoInfo: "kakao-api.kakaoInfo" as const, + }, + "collection.bru": { + collection: "collection.bru.collection" as const, + }, + environments: { + dev: "environments.dev" as const, + local: "environments.local" as const, + }, + "image-upload": { + folder: "image-upload.folder" as const, + slackNotification: "image-upload.slackNotification" as const, + uploadLanguageTestReport: "image-upload.uploadLanguageTestReport" as const, + uploadProfileImage: "image-upload.uploadProfileImage" as const, + uploadProfileImageBeforeSignup: "image-upload.uploadProfileImageBeforeSignup" as const, + uploadGpaReport: "image-upload.uploadGpaReport" as const, + }, +} as const; + +export type QueryKey = (typeof QueryKeys)[keyof typeof QueryKeys]; diff --git a/src/apis/reports/api.ts b/src/apis/reports/api.ts new file mode 100644 index 00000000..66f3b2b0 --- /dev/null +++ b/src/apis/reports/api.ts @@ -0,0 +1,23 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { ReportType } from "@/types/reports"; + +// ====== Types ====== +export interface UsePostReportsRequest { + targetType: "POST"; // 지금은 게시글 신고 기능만 존재 + targetId: number; // 신고하려는 리소스의 ID + reportType: ReportType; +} + +// ====== API Functions ====== +export const reportsApi = { + /** + * 신고 등록 + */ + postReport: async (body: UsePostReportsRequest): Promise => { + const response: AxiosResponse = await axiosInstance.post(`/reports`, body); + return response.data; + }, +}; diff --git a/src/apis/reports/index.ts b/src/apis/reports/index.ts new file mode 100644 index 00000000..8baf4cb5 --- /dev/null +++ b/src/apis/reports/index.ts @@ -0,0 +1,3 @@ +export { reportsApi } from "./api"; +export type { UsePostReportsRequest } from "./api"; +export { default as usePostReports } from "./postReport"; diff --git a/src/apis/reports/postReport.ts b/src/apis/reports/postReport.ts new file mode 100644 index 00000000..0e4665c0 --- /dev/null +++ b/src/apis/reports/postReport.ts @@ -0,0 +1,27 @@ +import { useRouter } from "next/navigation"; + +import { AxiosError } from "axios"; + +import { UsePostReportsRequest, reportsApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 신고 등록 훅 + */ +const usePostReports = () => { + const router = useRouter(); + return useMutation, UsePostReportsRequest>({ + mutationFn: reportsApi.postReport, + onSuccess: () => { + toast.success("신고가 성공적으로 등록되었습니다."); + router.back(); + }, + onError: (error) => { + toast.error("신고 등록에 실패했습니다. 잠시 후 다시 시도해주세요."); + }, + }); +}; + +export default usePostReports; diff --git a/src/apis/universities/api.ts b/src/apis/universities/api.ts new file mode 100644 index 00000000..3a8b70aa --- /dev/null +++ b/src/apis/universities/api.ts @@ -0,0 +1,193 @@ +import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; + +export interface RecommendedUniversitiesResponseRecommendedUniversitiesItem { + id: number; + term: string; + koreanName: string; + region: string; + country: string; + logoImageUrl: string; + backgroundImageUrl: string; + studentCapacity: number; + languageRequirements: RecommendedUniversitiesResponseRecommendedUniversitiesItemLanguageRequirementsItem[]; +} + +export interface RecommendedUniversitiesResponseRecommendedUniversitiesItemLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface RecommendedUniversitiesResponse { + recommendedUniversities: RecommendedUniversitiesResponseRecommendedUniversitiesItem[]; +} + +export interface WishListResponseItem { + id: number; + term: string; + koreanName: string; + region: string; + country: string; + logoImageUrl: string; + backgroundImageUrl: string; + studentCapacity: number; + languageRequirements: WishListResponseItemLanguageRequirementsItem[]; +} + +export interface WishListResponseItemLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface WishListResponse { + 0: WishListResponseItem[]; + 1: WishListResponseItem[]; +} + +export type WishResponse = void; + +export type AddWishResponse = void; + +export type AddWishRequest = Record; + +export type IsWishResponse = void; + +export interface UniversityDetailResponseLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface UniversityDetailResponse { + id: number; + term: string; + koreanName: string; + englishName: string; + formatName: string; + region: string; + country: string; + homepageUrl: string; + logoImageUrl: string; + backgroundImageUrl: string; + detailsForLocal: string; + studentCapacity: number; + tuitionFeeType: string; + semesterAvailableForDispatch: string; + languageRequirements: UniversityDetailResponseLanguageRequirementsItem[]; + detailsForLanguage: string; + gpaRequirement: string; + gpaRequirementCriteria: string; + semesterRequirement: string; + detailsForApply: null; + detailsForMajor: string; + detailsForAccommodation: null; + detailsForEnglishCourse: null; + details: string; + accommodationUrl: string; + englishCourseUrl: string; +} + +export interface SearchTextResponseUnivApplyInfoPreviewsItem { + id: number; + term: string; + koreanName: string; + region: string; + country: string; + logoImageUrl: string; + backgroundImageUrl: string; + studentCapacity: number; + languageRequirements: SearchTextResponseUnivApplyInfoPreviewsItemLanguageRequirementsItem[]; +} + +export interface SearchTextResponseUnivApplyInfoPreviewsItemLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface SearchTextResponse { + univApplyInfoPreviews: SearchTextResponseUnivApplyInfoPreviewsItem[]; +} + +export interface SearchFilterResponseUnivApplyInfoPreviewsItem { + id: number; + term: string; + koreanName: string; + region: string; + country: string; + logoImageUrl: string; + backgroundImageUrl: string; + studentCapacity: number; + languageRequirements: SearchFilterResponseUnivApplyInfoPreviewsItemLanguageRequirementsItem[]; +} + +export interface SearchFilterResponseUnivApplyInfoPreviewsItemLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface SearchFilterResponse { + univApplyInfoPreviews: SearchFilterResponseUnivApplyInfoPreviewsItem[]; +} + +export type ByRegionCountryResponse = void; + +export const universitiesApi = { + getRecommendedUniversities: async (params?: { isLogin?: boolean }): Promise => { + const instance = params?.isLogin ? axiosInstance : publicAxiosInstance; + const res = await instance.get(`/univ-apply-infos/recommend`); + return res.data; + }, + + getWishList: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/univ-apply-infos/like`, { params: params?.params }); + return res.data; + }, + + deleteWish: async (params: { univApplyInfoId: string | number }): Promise => { + const res = await axiosInstance.delete(`/univ-apply-infos/${params.univApplyInfoId}/like`); + return res.data; + }, + + postAddWish: async (params: { + univApplyInfoId: string | number; + data?: AddWishRequest; + }): Promise => { + const res = await axiosInstance.post( + `/univ-apply-infos/${params.univApplyInfoId}/like`, + params?.data, + ); + return res.data; + }, + + getIsWish: async (params: { + univApplyInfoId: string | number; + params?: Record; + }): Promise => { + const res = await axiosInstance.get(`/univ-apply-infos/${params.univApplyInfoId}/like`, { + params: params?.params, + }); + return res.data; + }, + + getUniversityDetail: async (params: { univApplyInfoId: string | number }): Promise => { + const res = await publicAxiosInstance.get(`/univ-apply-infos/${params.univApplyInfoId}`); + return res.data; + }, + + getSearchText: async (params?: { value?: string }): Promise => { + const res = await publicAxiosInstance.get(`/univ-apply-infos/search/text`, { + params: { value: params?.value ?? "" }, + }); + return res.data; + }, + + getSearchFilter: async (params?: { params?: Record }): Promise => { + const res = await publicAxiosInstance.get(`/univ-apply-infos/search/filter`, { + params: params?.params, + }); + return res.data; + }, + + getByRegionCountry: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/universities/search`, { params: params?.params }); + return res.data; + }, +}; diff --git a/src/apis/universities/deleteWish.ts b/src/apis/universities/deleteWish.ts new file mode 100644 index 00000000..5e3fc309 --- /dev/null +++ b/src/apis/universities/deleteWish.ts @@ -0,0 +1,22 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { WishResponse, universitiesApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 위시리스트에서 학교를 삭제하는 useMutation 커스텀 훅 + */ +const useDeleteWish = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (universityInfoForApplyId) => universitiesApi.deleteWish({ univApplyInfoId: universityInfoForApplyId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.wishList] }); + }, + }); +}; + +export default useDeleteWish; diff --git a/src/apis/universities/getByRegionCountry.ts b/src/apis/universities/getByRegionCountry.ts new file mode 100644 index 00000000..29f34b56 --- /dev/null +++ b/src/apis/universities/getByRegionCountry.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { ByRegionCountryResponse, universitiesApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetByRegionCountry = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.universities.byRegionCountry, params], + queryFn: () => universitiesApi.getByRegionCountry(params ? { params } : {}), + }); +}; + +export default useGetByRegionCountry; diff --git a/src/apis/universities/getIsWish.ts b/src/apis/universities/getIsWish.ts new file mode 100644 index 00000000..f89e3f25 --- /dev/null +++ b/src/apis/universities/getIsWish.ts @@ -0,0 +1,16 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { IsWishResponse, universitiesApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetIsWish = (univApplyInfoId: string | number, params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.universities.isWish, univApplyInfoId, params], + queryFn: () => universitiesApi.getIsWish({ univApplyInfoId, params }), + enabled: !!univApplyInfoId, + }); +}; + +export default useGetIsWish; diff --git a/src/apis/universities/getRecommendedUniversities.ts b/src/apis/universities/getRecommendedUniversities.ts new file mode 100644 index 00000000..a316317e --- /dev/null +++ b/src/apis/universities/getRecommendedUniversities.ts @@ -0,0 +1,27 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { RecommendedUniversitiesResponse, universitiesApi } from "./api"; + +import { ListUniversity } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +type UseGetRecommendedUniversitiesParams = { + isLogin: boolean; +}; + +/** + * @description 추천 대학 목록 조회를 위한 useQuery 커스텀 훅 + * @param params.isLogin - 로그인 여부 (인스턴스 결정에 사용) + */ +const useGetRecommendedUniversities = ({ isLogin }: UseGetRecommendedUniversitiesParams) => { + return useQuery({ + queryKey: [QueryKeys.universities.recommendedUniversities, isLogin], + queryFn: () => universitiesApi.getRecommendedUniversities({ isLogin }), + staleTime: 1000 * 60 * 5, + select: (data) => data.recommendedUniversities as unknown as ListUniversity[], + }); +}; + +export default useGetRecommendedUniversities; diff --git a/src/apis/universities/getSearchFilter.ts b/src/apis/universities/getSearchFilter.ts new file mode 100644 index 00000000..09b9f3c7 --- /dev/null +++ b/src/apis/universities/getSearchFilter.ts @@ -0,0 +1,47 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { SearchFilterResponse, universitiesApi } from "./api"; + +import { CountryCode, LanguageTestType, ListUniversity } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +export interface UniversitySearchFilterParams { + languageTestType?: LanguageTestType; + testScore?: number; + countryCode?: CountryCode[]; +} + +/** + * @description 필터로 대학 검색을 위한 useQuery 커스텀 훅 + * @param filters - 검색 필터 파라미터 + */ +const useGetUniversitySearchByFilter = (filters: UniversitySearchFilterParams) => { + // 필터 파라미터 구성 + const buildParams = () => { + const params: Record = {}; + if (filters.languageTestType) { + params.languageTestType = filters.languageTestType; + } + if (filters.testScore !== undefined) { + params.testScore = String(filters.testScore); + } + if (filters.countryCode && filters.countryCode.length > 0) { + params.countryCode = filters.countryCode; + } + return params; + }; + + return useQuery({ + queryKey: [QueryKeys.universities.searchFilter, filters], + queryFn: () => universitiesApi.getSearchFilter({ params: buildParams() }), + enabled: Object.values(filters).some((value) => { + if (Array.isArray(value)) return value.length > 0; + return value !== undefined && value !== ""; + }), + select: (data) => data.univApplyInfoPreviews as unknown as ListUniversity[], + }); +}; + +export default useGetUniversitySearchByFilter; diff --git a/src/apis/universities/getSearchText.ts b/src/apis/universities/getSearchText.ts new file mode 100644 index 00000000..e0cb52ce --- /dev/null +++ b/src/apis/universities/getSearchText.ts @@ -0,0 +1,56 @@ +import { useMemo } from "react"; + +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { SearchTextResponse, universitiesApi } from "./api"; + +import { ListUniversity } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 대학 검색을 위한 useQuery 커스텀 훅 + * 모든 대학 데이터를 한 번만 가져와 캐싱하고, 검색어에 따라 클라이언트에서 필터링합니다. + * @param searchValue - 검색어 + */ +const useUniversitySearch = (searchValue: string) => { + // 1. 모든 대학 데이터를 한 번만 가져와 'Infinity' 캐시로 저장합니다. + const { + data: allUniversities, + isLoading, + isError, + error, + } = useQuery({ + queryKey: [QueryKeys.universities.searchText], + queryFn: () => universitiesApi.getSearchText({ value: "" }), + staleTime: Infinity, + gcTime: Infinity, + select: (data) => data.univApplyInfoPreviews as unknown as ListUniversity[], + }); + + // 2. 검색어가 변경될 때만 캐시된 데이터를 필터링합니다. + const filteredUniversities = useMemo(() => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); + + if (!normalizedSearchValue) { + return allUniversities; + } + + if (!allUniversities) { + return []; + } + + return allUniversities.filter((university) => university.koreanName.toLowerCase().includes(normalizedSearchValue)); + }, [allUniversities, searchValue]); + + return { + data: filteredUniversities, + isLoading, + isError, + error, + totalCount: allUniversities?.length || 0, + }; +}; + +export default useUniversitySearch; diff --git a/src/apis/universities/getUniversityDetail.ts b/src/apis/universities/getUniversityDetail.ts new file mode 100644 index 00000000..ce2ff998 --- /dev/null +++ b/src/apis/universities/getUniversityDetail.ts @@ -0,0 +1,23 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { UniversityDetailResponse, universitiesApi } from "./api"; + +import { University } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 대학 상세 조회를 위한 useQuery 커스텀 훅 + * @param universityInfoForApplyId - 대학 ID + */ +const useGetUniversityDetail = (universityInfoForApplyId: number) => { + return useQuery({ + queryKey: [QueryKeys.universities.universityDetail, universityInfoForApplyId], + queryFn: () => universitiesApi.getUniversityDetail({ univApplyInfoId: universityInfoForApplyId }), + enabled: !!universityInfoForApplyId, + select: (data) => data as unknown as University, + }); +}; + +export default useGetUniversityDetail; diff --git a/src/apis/universities/getWishList.ts b/src/apis/universities/getWishList.ts new file mode 100644 index 00000000..64fc2953 --- /dev/null +++ b/src/apis/universities/getWishList.ts @@ -0,0 +1,24 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { WishListResponse, universitiesApi } from "./api"; + +import { ListUniversity } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 내 위시리스트 대학 목록 조회를 위한 useQuery 커스텀 훅 + * @param enabled - 쿼리 활성화 여부 + */ +const useGetWishList = (enabled: boolean = true) => { + return useQuery({ + queryKey: [QueryKeys.universities.wishList], + queryFn: () => universitiesApi.getWishList({}), + staleTime: 1000 * 60 * 5, + select: (data) => data as unknown as ListUniversity[], + enabled, + }); +}; + +export default useGetWishList; diff --git a/src/apis/universities/index.ts b/src/apis/universities/index.ts new file mode 100644 index 00000000..95a44032 --- /dev/null +++ b/src/apis/universities/index.ts @@ -0,0 +1,10 @@ +export { universitiesApi } from "./api"; +export { default as useDeleteWish } from "./deleteWish"; +export { default as useGetByRegionCountry } from "./getByRegionCountry"; +export { default as useGetIsWish } from "./getIsWish"; +export { default as useGetRecommendedUniversities } from "./getRecommendedUniversities"; +export { default as useGetUniversitySearchByFilter, type UniversitySearchFilterParams } from "./getSearchFilter"; +export { default as useUniversitySearch } from "./getSearchText"; +export { default as useGetUniversityDetail } from "./getUniversityDetail"; +export { default as useGetWishList } from "./getWishList"; +export { default as usePostAddWish } from "./postAddWish"; diff --git a/src/apis/universities/postAddWish.ts b/src/apis/universities/postAddWish.ts new file mode 100644 index 00000000..4f7f22bd --- /dev/null +++ b/src/apis/universities/postAddWish.ts @@ -0,0 +1,26 @@ +import { AxiosError } from "axios"; + +import { createMutationErrorHandler } from "@/utils/errorHandler"; + +import { QueryKeys } from "../queryKeys"; +import { AddWishResponse, universitiesApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 위시리스트에 학교를 추가하는 useMutation 커스텀 훅 + */ +const usePostAddWish = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (universityInfoForApplyId) => + universitiesApi.postAddWish({ univApplyInfoId: universityInfoForApplyId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.wishList] }); + }, + onError: createMutationErrorHandler("위시리스트 추가에 실패했습니다."), + }); +}; + +export default usePostAddWish; diff --git a/src/api/university/server/getRecommendedUniversity.ts b/src/apis/universities/server/getRecommendedUniversity.ts similarity index 100% rename from src/api/university/server/getRecommendedUniversity.ts rename to src/apis/universities/server/getRecommendedUniversity.ts diff --git a/src/api/university/server/getSearchUniversitiesByFilter.ts b/src/apis/universities/server/getSearchUniversitiesByFilter.ts similarity index 100% rename from src/api/university/server/getSearchUniversitiesByFilter.ts rename to src/apis/universities/server/getSearchUniversitiesByFilter.ts diff --git a/src/api/university/server/getSearchUniversitiesByText.ts b/src/apis/universities/server/getSearchUniversitiesByText.ts similarity index 100% rename from src/api/university/server/getSearchUniversitiesByText.ts rename to src/apis/universities/server/getSearchUniversitiesByText.ts diff --git a/src/api/university/server/getUniversityDetail.ts b/src/apis/universities/server/getUniversityDetail.ts similarity index 100% rename from src/api/university/server/getUniversityDetail.ts rename to src/apis/universities/server/getUniversityDetail.ts diff --git a/src/apis/universities/server/index.ts b/src/apis/universities/server/index.ts new file mode 100644 index 00000000..e98228fd --- /dev/null +++ b/src/apis/universities/server/index.ts @@ -0,0 +1,9 @@ +// Server-side exports +export { default as getRecommendedUniversity } from "./getRecommendedUniversity"; +export { getUniversityDetail } from "./getUniversityDetail"; +export { getUniversitiesByText, getAllUniversities, getCategorizedUniversities } from "./getSearchUniversitiesByText"; +export { + getSearchUniversitiesByFilter, + getSearchUniversitiesAllRegions, + type UniversitySearchFilterParams, +} from "./getSearchUniversitiesByFilter"; diff --git a/src/apis/users/api.ts b/src/apis/users/api.ts new file mode 100644 index 00000000..5c69840d --- /dev/null +++ b/src/apis/users/api.ts @@ -0,0 +1,52 @@ +import { axiosInstance } from "@/utils/axiosInstance"; + +export interface NicknameExistsResponse { + exists: boolean; +} + +export type BlockUserResponse = void; + +export type BlockUserRequest = Record; + +export type UnblockUserRequest = Record; + +export type UnblockUserResponse = void; + +export interface BlockedUsersResponseContentItem { + id: number; + blockedId: number; + nickname: string; + createdAt: string; +} + +export interface BlockedUsersResponse { + content: BlockedUsersResponseContentItem[]; + nextPageNumber: number; +} + +export const usersApi = { + getNicknameExists: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/users/exists?nickname=abc`, { + params: params?.params, + }); + return res.data; + }, + + postBlockUser: async (params: { + blockedId: string | number; + data?: BlockUserRequest; + }): Promise => { + const res = await axiosInstance.post(`/users/block/${params.blockedId}`, params?.data); + return res.data; + }, + + deleteUnblockUser: async (params: { blockedId: string | number }): Promise => { + const res = await axiosInstance.delete(`/users/block/${params.blockedId}`); + return res.data; + }, + + getBlockedUsers: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/users/blocks`, { params: params?.params }); + return res.data; + }, +}; diff --git a/src/apis/users/deleteUnblockUser.ts b/src/apis/users/deleteUnblockUser.ts new file mode 100644 index 00000000..88cb8451 --- /dev/null +++ b/src/apis/users/deleteUnblockUser.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { UnblockUserRequest, UnblockUserResponse, usersApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const useDeleteUnblockUser = () => { + return useMutation({ + mutationFn: (variables) => usersApi.deleteUnblockUser(variables), + }); +}; + +export default useDeleteUnblockUser; diff --git a/src/apis/users/getBlockedUsers.ts b/src/apis/users/getBlockedUsers.ts new file mode 100644 index 00000000..4e33e910 --- /dev/null +++ b/src/apis/users/getBlockedUsers.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { BlockedUsersResponse, usersApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetBlockedUsers = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.users.blockedUsers, params], + queryFn: () => usersApi.getBlockedUsers(params ? { params } : {}), + }); +}; + +export default useGetBlockedUsers; diff --git a/src/apis/users/getNicknameExists.ts b/src/apis/users/getNicknameExists.ts new file mode 100644 index 00000000..fc5ff88a --- /dev/null +++ b/src/apis/users/getNicknameExists.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { NicknameExistsResponse, usersApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetNicknameExists = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.users.nicknameExists, params], + queryFn: () => usersApi.getNicknameExists(params ? { params } : {}), + }); +}; + +export default useGetNicknameExists; diff --git a/src/apis/users/index.ts b/src/apis/users/index.ts new file mode 100644 index 00000000..ff51c891 --- /dev/null +++ b/src/apis/users/index.ts @@ -0,0 +1,5 @@ +export { usersApi } from "./api"; +export { default as deleteUnblockUser } from "./deleteUnblockUser"; +export { default as getBlockedUsers } from "./getBlockedUsers"; +export { default as getNicknameExists } from "./getNicknameExists"; +export { default as postBlockUser } from "./postBlockUser"; diff --git a/src/apis/users/postBlockUser.ts b/src/apis/users/postBlockUser.ts new file mode 100644 index 00000000..027b49e0 --- /dev/null +++ b/src/apis/users/postBlockUser.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { BlockUserRequest, BlockUserResponse, usersApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostBlockUser = () => { + return useMutation({ + mutationFn: (variables) => usersApi.postBlockUser(variables), + }); +}; + +export default usePostBlockUser; diff --git a/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx b/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx index 13db3fde..f0425e96 100644 --- a/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx +++ b/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx @@ -1,7 +1,6 @@ "use client"; import { toast } from "@/lib/zustand/useToastStore"; - import { IconGraduationCap, IconRightArrow } from "@/public/svgs/home"; const FindLastYearScoreBar = () => { @@ -14,8 +13,8 @@ const FindLastYearScoreBar = () => {
- 작년 합격 점수가 궁금하신가요? - 작년도 합격 점수 확인하러 가기 + 작년 합격 점수가 궁금하신가요? + 작년도 합격 점수 확인하러 가기
diff --git a/src/app/(home)/_ui/NewsSection/index.tsx b/src/app/(home)/_ui/NewsSection/index.tsx index 7043fbda..e6021ebc 100644 --- a/src/app/(home)/_ui/NewsSection/index.tsx +++ b/src/app/(home)/_ui/NewsSection/index.tsx @@ -19,7 +19,7 @@ const NewsSection = ({ newsList }: NewsSectionProps) => { return (
-
+
솔커에서 맛보는 소식 {/* @@ -55,8 +55,8 @@ const NewsSection = ({ newsList }: NewsSectionProps) => { height={90} />
-
{news.title}
-
{news.description}
+
{news.title}
+
{news.description}
diff --git a/src/app/(home)/_ui/PopularUniversitySection/_ui/PopularUniversityCard.tsx b/src/app/(home)/_ui/PopularUniversitySection/_ui/PopularUniversityCard.tsx index 2845d0c8..4b795550 100644 --- a/src/app/(home)/_ui/PopularUniversitySection/_ui/PopularUniversityCard.tsx +++ b/src/app/(home)/_ui/PopularUniversitySection/_ui/PopularUniversityCard.tsx @@ -47,7 +47,7 @@ const PopularUniversityCard = ({ } />
-
+
{university.koreanName}
diff --git a/src/app/(home)/_ui/UniversityList/index.tsx b/src/app/(home)/_ui/UniversityList/index.tsx index 89afdabd..4b740fc2 100644 --- a/src/app/(home)/_ui/UniversityList/index.tsx +++ b/src/app/(home)/_ui/UniversityList/index.tsx @@ -29,9 +29,9 @@ const UniversityList = ({ allRegionsUniversityList }: UniversityListProps) => { return (
- 전체 학교 리스트 + 전체 학교 리스트 - + 더보기 @@ -49,7 +49,7 @@ const UniversityList = ({ allRegionsUniversityList }: UniversityListProps) => { background: "white", }} /> - +
); }; diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index 107e62f5..8b6c4c09 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -7,8 +7,7 @@ import NewsSectionSkeleton from "./_ui/NewsSection/skeleton"; import PopularUniversitySection from "./_ui/PopularUniversitySection"; import UniversityList from "./_ui/UniversityList"; -import getRecommendedUniversity from "@/api/university/server/getRecommendedUniversity"; -import { getCategorizedUniversities } from "@/api/university/server/getSearchUniversitiesByText"; +import { getCategorizedUniversities, getRecommendedUniversity } from "@/apis/universities/server"; import { fetchAllNews } from "@/lib/firebaseNews"; import { IconIdCard, IconMagnifyingGlass, IconMuseum, IconPaper } from "@/public/svgs/home"; @@ -81,24 +80,24 @@ const HomePage = async () => {
- 학교 검색하기 - 모든 학교 목록을 확인해보세요 + 학교 검색하기 + 모든 학교 목록을 확인해보세요
- 성적 입력하기 - 성적을 입력해보세요 + 성적 입력하기 + 성적을 입력해보세요
@@ -107,24 +106,24 @@ const HomePage = async () => {
- 학교 지원하기 - 학교를 지원해주세요 + 학교 지원하기 + 학교를 지원해주세요
- 지원자 현황 확인 - 경쟁률을 바로 분석해드려요 + 지원자 현황 확인 + 경쟁률을 바로 분석해드려요
@@ -134,7 +133,7 @@ const HomePage = async () => {
-
실시간 인기있는 파견학교
+
실시간 인기있는 파견학교
diff --git a/src/app/community/[boardCode]/CommunityPageContent.tsx b/src/app/community/[boardCode]/CommunityPageContent.tsx index cd11803f..286a8f13 100644 --- a/src/app/community/[boardCode]/CommunityPageContent.tsx +++ b/src/app/community/[boardCode]/CommunityPageContent.tsx @@ -11,7 +11,7 @@ import PostWriteButton from "./PostWriteButton"; import { COMMUNITY_BOARDS, COMMUNITY_CATEGORIES } from "@/constants/community"; -import useGetPostList from "@/api/boards/clients/useGetPostList"; +import { useGetPostList } from "@/apis/community"; interface CommunityPageContentProps { boardCode: string; diff --git a/src/app/community/[boardCode]/PostCards.tsx b/src/app/community/[boardCode]/PostCards.tsx index 7adc96ae..9d34743c 100644 --- a/src/app/community/[boardCode]/PostCards.tsx +++ b/src/app/community/[boardCode]/PostCards.tsx @@ -12,7 +12,6 @@ import { ListPost } from "@/types/community"; import { IconPostLikeOutline } from "@/public/svgs"; import { IconCommunication } from "@/public/svgs/community"; import { IconSolidConnentionLogo } from "@/public/svgs/mentor"; - import { useVirtualizer } from "@tanstack/react-virtual"; type PostCardsProps = { @@ -80,26 +79,20 @@ export const PostCard = ({ post }: { post: ListPost }) => (
{post.postCategory || ""} - - {convertISODateToDate(post.createdAt) || "1970. 1. 1."} - + {convertISODateToDate(post.createdAt) || "1970. 1. 1."}
- {post.title || ""} -
+ {post.title || ""} +
{post.content || "내용 없음"}
- - {post.likeCount || 0} - + {post.likeCount || 0}
- - {post.commentCount || 0} - + {post.commentCount || 0}
diff --git a/src/app/community/[boardCode]/PostWriteButton.tsx b/src/app/community/[boardCode]/PostWriteButton.tsx index 1aebfbb6..43a243a2 100644 --- a/src/app/community/[boardCode]/PostWriteButton.tsx +++ b/src/app/community/[boardCode]/PostWriteButton.tsx @@ -29,7 +29,7 @@ const PostWriteButton = ({ onClick }: PostWriteButtonProps) => { return (
-
- {isDeleted ? "삭제된 댓글입니다" : comment.content} -
-
+
{isDeleted ? "삭제된 댓글입니다" : comment.content}
+
{convertISODateToDateTime(comment.createdAt) || "1970. 01. 01. 00:00"}
@@ -153,7 +151,7 @@ const CommentProfile = ({ user }: { user: CommunityUser }) => { alt="alt" />
-
{user?.nickname}
+
{user?.nickname}
); }; diff --git a/src/app/community/[boardCode]/[postId]/Content.tsx b/src/app/community/[boardCode]/[postId]/Content.tsx index 5d5a3f79..895977f1 100644 --- a/src/app/community/[boardCode]/[postId]/Content.tsx +++ b/src/app/community/[boardCode]/[postId]/Content.tsx @@ -9,8 +9,7 @@ import LinkifyText from "@/components/ui/LinkifyText"; import { PostImage as PostImageType, Post as PostType } from "@/types/community"; -import useDeleteLike from "@/api/community/client/useDeleteLike"; -import usePostLike from "@/api/community/client/usePostLike"; +import { useDeleteLike, usePostLike } from "@/apis/community"; import { IconCloseFilled, IconPostLikeFilled, IconPostLikeOutline } from "@/public/svgs"; import { IconCommunication } from "@/public/svgs/community"; @@ -81,11 +80,11 @@ const Content = ({ post, postId }: ContentProps) => { return ( <>
-
+
{post.postCategory || "카테고리"}
-
{post.title || ""}
-
+
{post.title || ""}
+
{post.content || ""}
@@ -103,15 +102,11 @@ const Content = ({ post, postId }: ContentProps) => {
- - {post?.commentCount || 0} - + {post?.commentCount || 0}
@@ -132,10 +127,10 @@ const Content = ({ post, postId }: ContentProps) => { />
-
+
{post.postFindSiteUserResponse.nickname || ""}
-
+
{convertISODateToDateTime(post.createdAt) || ""}
@@ -198,9 +193,7 @@ const ImagePopup = ({ image, title, onClose }: ImagePopupProps) => ( > - - {title} - + {title}
diff --git a/src/app/community/[boardCode]/[postId]/KebabMenu.tsx b/src/app/community/[boardCode]/[postId]/KebabMenu.tsx index 36ef1366..a38d8479 100644 --- a/src/app/community/[boardCode]/[postId]/KebabMenu.tsx +++ b/src/app/community/[boardCode]/[postId]/KebabMenu.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from "react"; import ReportPanel from "@/components/ui/ReportPanel"; -import useDeletePost from "@/api/community/client/useDeletePost"; +import { useDeletePost } from "@/apis/community"; import { toast } from "@/lib/zustand/useToastStore"; import { IconSetting } from "@/public/svgs/mentor"; @@ -91,7 +91,7 @@ const KebabMenu = ({ postId, boardCode, isOwner = false }: KebabMenuProps) => {
  • @@ -118,7 +118,7 @@ const KebabMenu = ({ postId, boardCode, isOwner = false }: KebabMenuProps) => { deletePost(postId); } }} - className={`flex w-full items-center gap-3 rounded-md px-3 py-2.5 typo-regular-2 text-gray-700 hover:bg-gray-50`} + className={`flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-gray-700 typo-regular-2 hover:bg-gray-50`} > {"삭제하기"} diff --git a/src/app/community/[boardCode]/[postId]/PostPageContent.tsx b/src/app/community/[boardCode]/[postId]/PostPageContent.tsx index a80ee957..d31e3b66 100644 --- a/src/app/community/[boardCode]/[postId]/PostPageContent.tsx +++ b/src/app/community/[boardCode]/[postId]/PostPageContent.tsx @@ -9,7 +9,7 @@ import CommentSection from "./CommentSection"; import Content from "./Content"; import KebabMenu from "./KebabMenu"; -import useGetPostDetail from "@/api/community/client/useGetPostDetail"; +import { useGetPostDetail } from "@/apis/community"; interface PostPageContentProps { boardCode: string; diff --git a/src/app/community/[boardCode]/[postId]/modify/PostModifyContent.tsx b/src/app/community/[boardCode]/[postId]/modify/PostModifyContent.tsx index bf9a018f..39271dca 100644 --- a/src/app/community/[boardCode]/[postId]/modify/PostModifyContent.tsx +++ b/src/app/community/[boardCode]/[postId]/modify/PostModifyContent.tsx @@ -7,7 +7,7 @@ import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; import PostModifyForm from "./PostModifyForm"; -import useGetPostDetail from "@/api/community/client/useGetPostDetail"; +import { useGetPostDetail } from "@/apis/community"; interface PostModifyContentProps { boardCode: string; diff --git a/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx b/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx index d1e3d470..6592a171 100644 --- a/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx +++ b/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; -import useUpdatePost from "@/api/community/client/useUpdatePost"; +import { useUpdatePost } from "@/apis/community"; import { IconArrowBackFilled, IconImage, IconPostCheckboxFilled, IconPostCheckboxOutlined } from "@/public/svgs"; type PostModifyFormProps = { @@ -92,7 +92,7 @@ const PostModifyForm = ({ ref={titleRef} >