diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 000000000..6e655bf7a --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,213 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +KOIN is a campus service web application for Korea University of Technology and Education (KOREATECH). It provides timetable management, bus schedules, cafeteria menus, store/shop listings, community articles, clubs, lost & found, and graduation calculators. + +Built with **Next.js 15 (Pages Router)**, **React 19**, and **TypeScript** (strict mode). + +- Package manager: **Yarn 4 (Berry)** with PnP. Never use `npm install`. +- Node version: **20.11.1** + +## Commands + +```bash +yarn start # Dev server (next dev) +yarn build # Type-check (tsc) then production build +yarn lint # ESLint + Stylelint +yarn lint:eslint # ESLint only (src/) +yarn lint:stylelint # Stylelint only (src/**/*.scss) +yarn log # Generate analytics logging hooks from Notion spec +``` + +## Architecture + +### Routing (Pages Router) + +File-based routing in `src/pages/`. Type-safe route builder in `src/static/routes.ts`: + +```typescript +ROUTES.StoreDetail({ id: '123' }); // → '/store/123' +``` + +Pages can declare static properties: + +```typescript +Page.getLayout = (page: React.ReactNode) => {page}; +Page.requireAuth = true; +Page.title = 'Page Title'; +``` + +### API Layer + +`src/api/` uses a class-based pattern with `APIClient` wrapper (`src/utils/ts/apiClient.ts`): + +1. Define request class in `APIDetail.ts` implementing `APIRequest` +2. Define request/response types in `entity.ts` +3. Export callable function via `APIClient.of(DetailClass)` in `index.ts` + +```typescript +export class Login implements APIRequest { + path = '/user/login'; + method = HTTP_METHOD.POST; + data: LoginRequest; +} +// index.ts +export const login = APIClient.of(Login); +``` + +The APIClient handles token refresh (401), maintenance mode (503), and user type verification (403) automatically. + +### State Management + +- **Server state**: TanStack React Query (`staleTime: 60000`, `retry: false`). SSR via `getServerSideProps` + `HydrationBoundary`. +- **Client state**: Zustand stores in `src/utils/zustand/`. Many stores separate `State` and `Actions` types — follow the existing pattern of each file. + +Zustand stores export selectors: + +```typescript +export const useStateSelector = () => useStore((state) => state.prop); +export const useActions = () => useStore((state) => state.action); +``` + +### Styling + +SCSS with CSS Modules (`[Component].module.scss`) using BEM methodology. Desktop-first approach with mobile overrides via responsive mixins in `src/utils/scss/`: + +```scss +@include media.media-breakpoint(mobile) { + /* breakpoint: 576px */ +} +``` + +### Component Organization + +Feature-based: `src/components/[Feature]/` with co-located hooks in `hooks/` subdirectory and styles. Split responsive views into `MobileView/` and `PCView/` directories when layouts differ. Shared UI in `src/components/ui/`, layouts in `src/components/layout/`, modals in `src/components/modal/`. + +### Layout + +Two layout components in `src/components/layout/`: + +- **`SSRLayout`**: No Suspense wrapping. Used for SSR pages via `getLayout`. +- **`Layout`** (default): Wraps Header/children in Suspense boundaries. Hides Footer in native WebView. + +Misusing these causes hydration errors. + +### Custom Hooks + +Located in `src/utils/hooks/`, organized by category: + +- `auth/` — useAuth, useAutoLogin, useLoginRedirect, useLogout +- `ui/` — useBodyScrollLock, useOutsideClick, useEscapeKeyDown +- `state/` — useBooleanState, useLocalStorage, useWebStorage, useMount +- `layout/` — useMediaQuery, useModalPortal +- `analytics/` — useLogger, useScrollLogging + +### Internal Packages + +- **`@bcsdlab/koin`**: `isKoinError()` type guard, `sendClientError()` (sends errors to internal Slack). +- **`@bcsdlab/utils`**: `cn()` (className merger), `sha256()` (Web Crypto hashing). + +These are internal BCSD Lab packages — do not suggest replacing them with external alternatives. + +### Cookie Management + +Cookie keys are environment-aware via `IS_STAGE` flag in `src/static/url.ts`. Stage and production use different cookie key names (e.g., `STAGE_AUTH_TOKEN_KEY` vs `AUTH_TOKEN_KEY`) and different domains (`.stage.koreatech.in` vs `.koreatech.in`). Always use `COOKIE_KEY` constants and `getCookieDomain()` — never hard-code cookie names or domains. + +### iOS Native Bridge + +WebKit message handlers for token sync between web and native app via `window.webkit.messageHandlers`. Bridge functions in `src/utils/ts/iosBridge.ts`. + +### Analytics + +Generated logging hooks in `src/generated/analytics/` from Notion spec via `yarn log`. Google Analytics + GTM + Sentry error tracking. + +## Code Conventions + +### Imports + +Absolute imports via `*` → `src/*` path mapping. Use `import X from 'components/...'` not `'../../../components/...'`. Relative parent imports (`../*`) are forbidden by ESLint. + +Import order (enforced): React/Next → builtins → external packages → internal (`@/**`) → parent/sibling → types → styles (`.scss`). + +### Naming + +- Components/directories: PascalCase (`ArticleList.tsx`) +- Utilities: camelCase (`apiClient.ts`) +- Hooks: `use[Name].ts` +- Styles: `[Component].module.scss` +- API types: `entity.ts`, API classes: `APIDetail.ts` + +### Formatting + +Prettier: 120 char width, single quotes, trailing commas, 2-space indent. Stylelint enforces `stylelint-config-standard-scss`. + +## PR Review Rules (for claude-code-action) + +Write all review comments in Korean. + +Focus on correctness, regression risk, security, and performance before style. + +Use this output format for every finding: + +- Severity: `[P0]` (blocks merge), `[P1]` (should fix), `[P2]` (suggestion) +- Location: `file:line` +- Why it matters +- Minimal fix suggestion + +### Error Handling + +- Always use `isKoinError()` type guard before accessing error properties on API errors. +- In ErrorBoundary, use `isAxiosError()` type guard to branch handling. + +```typescript +onError: (error) => { + if (isKoinError(error)) { + showToast('error', error.message || 'fallback message'); + } else { + showToast('error', 'fallback message'); + } +}; +``` + +### React Query + +- Query keys as arrays: `['resource', 'action', params]`. +- Prefer `useSuspenseQuery` when a Suspense boundary wraps the component for blocking UI. Use `useQuery` for conditional fetching (`enabled`), background refresh, or non-blocking patterns. +- Invalidate cache via `queryClient.invalidateQueries()` after mutations. +- Every mutation `onError` must follow the error handling pattern above. + +### Analytics Logging + +- All user interactions (click, swipe, page load) must include logging. +- Use the `useLogger()` hook with `team`, `event_label`, `value` structure. +- Define logging constants (`loggingTitle`, `loggingValue`) at the top of the component. + +### General + +- No `console.log` (ESLint warn). Only `console.warn` and `console.error` allowed. +- Import SVGs as React components via `@svgr/webpack`. +- Use `showToast(type, message)` utility instead of calling `toast()` directly. Type: `'success' | 'error' | 'info' | 'warning' | 'default'`. + +### Project-Specific Must-Checks + +- **SSR safety**: Guard `window`, `document`, `localStorage`, `sessionStorage` with browser checks. Verify correct layout usage (`SSRLayout` for SSR pages, `Layout` for client pages). +- **React Query SSR hydration**: Verify prefetch query keys and hydration state keys are consistent. +- **Auth/API stability**: Do not break token refresh lock, 401/403/503 handling, or retry flow. +- **Cookie safety**: Must use `COOKIE_KEY` constants and `getCookieDomain()` — never hard-code cookie names or domains. Verify stage/production separation is preserved. +- **iOS bridge stability**: Preserve `window.webkit` optional chaining and native callback contract. +- **Routing consistency**: Prefer `ROUTES` helpers over hard-coded path strings. +- **Next.js performance**: Flag async waterfalls, unnecessary heavy static imports, and missed dynamic imports. + +### Validation Policy + +- Primary check: `yarn lint`. +- `yarn build` may fail due to environment constraints (missing API access, sandbox limitations); treat as non-blocking unless the changed code directly caused the failure. + +### Do Not Review + +- Pure formatting/import-order noise already covered by lint. +- Generated artifacts only (`analytics.events.json`, `src/generated/**`) unless generation logic changed. +- `@bcsdlab/koin` and `@bcsdlab/utils` are internal packages — do not suggest external replacements. diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..c094a8262 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,82 @@ +language: ko +early_access: true +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + high_level_summary_placeholder: '@coderabbitai summary' + changed_files_summary: true + sequence_diagrams: true + poem: false + review_status: true + collapse_walkthrough: false + path_filters: + - '**' + - '!.next/**' + - '!yarn.lock' + - '!.yarn/**' + - '!.pnp.cjs' + - '!.pnp.loader.mjs' + - '!public/**' + auto_review: + enabled: true + drafts: false + base_branches: + - develop + - main + tools: + shellcheck: + enabled: false + ruff: + enabled: false + biome: + enabled: false + github-checks: + enabled: true + timeout_ms: 120000 + path_instructions: + - path: src/pages/** + instructions: | + 이 디렉토리는 Next.js Pages Router의 페이지 컴포넌트입니다. + 1. 페이지 컴포넌트에서 비즈니스 로직이 과도하게 포함되어 있지 않은지 확인해주세요. + 2. SEO 관련 메타 태그(Head 컴포넌트)가 적절히 설정되어 있는지 확인해주세요. + 3. getServerSideProps/getStaticProps 사용 시 에러 핸들링이 적절한지 확인해주세요. + - path: src/components/** + instructions: | + 1. 컴포넌트의 단일 책임 원칙(SRP)을 준수하고 있는지 확인해주세요. + 2. Props 타입이 명확하게 정의되어 있는지(interface 사용 권장) 확인해주세요. + 3. 불필요한 리렌더링을 유발하는 패턴(인라인 함수, 객체 리터럴 등)이 있는지 확인해주세요. + 4. 접근성(a11y) 관련 속성이 적절히 사용되고 있는지 확인해주세요. + - path: src/api/** + instructions: | + 1. API 호출 시 적절한 에러 핸들링이 되어 있는지 확인해주세요. + 2. 요청/응답 타입이 명확하게 정의되어 있는지 확인해주세요. + 3. API 엔드포인트 URL에 하드코딩된 값이 없는지 확인해주세요. + 4. 인증 토큰 등 민감 정보 처리가 안전한지 확인해주세요. + - path: src/hooks/** + instructions: | + 1. 커스텀 훅의 네이밍이 use 접두사를 따르고 있는지 확인해주세요. + 2. 의존성 배열이 정확하게 설정되어 있는지 확인해주세요. + 3. 메모리 누수 가능성이 있는 패턴(cleanup 미처리)이 없는지 확인해주세요. + - path: src/utils/** + instructions: | + 1. 유틸 함수가 순수 함수로 작성되어 있는지 확인해주세요. + 2. 엣지 케이스(null, undefined, 빈 배열 등) 처리가 되어 있는지 확인해주세요. + - path: src/** + instructions: | + 1. Next.js, React, TypeScript 팀 코드 컨벤션 및 공식 스타일 가이드(React/TS best practices)를 우선적으로 반영하여, 가독성·안정성(Null/에러 처리)·테스트/유지보수 용이성·브라우저/접근성 이슈 등을 검토해주세요. + 2. 최신 React/TypeScript 트렌드, 팀 스타일 통일성, 성능 최적화, 보안 취약점 등도 함께 고려해주세요. + 3. 각 리뷰 포인트별로 문제점과 대안, 장단점을 논리적으로 제시하고, 필요한 경우 예시 코드도 추가해주세요. + 4. 리뷰가 너무 많아서 피로감을 줄 수 있으니, 꼭 필요한 부분에 집중해주고, 나머지는 캡션으로 설명해주세요. + 5. 리뷰 남겨주는 부분은 해당 라인 범위의 코멘트에 작성해주세요. + 6. zustand 스토어 사용 시 selector 패턴을 권장해주세요. + 7. @tanstack/react-query 사용 시 queryKey 컨벤션과 에러/로딩 상태 처리를 확인해주세요. + +chat: + auto_reply: true + +knowledge_base: + web_search: + enabled: true + learnings: + scope: local diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4f238b521..7dc6b3336 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,23 +1,21 @@ - Close #ISSUE_NUMBER - + ## What is this PR? 🔍 -- 기능 : +- 기능 : - issue : # ## Changes 📝 - - ## ScreenShot 📷 ## Test CheckList ✅ - @@ -27,7 +25,6 @@ ## Precaution - ## ✔️ Please check if the PR fulfills these requirements - [ ] It's submitted to the correct branch, not the `develop` branch unconditionally? diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..7ceaa3414 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,158 @@ +name: Deploy + +on: + push: + branches: + - develop + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.11.1' + cache: 'yarn' + + - name: Install dependencies + run: yarn + + - name: Run lint + run: yarn lint + + build-and-deploy: + needs: + - lint + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: ${{ github.ref_name == 'main' && 'production' || 'stage' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.11.1' + cache: 'yarn' + + - name: Set deploy config + run: | + if [ "${{ github.ref_name }}" = "main" ]; then + echo "ARCHIVE_NAME=dist-production.tar.gz" >> $GITHUB_ENV + echo "SENTRY_PROJECT=koin-prod" >> $GITHUB_ENV + echo "DEPLOY_SCRIPT=/usr/local/koin/production/deploy/deploy.sh" >> $GITHUB_ENV + else + echo "ARCHIVE_NAME=dist.tar.gz" >> $GITHUB_ENV + echo "SENTRY_PROJECT=koin-stage" >> $GITHUB_ENV + echo "DEPLOY_SCRIPT=/usr/local/koin/stage/deploy/deploy.sh" >> $GITHUB_ENV + fi + + - name: Create .env + env: + NEXT_PUBLIC_API_PATH: ${{ secrets.NEXT_PUBLIC_API_PATH }} + NEXT_PUBLIC_NAVER_MAPS_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_NAVER_MAPS_CLIENT_ID }} + NEXT_PUBLIC_GOOGLE_ANALYTICS_ID: ${{ secrets.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID }} + NEXT_PUBLIC_GTM_ID: ${{ secrets.NEXT_PUBLIC_GTM_ID }} + NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }} + NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} + run: | + echo "NEXT_PUBLIC_API_PATH=$NEXT_PUBLIC_API_PATH" >> .env + echo "NEXT_PUBLIC_NAVER_MAPS_CLIENT_ID=$NEXT_PUBLIC_NAVER_MAPS_CLIENT_ID" >> .env + echo "NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=$NEXT_PUBLIC_GOOGLE_ANALYTICS_ID" >> .env + echo "NEXT_PUBLIC_GTM_ID=$NEXT_PUBLIC_GTM_ID" >> .env + echo "NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN" >> .env + echo "NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT" >> .env + + - name: Install dependencies + run: yarn + + - name: Build + run: yarn build + env: + NODE_OPTIONS: --max-old-space-size=6144 + SENTRY_ORG: bcsd + SENTRY_PROJECT: ${{ env.SENTRY_PROJECT }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + NEXT_PUBLIC_SENTRY_RELEASE: ${{ github.sha }} + + - name: Create deploy package + run: | + mkdir -p deploy-pkg + cp -r .next deploy-pkg/ + cp -r public deploy-pkg/ 2>/dev/null || true + cp package.json deploy-pkg/ + cp yarn.lock deploy-pkg/ + cp .env deploy-pkg/ + cp next.config.mjs deploy-pkg/ + cp -r .yarn deploy-pkg/ 2>/dev/null || true + cp .pnp.* deploy-pkg/ 2>/dev/null || true + cp .yarnrc.yml deploy-pkg/ 2>/dev/null || true + tar -czf $ARCHIVE_NAME -C deploy-pkg . + rm -rf deploy-pkg + + - name: Setup SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add SSH known hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -p 22222 ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy + run: | + scp -o ConnectTimeout=30 -P 22222 $ARCHIVE_NAME ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/home/ubuntu/koin/web/ + ssh -o ConnectTimeout=30 -p 22222 ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} "bash -lc '$DEPLOY_SCRIPT'" + + notify: + needs: [build-and-deploy] + if: always() + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve actor name + id: resolve_actor + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const developers = JSON.parse(fs.readFileSync(`${{ github.workspace }}/.github/workflows/reviewer.json`)); + const matched = developers.reviewers.find(p => p.githubName === '${{ github.actor }}'); + core.setOutput('actorName', JSON.stringify(matched?.name ?? '${{ github.actor }}')); + + - name: Send Slack Notification + run: | + STATUS="${{ needs.build-and-deploy.result }}" + ENVIRONMENT=$([[ "${{ github.ref_name }}" == "main" ]] && echo "Production" || echo "Stage") + COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | head -1) + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + ACTOR=${{ steps.resolve_actor.outputs.actorName }} + + BODY=$(jq -n \ + --arg status "$STATUS" \ + --arg environment "$ENVIRONMENT" \ + --arg repository "${{ github.repository }}" \ + --arg branch "${{ github.ref_name }}" \ + --arg actor "$ACTOR" \ + --arg commitMessage "$COMMIT_MSG" \ + --arg runUrl "$RUN_URL" \ + '{status: $status, environment: $environment, repository: $repository, branch: $branch, actor: $actor, commitMessage: $commitMessage, runUrl: $runUrl}') + + curl -X POST https://api-slack.internal.bcsdlab.com/api/deploy/frontend \ + -H 'Content-Type: application/json' \ + -d "$BODY" diff --git a/.github/workflows/reviewer.json b/.github/workflows/reviewer.json index 62eb92ce6..07fbe7b60 100644 --- a/.github/workflows/reviewer.json +++ b/.github/workflows/reviewer.json @@ -17,12 +17,8 @@ "githubName": "ff1451" }, { - "name": "곽승주", - "githubName": "Gwak-Seungju" - }, - { - "name": "서예진", - "githubName": "Yejin0070" + "name": "박성주", + "githubName": "ParkSungju01" } ] } diff --git a/.gitignore b/.gitignore index 0d8a28cbe..c18065846 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ tsconfig.tsbuildinfo .idea/* .next -tsconfig.tsbuildinfo \ No newline at end of file +tsconfig.tsbuildinfo +# Sentry Config File +.env.sentry-build-plugin diff --git a/.pnp.cjs b/.pnp.cjs index 3c12cd0d5..0bf6ee377 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -32,11 +32,11 @@ const RAW_RUNTIME_STATE = ["@bcsdlab/utils", "npm:0.0.15"],\ ["@next/eslint-plugin-next", "npm:16.0.0"],\ ["@next/third-parties", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:15.5.2"],\ - ["@sentry/browser", "npm:9.14.0"],\ + ["@notionhq/client", "npm:5.9.0"],\ ["@sentry/cli", "npm:2.45.0"],\ - ["@stomp/stompjs", "npm:7.0.0"],\ + ["@sentry/nextjs", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:10.43.0"],\ ["@svgr/webpack", "npm:8.1.0"],\ - ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.28.6"],\ + ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.90.21"],\ ["@testing-library/jest-dom", "npm:5.17.0"],\ ["@testing-library/react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:13.4.0"],\ ["@testing-library/user-event", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:13.5.0"],\ @@ -50,6 +50,7 @@ const RAW_RUNTIME_STATE = ["@types/react-window", "npm:1.8.8"],\ ["axios", "npm:0.27.2"],\ ["dayjs", "npm:1.11.12"],\ + ["dotenv", "npm:17.2.3"],\ ["embla-carousel-autoplay", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:8.0.4"],\ ["embla-carousel-react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:8.0.4"],\ ["eslint", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:9.38.0"],\ @@ -65,6 +66,7 @@ const RAW_RUNTIME_STATE = ["globals", "npm:16.4.0"],\ ["html-react-parser", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.1.10"],\ ["html2canvas", "npm:1.4.1"],\ + ["idb", "npm:8.0.3"],\ ["jest", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:29.7.0"],\ ["koin_web_recode", "workspace:."],\ ["lottie-react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:2.4.1"],\ @@ -85,6 +87,7 @@ const RAW_RUNTIME_STATE = ["stylelint-config-standard", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:26.0.0"],\ ["stylelint-config-standard-scss", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:4.0.0"],\ ["stylelint-selector-bem-pattern", "npm:2.1.1"],\ + ["tsx", "npm:4.21.0"],\ ["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5"],\ ["typescript-eslint", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:8.46.2"],\ ["web-vitals", "npm:2.1.4"],\ @@ -159,6 +162,16 @@ const RAW_RUNTIME_STATE = ["picocolors", "npm:1.1.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.0", {\ + "packageLocation": "./.yarn/cache/@babel-code-frame-npm-7.29.0-6c4947d913-199e15ff89.zip/node_modules/@babel/code-frame/",\ + "packageDependencies": [\ + ["@babel/code-frame", "npm:7.29.0"],\ + ["@babel/helper-validator-identifier", "npm:7.28.5"],\ + ["js-tokens", "npm:4.0.0"],\ + ["picocolors", "npm:1.1.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/compat-data", [\ @@ -189,6 +202,13 @@ const RAW_RUNTIME_STATE = ["@babel/compat-data", "npm:7.28.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.0", {\ + "packageLocation": "./.yarn/cache/@babel-compat-data-npm-7.29.0-6b4382e79f-7f21beedb9.zip/node_modules/@babel/compat-data/",\ + "packageDependencies": [\ + ["@babel/compat-data", "npm:7.29.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/core", [\ @@ -257,6 +277,28 @@ const RAW_RUNTIME_STATE = ["semver", "npm:6.3.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.0", {\ + "packageLocation": "./.yarn/cache/@babel-core-npm-7.29.0-a74bfc561b-25f4e91688.zip/node_modules/@babel/core/",\ + "packageDependencies": [\ + ["@babel/code-frame", "npm:7.29.0"],\ + ["@babel/core", "npm:7.29.0"],\ + ["@babel/generator", "npm:7.29.1"],\ + ["@babel/helper-compilation-targets", "npm:7.28.6"],\ + ["@babel/helper-module-transforms", "virtual:a74bfc561b28f961f46b2ec8ae406d012b5fbed31a317cc6e0c8e0e4bc61a668944b271114f1150bc3cadae9a39987a6be16fb9362801892abacc23919c76dd7#npm:7.28.6"],\ + ["@babel/helpers", "npm:7.28.6"],\ + ["@babel/parser", "npm:7.29.0"],\ + ["@babel/template", "npm:7.28.6"],\ + ["@babel/traverse", "npm:7.29.0"],\ + ["@babel/types", "npm:7.29.0"],\ + ["@jridgewell/remapping", "npm:2.3.5"],\ + ["convert-source-map", "npm:2.0.0"],\ + ["debug", "virtual:428f325a939c2653ad822eb3d75efb02ac311523dd0d4f9645afc39ea00bd86eceac35a9d59c9b6977d76b670a4ef0ae057ea572338a44729aa592711a8c05a3#npm:4.3.4"],\ + ["gensync", "npm:1.0.0-beta.2"],\ + ["json5", "npm:2.2.3"],\ + ["semver", "npm:6.3.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/generator", [\ @@ -305,6 +347,18 @@ const RAW_RUNTIME_STATE = ["jsesc", "npm:3.0.2"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.1", {\ + "packageLocation": "./.yarn/cache/@babel-generator-npm-7.29.1-b1bf16fe79-61fe4ddd6e.zip/node_modules/@babel/generator/",\ + "packageDependencies": [\ + ["@babel/generator", "npm:7.29.1"],\ + ["@babel/parser", "npm:7.29.0"],\ + ["@babel/types", "npm:7.29.0"],\ + ["@jridgewell/gen-mapping", "npm:0.3.13"],\ + ["@jridgewell/trace-mapping", "npm:0.3.30"],\ + ["jsesc", "npm:3.0.2"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/helper-annotate-as-pure", [\ @@ -353,6 +407,18 @@ const RAW_RUNTIME_STATE = ["semver", "npm:6.3.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.28.6", {\ + "packageLocation": "./.yarn/cache/@babel-helper-compilation-targets-npm-7.28.6-8880f389c9-f512a5aeee.zip/node_modules/@babel/helper-compilation-targets/",\ + "packageDependencies": [\ + ["@babel/compat-data", "npm:7.29.0"],\ + ["@babel/helper-compilation-targets", "npm:7.28.6"],\ + ["@babel/helper-validator-option", "npm:7.27.1"],\ + ["browserslist", "npm:4.24.0"],\ + ["lru-cache", "npm:5.1.1"],\ + ["semver", "npm:6.3.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/helper-create-class-features-plugin", [\ @@ -483,6 +549,15 @@ const RAW_RUNTIME_STATE = ["@babel/types", "npm:7.27.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.28.6", {\ + "packageLocation": "./.yarn/cache/@babel-helper-module-imports-npm-7.28.6-5b95b9145c-64b1380d74.zip/node_modules/@babel/helper-module-imports/",\ + "packageDependencies": [\ + ["@babel/helper-module-imports", "npm:7.28.6"],\ + ["@babel/traverse", "npm:7.29.0"],\ + ["@babel/types", "npm:7.29.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/helper-module-transforms", [\ @@ -514,6 +589,13 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ + ["npm:7.28.6", {\ + "packageLocation": "./.yarn/cache/@babel-helper-module-transforms-npm-7.28.6-5923cf5a95-2e421c7db7.zip/node_modules/@babel/helper-module-transforms/",\ + "packageDependencies": [\ + ["@babel/helper-module-transforms", "npm:7.28.6"]\ + ],\ + "linkType": "SOFT"\ + }],\ ["virtual:341930f80996f4b1e479f0ee33257969b2165bf70992bcc76aa889af20d1c39a2bfc637461175a3ea65d6c75949d04c5fd87140f3b91c8912352de080c45e357#npm:7.25.2", {\ "packageLocation": "./.yarn/__virtual__/@babel-helper-module-transforms-virtual-b14538d1e7/0/cache/@babel-helper-module-transforms-npm-7.25.2-2c8d511580-a3bcf7815f.zip/node_modules/@babel/helper-module-transforms/",\ "packageDependencies": [\ @@ -547,6 +629,22 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ + ["virtual:a74bfc561b28f961f46b2ec8ae406d012b5fbed31a317cc6e0c8e0e4bc61a668944b271114f1150bc3cadae9a39987a6be16fb9362801892abacc23919c76dd7#npm:7.28.6", {\ + "packageLocation": "./.yarn/__virtual__/@babel-helper-module-transforms-virtual-3435e223f6/0/cache/@babel-helper-module-transforms-npm-7.28.6-5923cf5a95-2e421c7db7.zip/node_modules/@babel/helper-module-transforms/",\ + "packageDependencies": [\ + ["@babel/core", "npm:7.29.0"],\ + ["@babel/helper-module-imports", "npm:7.28.6"],\ + ["@babel/helper-module-transforms", "virtual:a74bfc561b28f961f46b2ec8ae406d012b5fbed31a317cc6e0c8e0e4bc61a668944b271114f1150bc3cadae9a39987a6be16fb9362801892abacc23919c76dd7#npm:7.28.6"],\ + ["@babel/helper-validator-identifier", "npm:7.28.5"],\ + ["@babel/traverse", "npm:7.29.0"],\ + ["@types/babel__core", null]\ + ],\ + "packagePeers": [\ + "@babel/core",\ + "@types/babel__core"\ + ],\ + "linkType": "HARD"\ + }],\ ["virtual:cb5fd966cc0f771275597c5aca3369c8164a2bcf171c237ee6cc4846ca8ef9a53870ddd48fbd1c7a680b0f66f2149c2a7694b56f9145852a93e325e1934103f4#npm:7.28.3", {\ "packageLocation": "./.yarn/__virtual__/@babel-helper-module-transforms-virtual-63c6c5653f/0/cache/@babel-helper-module-transforms-npm-7.28.3-7b69ec189a-598fdd8aa5.zip/node_modules/@babel/helper-module-transforms/",\ "packageDependencies": [\ @@ -746,6 +844,13 @@ const RAW_RUNTIME_STATE = ["@babel/helper-validator-identifier", "npm:7.27.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.28.5", {\ + "packageLocation": "./.yarn/cache/@babel-helper-validator-identifier-npm-7.28.5-1953d49d2b-8e5d9b0133.zip/node_modules/@babel/helper-validator-identifier/",\ + "packageDependencies": [\ + ["@babel/helper-validator-identifier", "npm:7.28.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/helper-validator-option", [\ @@ -810,6 +915,15 @@ const RAW_RUNTIME_STATE = ["@babel/types", "npm:7.28.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.28.6", {\ + "packageLocation": "./.yarn/cache/@babel-helpers-npm-7.28.6-682df48628-213485cdff.zip/node_modules/@babel/helpers/",\ + "packageDependencies": [\ + ["@babel/helpers", "npm:7.28.6"],\ + ["@babel/template", "npm:7.28.6"],\ + ["@babel/types", "npm:7.29.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/highlight", [\ @@ -894,6 +1008,14 @@ const RAW_RUNTIME_STATE = ["@babel/types", "npm:7.28.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.0", {\ + "packageLocation": "./.yarn/cache/@babel-parser-npm-7.29.0-c605c63e8b-b1576dca41.zip/node_modules/@babel/parser/",\ + "packageDependencies": [\ + ["@babel/parser", "npm:7.29.0"],\ + ["@babel/types", "npm:7.29.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/plugin-bugfix-firefox-class-in-computed-class-key", [\ @@ -3320,6 +3442,16 @@ const RAW_RUNTIME_STATE = ["@babel/types", "npm:7.27.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.28.6", {\ + "packageLocation": "./.yarn/cache/@babel-template-npm-7.28.6-bff3bc3923-0ad6e32bf1.zip/node_modules/@babel/template/",\ + "packageDependencies": [\ + ["@babel/code-frame", "npm:7.29.0"],\ + ["@babel/parser", "npm:7.29.0"],\ + ["@babel/template", "npm:7.28.6"],\ + ["@babel/types", "npm:7.29.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/traverse", [\ @@ -3392,6 +3524,20 @@ const RAW_RUNTIME_STATE = ["debug", "virtual:428f325a939c2653ad822eb3d75efb02ac311523dd0d4f9645afc39ea00bd86eceac35a9d59c9b6977d76b670a4ef0ae057ea572338a44729aa592711a8c05a3#npm:4.3.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.0", {\ + "packageLocation": "./.yarn/cache/@babel-traverse-npm-7.29.0-85d5d916b6-3a0d0438f1.zip/node_modules/@babel/traverse/",\ + "packageDependencies": [\ + ["@babel/code-frame", "npm:7.29.0"],\ + ["@babel/generator", "npm:7.29.1"],\ + ["@babel/helper-globals", "npm:7.28.0"],\ + ["@babel/parser", "npm:7.29.0"],\ + ["@babel/template", "npm:7.28.6"],\ + ["@babel/traverse", "npm:7.29.0"],\ + ["@babel/types", "npm:7.29.0"],\ + ["debug", "virtual:428f325a939c2653ad822eb3d75efb02ac311523dd0d4f9645afc39ea00bd86eceac35a9d59c9b6977d76b670a4ef0ae057ea572338a44729aa592711a8c05a3#npm:4.3.4"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/types", [\ @@ -3451,6 +3597,15 @@ const RAW_RUNTIME_STATE = ["@babel/types", "npm:7.28.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.0", {\ + "packageLocation": "./.yarn/cache/@babel-types-npm-7.29.0-6c2fa77581-bfc2b21121.zip/node_modules/@babel/types/",\ + "packageDependencies": [\ + ["@babel/helper-string-parser", "npm:7.27.1"],\ + ["@babel/helper-validator-identifier", "npm:7.28.5"],\ + ["@babel/types", "npm:7.29.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@bcoe/v8-coverage", [\ @@ -3633,6 +3788,240 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@esbuild/aix-ppc64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-aix-ppc64-npm-0.27.2-345b18ab38/node_modules/@esbuild/aix-ppc64/",\ + "packageDependencies": [\ + ["@esbuild/aix-ppc64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/android-arm", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-android-arm-npm-0.27.2-b9ce8adb94/node_modules/@esbuild/android-arm/",\ + "packageDependencies": [\ + ["@esbuild/android-arm", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/android-arm64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-android-arm64-npm-0.27.2-15df2cdd67/node_modules/@esbuild/android-arm64/",\ + "packageDependencies": [\ + ["@esbuild/android-arm64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/android-x64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-android-x64-npm-0.27.2-66d99a6933/node_modules/@esbuild/android-x64/",\ + "packageDependencies": [\ + ["@esbuild/android-x64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/darwin-arm64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-darwin-arm64-npm-0.27.2-d675c4a521/node_modules/@esbuild/darwin-arm64/",\ + "packageDependencies": [\ + ["@esbuild/darwin-arm64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/darwin-x64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-darwin-x64-npm-0.27.2-ae63bf405f/node_modules/@esbuild/darwin-x64/",\ + "packageDependencies": [\ + ["@esbuild/darwin-x64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/freebsd-arm64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-freebsd-arm64-npm-0.27.2-e37daed3be/node_modules/@esbuild/freebsd-arm64/",\ + "packageDependencies": [\ + ["@esbuild/freebsd-arm64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/freebsd-x64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-freebsd-x64-npm-0.27.2-4cb2e19a78/node_modules/@esbuild/freebsd-x64/",\ + "packageDependencies": [\ + ["@esbuild/freebsd-x64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-arm", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-arm-npm-0.27.2-43d77dd61a/node_modules/@esbuild/linux-arm/",\ + "packageDependencies": [\ + ["@esbuild/linux-arm", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-arm64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-arm64-npm-0.27.2-bf1b0979ac/node_modules/@esbuild/linux-arm64/",\ + "packageDependencies": [\ + ["@esbuild/linux-arm64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-ia32", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-ia32-npm-0.27.2-9e57150846/node_modules/@esbuild/linux-ia32/",\ + "packageDependencies": [\ + ["@esbuild/linux-ia32", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-loong64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-loong64-npm-0.27.2-a322ec9c1d/node_modules/@esbuild/linux-loong64/",\ + "packageDependencies": [\ + ["@esbuild/linux-loong64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-mips64el", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-mips64el-npm-0.27.2-9b26d4ee73/node_modules/@esbuild/linux-mips64el/",\ + "packageDependencies": [\ + ["@esbuild/linux-mips64el", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-ppc64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-ppc64-npm-0.27.2-28d849768e/node_modules/@esbuild/linux-ppc64/",\ + "packageDependencies": [\ + ["@esbuild/linux-ppc64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-riscv64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-riscv64-npm-0.27.2-73c96cf77f/node_modules/@esbuild/linux-riscv64/",\ + "packageDependencies": [\ + ["@esbuild/linux-riscv64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-s390x", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-s390x-npm-0.27.2-1b2065e648/node_modules/@esbuild/linux-s390x/",\ + "packageDependencies": [\ + ["@esbuild/linux-s390x", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/linux-x64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-linux-x64-npm-0.27.2-11f1a3d9db/node_modules/@esbuild/linux-x64/",\ + "packageDependencies": [\ + ["@esbuild/linux-x64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/netbsd-arm64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-netbsd-arm64-npm-0.27.2-dd6c103966/node_modules/@esbuild/netbsd-arm64/",\ + "packageDependencies": [\ + ["@esbuild/netbsd-arm64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/netbsd-x64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-netbsd-x64-npm-0.27.2-1e03e8a7a5/node_modules/@esbuild/netbsd-x64/",\ + "packageDependencies": [\ + ["@esbuild/netbsd-x64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/openbsd-arm64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-openbsd-arm64-npm-0.27.2-1d7a76cae4/node_modules/@esbuild/openbsd-arm64/",\ + "packageDependencies": [\ + ["@esbuild/openbsd-arm64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/openbsd-x64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-openbsd-x64-npm-0.27.2-27238acba8/node_modules/@esbuild/openbsd-x64/",\ + "packageDependencies": [\ + ["@esbuild/openbsd-x64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/openharmony-arm64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-openharmony-arm64-npm-0.27.2-b815985320/node_modules/@esbuild/openharmony-arm64/",\ + "packageDependencies": [\ + ["@esbuild/openharmony-arm64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/sunos-x64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-sunos-x64-npm-0.27.2-fb3c4c523d/node_modules/@esbuild/sunos-x64/",\ + "packageDependencies": [\ + ["@esbuild/sunos-x64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/win32-arm64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-win32-arm64-npm-0.27.2-78a0e828ec/node_modules/@esbuild/win32-arm64/",\ + "packageDependencies": [\ + ["@esbuild/win32-arm64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/win32-ia32", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-win32-ia32-npm-0.27.2-f7488076af/node_modules/@esbuild/win32-ia32/",\ + "packageDependencies": [\ + ["@esbuild/win32-ia32", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@esbuild/win32-x64", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/@esbuild-win32-x64-npm-0.27.2-fb03408001/node_modules/@esbuild/win32-x64/",\ + "packageDependencies": [\ + ["@esbuild/win32-x64", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@eslint-community/eslint-utils", [\ ["npm:4.9.0", {\ "packageLocation": "./.yarn/cache/@eslint-community-eslint-utils-npm-4.9.0-fe45a08548-89b1eb3137.zip/node_modules/@eslint-community/eslint-utils/",\ @@ -3744,6 +4133,32 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@fastify/otel", [\ + ["npm:0.16.0", {\ + "packageLocation": "./.yarn/cache/@fastify-otel-npm-0.16.0-d2ae32e4f2-b8a4e12285.zip/node_modules/@fastify/otel/",\ + "packageDependencies": [\ + ["@fastify/otel", "npm:0.16.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.16.0", {\ + "packageLocation": "./.yarn/__virtual__/@fastify-otel-virtual-19782a288c/0/cache/@fastify-otel-npm-0.16.0-d2ae32e4f2-b8a4e12285.zip/node_modules/@fastify/otel/",\ + "packageDependencies": [\ + ["@fastify/otel", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.16.0"],\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:19782a288caeaa3b45fee2c62bcfc6d96ced430e66cb4c89bd8b79c34272db641e47cecd3ac1b64d83f0ef0e1d27b8fbccaef51c3c02646d4366a4dd689a5f39#npm:0.208.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null],\ + ["minimatch", "npm:10.2.4"]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@humanfs/core", [\ ["npm:0.19.1", {\ "packageLocation": "./.yarn/cache/@humanfs-core-npm-0.19.1-e2e7aaeb6e-270d936be4.zip/node_modules/@humanfs/core/",\ @@ -4570,6 +4985,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@notionhq/client", [\ + ["npm:5.9.0", {\ + "packageLocation": "./.yarn/cache/@notionhq-client-npm-5.9.0-ed780ff571-337e2377fb.zip/node_modules/@notionhq/client/",\ + "packageDependencies": [\ + ["@notionhq/client", "npm:5.9.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@npmcli/agent", [\ ["npm:2.2.1", {\ "packageLocation": "./.yarn/cache/@npmcli-agent-npm-2.2.1-8af33193ae-d4a48128f6.zip/node_modules/@npmcli/agent/",\ @@ -4594,20 +5018,1127 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@pkgjs/parseargs", [\ - ["npm:0.11.0", {\ + ["@opentelemetry/api", [\ + ["npm:1.9.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-api-npm-1.9.0-7d0560d0dd-a607f0eef9.zip/node_modules/@opentelemetry/api/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/api-logs", [\ + ["npm:0.207.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-api-logs-npm-0.207.0-e198d835d9-d50251e34f.zip/node_modules/@opentelemetry/api-logs/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/api-logs", "npm:0.207.0"]\ + ],\ + "linkType": "HARD"\ + }],\ + ["npm:0.208.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-api-logs-npm-0.208.0-3af8bf5803-ae339416a2.zip/node_modules/@opentelemetry/api-logs/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/api-logs", "npm:0.208.0"]\ + ],\ + "linkType": "HARD"\ + }],\ + ["npm:0.211.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-api-logs-npm-0.211.0-1661a3c311-d2e7ac9d99.zip/node_modules/@opentelemetry/api-logs/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/api-logs", "npm:0.211.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/context-async-hooks", [\ + ["npm:2.6.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-context-async-hooks-npm-2.6.0-18d63e1e37-a1f746fb9b.zip/node_modules/@opentelemetry/context-async-hooks/",\ + "packageDependencies": [\ + ["@opentelemetry/context-async-hooks", "npm:2.6.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-context-async-hooks-virtual-34beb95987/0/cache/@opentelemetry-context-async-hooks-npm-2.6.0-18d63e1e37-a1f746fb9b.zip/node_modules/@opentelemetry/context-async-hooks/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/context-async-hooks", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/core", [\ + ["npm:2.5.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-core-npm-2.5.0-00e0987751-62cb4721af.zip/node_modules/@opentelemetry/core/",\ + "packageDependencies": [\ + ["@opentelemetry/core", "npm:2.5.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["npm:2.6.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-core-npm-2.6.0-bf4a2b221e-21c017cc68.zip/node_modules/@opentelemetry/core/",\ + "packageDependencies": [\ + ["@opentelemetry/core", "npm:2.6.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:2ae2ccd0a0fae6c7b31311cdd50e0189e948a912aa2c738352dc7d7cdaa2639e95d95da7b8658de1f6b4a39fb313f78c94f040a8061075bb7ec21eb8abb14e58#npm:2.5.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-core-virtual-a8b2d32fba/0/cache/@opentelemetry-core-npm-2.5.0-00e0987751-62cb4721af.zip/node_modules/@opentelemetry/core/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:2ae2ccd0a0fae6c7b31311cdd50e0189e948a912aa2c738352dc7d7cdaa2639e95d95da7b8658de1f6b4a39fb313f78c94f040a8061075bb7ec21eb8abb14e58#npm:2.5.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-core-virtual-4cb0ba78a3/0/cache/@opentelemetry-core-npm-2.6.0-bf4a2b221e-21c017cc68.zip/node_modules/@opentelemetry/core/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation", [\ + ["npm:0.207.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-npm-0.207.0-d254f2d9c8-ea9b9a7324.zip/node_modules/@opentelemetry/instrumentation/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation", "npm:0.207.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["npm:0.208.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-npm-0.208.0-3730549526-0591121c1b.zip/node_modules/@opentelemetry/instrumentation/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation", "npm:0.208.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["npm:0.211.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-npm-0.211.0-1431153acb-d9efa3bf91.zip/node_modules/@opentelemetry/instrumentation/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation", "npm:0.211.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:19782a288caeaa3b45fee2c62bcfc6d96ced430e66cb4c89bd8b79c34272db641e47cecd3ac1b64d83f0ef0e1d27b8fbccaef51c3c02646d4366a4dd689a5f39#npm:0.208.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-virtual-16dcff303e/0/cache/@opentelemetry-instrumentation-npm-0.208.0-3730549526-0591121c1b.zip/node_modules/@opentelemetry/instrumentation/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/api-logs", "npm:0.208.0"],\ + ["@opentelemetry/instrumentation", "virtual:19782a288caeaa3b45fee2c62bcfc6d96ced430e66cb4c89bd8b79c34272db641e47cecd3ac1b64d83f0ef0e1d27b8fbccaef51c3c02646d4366a4dd689a5f39#npm:0.208.0"],\ + ["@types/opentelemetry__api", null],\ + ["import-in-the-middle", "npm:2.0.6"],\ + ["require-in-the-middle", "npm:8.0.1"]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-virtual-b33ccfda94/0/cache/@opentelemetry-instrumentation-npm-0.211.0-1431153acb-d9efa3bf91.zip/node_modules/@opentelemetry/instrumentation/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/api-logs", "npm:0.211.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@types/opentelemetry__api", null],\ + ["import-in-the-middle", "npm:2.0.6"],\ + ["require-in-the-middle", "npm:8.0.1"]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }],\ + ["virtual:d73dd3ace88ff0cbca495607521e724b15a8c2f3484adb0683fc11ecf6b5227b56117ac38b6e0e823755c66790d49f9b3a128591a40e59fcd06f3e7d43c7ebf3#npm:0.207.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-virtual-df8ec26b3d/0/cache/@opentelemetry-instrumentation-npm-0.207.0-d254f2d9c8-ea9b9a7324.zip/node_modules/@opentelemetry/instrumentation/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/api-logs", "npm:0.207.0"],\ + ["@opentelemetry/instrumentation", "virtual:d73dd3ace88ff0cbca495607521e724b15a8c2f3484adb0683fc11ecf6b5227b56117ac38b6e0e823755c66790d49f9b3a128591a40e59fcd06f3e7d43c7ebf3#npm:0.207.0"],\ + ["@types/opentelemetry__api", null],\ + ["import-in-the-middle", "npm:2.0.6"],\ + ["require-in-the-middle", "npm:8.0.1"]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-amqplib", [\ + ["npm:0.58.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-amqplib-npm-0.58.0-4a0bde0e6c-de6d6cd239.zip/node_modules/@opentelemetry/instrumentation-amqplib/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-amqplib", "npm:0.58.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.58.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-amqplib-virtual-4b7b7bfea4/0/cache/@opentelemetry-instrumentation-amqplib-npm-0.58.0-4a0bde0e6c-de6d6cd239.zip/node_modules/@opentelemetry/instrumentation-amqplib/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-amqplib", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.58.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-connect", [\ + ["npm:0.54.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-connect-npm-0.54.0-5f669c7fc1-335e42610a.zip/node_modules/@opentelemetry/instrumentation-connect/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-connect", "npm:0.54.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.54.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-connect-virtual-8dacfbcfed/0/cache/@opentelemetry-instrumentation-connect-npm-0.54.0-5f669c7fc1-335e42610a.zip/node_modules/@opentelemetry/instrumentation-connect/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-connect", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.54.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/connect", "npm:3.4.38"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-dataloader", [\ + ["npm:0.28.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-dataloader-npm-0.28.0-90e63b72ad-e2c30ee9d4.zip/node_modules/@opentelemetry/instrumentation-dataloader/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-dataloader", "npm:0.28.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.28.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-dataloader-virtual-cdd8358e3f/0/cache/@opentelemetry-instrumentation-dataloader-npm-0.28.0-90e63b72ad-e2c30ee9d4.zip/node_modules/@opentelemetry/instrumentation-dataloader/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-dataloader", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.28.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-express", [\ + ["npm:0.59.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-express-npm-0.59.0-4c871e3957-a7da4e4942.zip/node_modules/@opentelemetry/instrumentation-express/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-express", "npm:0.59.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-express-virtual-65248b975f/0/cache/@opentelemetry-instrumentation-express-npm-0.59.0-4c871e3957-a7da4e4942.zip/node_modules/@opentelemetry/instrumentation-express/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-express", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-fs", [\ + ["npm:0.30.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-fs-npm-0.30.0-3f3c0c2d74-5bae626d15.zip/node_modules/@opentelemetry/instrumentation-fs/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-fs", "npm:0.30.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.30.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-fs-virtual-030cd59300/0/cache/@opentelemetry-instrumentation-fs-npm-0.30.0-3f3c0c2d74-5bae626d15.zip/node_modules/@opentelemetry/instrumentation-fs/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-fs", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.30.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-generic-pool", [\ + ["npm:0.54.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-generic-pool-npm-0.54.0-f61269cfac-3a92649c14.zip/node_modules/@opentelemetry/instrumentation-generic-pool/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-generic-pool", "npm:0.54.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.54.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-generic-pool-virtual-7fd415f15a/0/cache/@opentelemetry-instrumentation-generic-pool-npm-0.54.0-f61269cfac-3a92649c14.zip/node_modules/@opentelemetry/instrumentation-generic-pool/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-generic-pool", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.54.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-graphql", [\ + ["npm:0.58.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-graphql-npm-0.58.0-f8600b827a-a87490490a.zip/node_modules/@opentelemetry/instrumentation-graphql/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-graphql", "npm:0.58.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.58.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-graphql-virtual-ddcda6dc0d/0/cache/@opentelemetry-instrumentation-graphql-npm-0.58.0-f8600b827a-a87490490a.zip/node_modules/@opentelemetry/instrumentation-graphql/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-graphql", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.58.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-hapi", [\ + ["npm:0.57.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-hapi-npm-0.57.0-8c15961caf-142684b85a.zip/node_modules/@opentelemetry/instrumentation-hapi/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-hapi", "npm:0.57.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-hapi-virtual-95fb286b65/0/cache/@opentelemetry-instrumentation-hapi-npm-0.57.0-8c15961caf-142684b85a.zip/node_modules/@opentelemetry/instrumentation-hapi/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-hapi", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-http", [\ + ["npm:0.211.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-http-npm-0.211.0-daf732bd8d-f20ca2e9f4.zip/node_modules/@opentelemetry/instrumentation-http/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-http", "npm:0.211.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-http-virtual-2ae2ccd0a0/0/cache/@opentelemetry-instrumentation-http-npm-0.211.0-daf732bd8d-f20ca2e9f4.zip/node_modules/@opentelemetry/instrumentation-http/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:2ae2ccd0a0fae6c7b31311cdd50e0189e948a912aa2c738352dc7d7cdaa2639e95d95da7b8658de1f6b4a39fb313f78c94f040a8061075bb7ec21eb8abb14e58#npm:2.5.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-http", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null],\ + ["forwarded-parse", "npm:2.1.2"]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-ioredis", [\ + ["npm:0.59.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-ioredis-npm-0.59.0-428da74c41-59ba1ccf7f.zip/node_modules/@opentelemetry/instrumentation-ioredis/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-ioredis", "npm:0.59.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-ioredis-virtual-cb6ae2bd1b/0/cache/@opentelemetry-instrumentation-ioredis-npm-0.59.0-428da74c41-59ba1ccf7f.zip/node_modules/@opentelemetry/instrumentation-ioredis/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-ioredis", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0"],\ + ["@opentelemetry/redis-common", "npm:0.38.2"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-kafkajs", [\ + ["npm:0.20.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-kafkajs-npm-0.20.0-acbf67f0ec-f6d67ab1fc.zip/node_modules/@opentelemetry/instrumentation-kafkajs/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-kafkajs", "npm:0.20.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.20.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-kafkajs-virtual-a7163582fe/0/cache/@opentelemetry-instrumentation-kafkajs-npm-0.20.0-acbf67f0ec-f6d67ab1fc.zip/node_modules/@opentelemetry/instrumentation-kafkajs/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-kafkajs", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.20.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-knex", [\ + ["npm:0.55.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-knex-npm-0.55.0-ba0c29fdf4-02d9a3c194.zip/node_modules/@opentelemetry/instrumentation-knex/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-knex", "npm:0.55.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.55.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-knex-virtual-26cf04789d/0/cache/@opentelemetry-instrumentation-knex-npm-0.55.0-ba0c29fdf4-02d9a3c194.zip/node_modules/@opentelemetry/instrumentation-knex/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-knex", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.55.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-koa", [\ + ["npm:0.59.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-koa-npm-0.59.0-9edebdc2da-604468baf6.zip/node_modules/@opentelemetry/instrumentation-koa/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-koa", "npm:0.59.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-koa-virtual-6640f3112a/0/cache/@opentelemetry-instrumentation-koa-npm-0.59.0-9edebdc2da-604468baf6.zip/node_modules/@opentelemetry/instrumentation-koa/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-koa", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-lru-memoizer", [\ + ["npm:0.55.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-lru-memoizer-npm-0.55.0-cdc9d61fba-6c2594032e.zip/node_modules/@opentelemetry/instrumentation-lru-memoizer/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-lru-memoizer", "npm:0.55.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.55.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-lru-memoizer-virtual-ca2c13b4bf/0/cache/@opentelemetry-instrumentation-lru-memoizer-npm-0.55.0-cdc9d61fba-6c2594032e.zip/node_modules/@opentelemetry/instrumentation-lru-memoizer/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-lru-memoizer", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.55.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-mongodb", [\ + ["npm:0.64.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-mongodb-npm-0.64.0-1c30b58c91-ecaef6f687.zip/node_modules/@opentelemetry/instrumentation-mongodb/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-mongodb", "npm:0.64.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.64.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-mongodb-virtual-e2c1c36c7c/0/cache/@opentelemetry-instrumentation-mongodb-npm-0.64.0-1c30b58c91-ecaef6f687.zip/node_modules/@opentelemetry/instrumentation-mongodb/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-mongodb", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.64.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-mongoose", [\ + ["npm:0.57.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-mongoose-npm-0.57.0-aec108899e-a64878d3a6.zip/node_modules/@opentelemetry/instrumentation-mongoose/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-mongoose", "npm:0.57.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-mongoose-virtual-22af329dbe/0/cache/@opentelemetry-instrumentation-mongoose-npm-0.57.0-aec108899e-a64878d3a6.zip/node_modules/@opentelemetry/instrumentation-mongoose/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-mongoose", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-mysql", [\ + ["npm:0.57.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-mysql-npm-0.57.0-efcfbde38c-d36cfca4f0.zip/node_modules/@opentelemetry/instrumentation-mysql/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-mysql", "npm:0.57.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-mysql-virtual-19713621c5/0/cache/@opentelemetry-instrumentation-mysql-npm-0.57.0-efcfbde38c-d36cfca4f0.zip/node_modules/@opentelemetry/instrumentation-mysql/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-mysql", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/mysql", "npm:2.15.27"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-mysql2", [\ + ["npm:0.57.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-mysql2-npm-0.57.0-530b730937-b1d120318c.zip/node_modules/@opentelemetry/instrumentation-mysql2/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-mysql2", "npm:0.57.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-mysql2-virtual-02009ae54e/0/cache/@opentelemetry-instrumentation-mysql2-npm-0.57.0-530b730937-b1d120318c.zip/node_modules/@opentelemetry/instrumentation-mysql2/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-mysql2", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@opentelemetry/sql-common", "virtual:02009ae54eace04a602fcbd9c53f4068883e76d0f127558af56479c4cadd277e2322e55ba16c4c9f64b986c4e6e6934e20e51fd823816bed0cd7c91fb0f79801#npm:0.41.2"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-pg", [\ + ["npm:0.63.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-pg-npm-0.63.0-c7392f516f-f937e1e535.zip/node_modules/@opentelemetry/instrumentation-pg/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-pg", "npm:0.63.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.63.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-pg-virtual-4c3bbba716/0/cache/@opentelemetry-instrumentation-pg-npm-0.63.0-c7392f516f-f937e1e535.zip/node_modules/@opentelemetry/instrumentation-pg/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-pg", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.63.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@opentelemetry/sql-common", "virtual:02009ae54eace04a602fcbd9c53f4068883e76d0f127558af56479c4cadd277e2322e55ba16c4c9f64b986c4e6e6934e20e51fd823816bed0cd7c91fb0f79801#npm:0.41.2"],\ + ["@types/opentelemetry__api", null],\ + ["@types/pg", "npm:8.15.6"],\ + ["@types/pg-pool", "npm:2.0.7"]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-redis", [\ + ["npm:0.59.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-redis-npm-0.59.0-5877bd7b80-b5f455901d.zip/node_modules/@opentelemetry/instrumentation-redis/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-redis", "npm:0.59.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-redis-virtual-c3a0ee1848/0/cache/@opentelemetry-instrumentation-redis-npm-0.59.0-5877bd7b80-b5f455901d.zip/node_modules/@opentelemetry/instrumentation-redis/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-redis", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0"],\ + ["@opentelemetry/redis-common", "npm:0.38.2"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-tedious", [\ + ["npm:0.30.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-tedious-npm-0.30.0-8766617d06-d8f62caa55.zip/node_modules/@opentelemetry/instrumentation-tedious/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-tedious", "npm:0.30.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.30.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-tedious-virtual-0827249ce7/0/cache/@opentelemetry-instrumentation-tedious-npm-0.30.0-8766617d06-d8f62caa55.zip/node_modules/@opentelemetry/instrumentation-tedious/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-tedious", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.30.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null],\ + ["@types/tedious", "npm:4.0.14"]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/instrumentation-undici", [\ + ["npm:0.21.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-instrumentation-undici-npm-0.21.0-c2e5951e5b-e2e39d7ef3.zip/node_modules/@opentelemetry/instrumentation-undici/",\ + "packageDependencies": [\ + ["@opentelemetry/instrumentation-undici", "npm:0.21.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.21.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-instrumentation-undici-virtual-fc61915812/0/cache/@opentelemetry-instrumentation-undici-npm-0.21.0-c2e5951e5b-e2e39d7ef3.zip/node_modules/@opentelemetry/instrumentation-undici/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-undici", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.21.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/redis-common", [\ + ["npm:0.38.2", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-redis-common-npm-0.38.2-2a4cd967c7-2a4f992572.zip/node_modules/@opentelemetry/redis-common/",\ + "packageDependencies": [\ + ["@opentelemetry/redis-common", "npm:0.38.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/resources", [\ + ["npm:2.6.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-resources-npm-2.6.0-100fb3be54-837e76911d.zip/node_modules/@opentelemetry/resources/",\ + "packageDependencies": [\ + ["@opentelemetry/resources", "npm:2.6.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-resources-virtual-894fd09ade/0/cache/@opentelemetry-resources-npm-2.6.0-100fb3be54-837e76911d.zip/node_modules/@opentelemetry/resources/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/resources", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/sdk-trace-base", [\ + ["npm:2.6.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-sdk-trace-base-npm-2.6.0-e7da6f6e16-8ca3c1c4d7.zip/node_modules/@opentelemetry/sdk-trace-base/",\ + "packageDependencies": [\ + ["@opentelemetry/sdk-trace-base", "npm:2.6.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-sdk-trace-base-virtual-eeb202df32/0/cache/@opentelemetry-sdk-trace-base-npm-2.6.0-e7da6f6e16-8ca3c1c4d7.zip/node_modules/@opentelemetry/sdk-trace-base/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/resources", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/sdk-trace-base", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/semantic-conventions", [\ + ["npm:1.40.0", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-semantic-conventions-npm-1.40.0-d0b94a9adb-edb5889459.zip/node_modules/@opentelemetry/semantic-conventions/",\ + "packageDependencies": [\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@opentelemetry/sql-common", [\ + ["npm:0.41.2", {\ + "packageLocation": "./.yarn/cache/@opentelemetry-sql-common-npm-0.41.2-1bbdc61904-3d57d5162c.zip/node_modules/@opentelemetry/sql-common/",\ + "packageDependencies": [\ + ["@opentelemetry/sql-common", "npm:0.41.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:02009ae54eace04a602fcbd9c53f4068883e76d0f127558af56479c4cadd277e2322e55ba16c4c9f64b986c4e6e6934e20e51fd823816bed0cd7c91fb0f79801#npm:0.41.2", {\ + "packageLocation": "./.yarn/__virtual__/@opentelemetry-sql-common-virtual-1fbe94d6be/0/cache/@opentelemetry-sql-common-npm-0.41.2-1bbdc61904-3d57d5162c.zip/node_modules/@opentelemetry/sql-common/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/sql-common", "virtual:02009ae54eace04a602fcbd9c53f4068883e76d0f127558af56479c4cadd277e2322e55ba16c4c9f64b986c4e6e6934e20e51fd823816bed0cd7c91fb0f79801#npm:0.41.2"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@pkgjs/parseargs", [\ + ["npm:0.11.0", {\ "packageLocation": "./.yarn/cache/@pkgjs-parseargs-npm-0.11.0-cd2a3fe948-115e8ceeec.zip/node_modules/@pkgjs/parseargs/",\ "packageDependencies": [\ - ["@pkgjs/parseargs", "npm:0.11.0"]\ + ["@pkgjs/parseargs", "npm:0.11.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@pkgr/core", [\ + ["npm:0.2.9", {\ + "packageLocation": "./.yarn/cache/@pkgr-core-npm-0.2.9-c65fc09be3-bb2fb86977.zip/node_modules/@pkgr/core/",\ + "packageDependencies": [\ + ["@pkgr/core", "npm:0.2.9"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@prisma/instrumentation", [\ + ["npm:7.2.0", {\ + "packageLocation": "./.yarn/cache/@prisma-instrumentation-npm-7.2.0-3bb371cc25-a02d16543f.zip/node_modules/@prisma/instrumentation/",\ + "packageDependencies": [\ + ["@prisma/instrumentation", "npm:7.2.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:7.2.0", {\ + "packageLocation": "./.yarn/__virtual__/@prisma-instrumentation-virtual-d73dd3ace8/0/cache/@prisma-instrumentation-npm-7.2.0-3bb371cc25-a02d16543f.zip/node_modules/@prisma/instrumentation/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/instrumentation", "virtual:d73dd3ace88ff0cbca495607521e724b15a8c2f3484adb0683fc11ecf6b5227b56117ac38b6e0e823755c66790d49f9b3a128591a40e59fcd06f3e7d43c7ebf3#npm:0.207.0"],\ + ["@prisma/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:7.2.0"],\ + ["@types/opentelemetry__api", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@types/opentelemetry__api"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/plugin-commonjs", [\ + ["npm:28.0.1", {\ + "packageLocation": "./.yarn/cache/@rollup-plugin-commonjs-npm-28.0.1-5224cbb009-e01d26ce41.zip/node_modules/@rollup/plugin-commonjs/",\ + "packageDependencies": [\ + ["@rollup/plugin-commonjs", "npm:28.0.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:28.0.1", {\ + "packageLocation": "./.yarn/__virtual__/@rollup-plugin-commonjs-virtual-7080bd8542/0/cache/@rollup-plugin-commonjs-npm-28.0.1-5224cbb009-e01d26ce41.zip/node_modules/@rollup/plugin-commonjs/",\ + "packageDependencies": [\ + ["@rollup/plugin-commonjs", "virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:28.0.1"],\ + ["@rollup/pluginutils", "virtual:7080bd85426952f99562f3b207534c3738369d7585127ef68f72f62f38cfeb3fa02f2210d4b3dbaa7b672e7d677090d1dfa93aa4924900fc190ba59b1d57b99c#npm:5.3.0"],\ + ["@types/rollup", null],\ + ["commondir", "npm:1.0.1"],\ + ["estree-walker", "npm:2.0.2"],\ + ["fdir", "virtual:7080bd85426952f99562f3b207534c3738369d7585127ef68f72f62f38cfeb3fa02f2210d4b3dbaa7b672e7d677090d1dfa93aa4924900fc190ba59b1d57b99c#npm:6.5.0"],\ + ["is-reference", "npm:1.2.1"],\ + ["magic-string", "npm:0.30.21"],\ + ["picomatch", "npm:4.0.3"],\ + ["rollup", "npm:4.59.0"]\ + ],\ + "packagePeers": [\ + "@types/rollup",\ + "rollup"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/pluginutils", [\ + ["npm:5.3.0", {\ + "packageLocation": "./.yarn/cache/@rollup-pluginutils-npm-5.3.0-41141e497e-6c7dbab90e.zip/node_modules/@rollup/pluginutils/",\ + "packageDependencies": [\ + ["@rollup/pluginutils", "npm:5.3.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:7080bd85426952f99562f3b207534c3738369d7585127ef68f72f62f38cfeb3fa02f2210d4b3dbaa7b672e7d677090d1dfa93aa4924900fc190ba59b1d57b99c#npm:5.3.0", {\ + "packageLocation": "./.yarn/__virtual__/@rollup-pluginutils-virtual-bc83bcf634/0/cache/@rollup-pluginutils-npm-5.3.0-41141e497e-6c7dbab90e.zip/node_modules/@rollup/pluginutils/",\ + "packageDependencies": [\ + ["@rollup/pluginutils", "virtual:7080bd85426952f99562f3b207534c3738369d7585127ef68f72f62f38cfeb3fa02f2210d4b3dbaa7b672e7d677090d1dfa93aa4924900fc190ba59b1d57b99c#npm:5.3.0"],\ + ["@types/estree", "npm:1.0.8"],\ + ["@types/rollup", null],\ + ["estree-walker", "npm:2.0.2"],\ + ["picomatch", "npm:4.0.3"],\ + ["rollup", "npm:4.59.0"]\ + ],\ + "packagePeers": [\ + "@types/rollup",\ + "rollup"\ ],\ "linkType": "HARD"\ }]\ ]],\ - ["@pkgr/core", [\ - ["npm:0.2.9", {\ - "packageLocation": "./.yarn/cache/@pkgr-core-npm-0.2.9-c65fc09be3-bb2fb86977.zip/node_modules/@pkgr/core/",\ + ["@rollup/rollup-android-arm-eabi", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-android-arm-eabi-npm-4.59.0-910d6ebd8c/node_modules/@rollup/rollup-android-arm-eabi/",\ "packageDependencies": [\ - ["@pkgr/core", "npm:0.2.9"]\ + ["@rollup/rollup-android-arm-eabi", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-android-arm64", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-android-arm64-npm-4.59.0-cfb9484bfa/node_modules/@rollup/rollup-android-arm64/",\ + "packageDependencies": [\ + ["@rollup/rollup-android-arm64", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-darwin-arm64", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-darwin-arm64-npm-4.59.0-db3495ba42/node_modules/@rollup/rollup-darwin-arm64/",\ + "packageDependencies": [\ + ["@rollup/rollup-darwin-arm64", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-darwin-x64", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-darwin-x64-npm-4.59.0-cfe999cbb8/node_modules/@rollup/rollup-darwin-x64/",\ + "packageDependencies": [\ + ["@rollup/rollup-darwin-x64", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-freebsd-arm64", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-freebsd-arm64-npm-4.59.0-d0d5ecfb7c/node_modules/@rollup/rollup-freebsd-arm64/",\ + "packageDependencies": [\ + ["@rollup/rollup-freebsd-arm64", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-freebsd-x64", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-freebsd-x64-npm-4.59.0-e833d306d6/node_modules/@rollup/rollup-freebsd-x64/",\ + "packageDependencies": [\ + ["@rollup/rollup-freebsd-x64", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-arm-gnueabihf", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-arm-gnueabihf-npm-4.59.0-d9427160f7/node_modules/@rollup/rollup-linux-arm-gnueabihf/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-arm-gnueabihf", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-arm-musleabihf", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-arm-musleabihf-npm-4.59.0-a56abe1623/node_modules/@rollup/rollup-linux-arm-musleabihf/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-arm-musleabihf", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-arm64-gnu", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-arm64-gnu-npm-4.59.0-8929991df7/node_modules/@rollup/rollup-linux-arm64-gnu/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-arm64-gnu", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-arm64-musl", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-arm64-musl-npm-4.59.0-fcbe29740d/node_modules/@rollup/rollup-linux-arm64-musl/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-arm64-musl", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-loong64-gnu", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-loong64-gnu-npm-4.59.0-53a2cbeb18/node_modules/@rollup/rollup-linux-loong64-gnu/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-loong64-gnu", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-loong64-musl", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-loong64-musl-npm-4.59.0-c53ce95532/node_modules/@rollup/rollup-linux-loong64-musl/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-loong64-musl", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-ppc64-gnu", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-ppc64-gnu-npm-4.59.0-2fa9a28c9f/node_modules/@rollup/rollup-linux-ppc64-gnu/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-ppc64-gnu", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-ppc64-musl", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-ppc64-musl-npm-4.59.0-7c69c76347/node_modules/@rollup/rollup-linux-ppc64-musl/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-ppc64-musl", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-riscv64-gnu", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-riscv64-gnu-npm-4.59.0-4efd598c98/node_modules/@rollup/rollup-linux-riscv64-gnu/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-riscv64-gnu", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-riscv64-musl", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-riscv64-musl-npm-4.59.0-e49f6c8605/node_modules/@rollup/rollup-linux-riscv64-musl/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-riscv64-musl", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-s390x-gnu", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-s390x-gnu-npm-4.59.0-6a6b94ad65/node_modules/@rollup/rollup-linux-s390x-gnu/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-s390x-gnu", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-x64-gnu", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-x64-gnu-npm-4.59.0-da6c703f69/node_modules/@rollup/rollup-linux-x64-gnu/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-x64-gnu", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-linux-x64-musl", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-linux-x64-musl-npm-4.59.0-50f79fbe61/node_modules/@rollup/rollup-linux-x64-musl/",\ + "packageDependencies": [\ + ["@rollup/rollup-linux-x64-musl", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-openbsd-x64", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-openbsd-x64-npm-4.59.0-f004324c8a/node_modules/@rollup/rollup-openbsd-x64/",\ + "packageDependencies": [\ + ["@rollup/rollup-openbsd-x64", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-openharmony-arm64", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-openharmony-arm64-npm-4.59.0-d418bec0d2/node_modules/@rollup/rollup-openharmony-arm64/",\ + "packageDependencies": [\ + ["@rollup/rollup-openharmony-arm64", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-win32-arm64-msvc", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-win32-arm64-msvc-npm-4.59.0-d1e11bba9f/node_modules/@rollup/rollup-win32-arm64-msvc/",\ + "packageDependencies": [\ + ["@rollup/rollup-win32-arm64-msvc", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-win32-ia32-msvc", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-win32-ia32-msvc-npm-4.59.0-d4b7207f70/node_modules/@rollup/rollup-win32-ia32-msvc/",\ + "packageDependencies": [\ + ["@rollup/rollup-win32-ia32-msvc", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-win32-x64-gnu", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-win32-x64-gnu-npm-4.59.0-462c90c9b5/node_modules/@rollup/rollup-win32-x64-gnu/",\ + "packageDependencies": [\ + ["@rollup/rollup-win32-x64-gnu", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@rollup/rollup-win32-x64-msvc", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/unplugged/@rollup-rollup-win32-x64-msvc-npm-4.59.0-1850b314ab/node_modules/@rollup/rollup-win32-x64-msvc/",\ + "packageDependencies": [\ + ["@rollup/rollup-win32-x64-msvc", "npm:4.59.0"]\ ],\ "linkType": "HARD"\ }]\ @@ -4622,57 +6153,82 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@sentry-internal/browser-utils", [\ - ["npm:9.14.0", {\ - "packageLocation": "./.yarn/cache/@sentry-internal-browser-utils-npm-9.14.0-cb7c00e1d8-d3c9e336c2.zip/node_modules/@sentry-internal/browser-utils/",\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-internal-browser-utils-npm-10.43.0-5faac97225-23de31117b.zip/node_modules/@sentry-internal/browser-utils/",\ "packageDependencies": [\ - ["@sentry-internal/browser-utils", "npm:9.14.0"],\ - ["@sentry/core", "npm:9.14.0"]\ + ["@sentry-internal/browser-utils", "npm:10.43.0"],\ + ["@sentry/core", "npm:10.43.0"]\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@sentry-internal/feedback", [\ - ["npm:9.14.0", {\ - "packageLocation": "./.yarn/cache/@sentry-internal-feedback-npm-9.14.0-2cf0ca7c6d-aed56b19d7.zip/node_modules/@sentry-internal/feedback/",\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-internal-feedback-npm-10.43.0-03476b28f7-394fd58d0a.zip/node_modules/@sentry-internal/feedback/",\ "packageDependencies": [\ - ["@sentry-internal/feedback", "npm:9.14.0"],\ - ["@sentry/core", "npm:9.14.0"]\ + ["@sentry-internal/feedback", "npm:10.43.0"],\ + ["@sentry/core", "npm:10.43.0"]\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@sentry-internal/replay", [\ - ["npm:9.14.0", {\ - "packageLocation": "./.yarn/cache/@sentry-internal-replay-npm-9.14.0-d6681f4f5f-86bd17b32e.zip/node_modules/@sentry-internal/replay/",\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-internal-replay-npm-10.43.0-82533134d0-dd56b9c358.zip/node_modules/@sentry-internal/replay/",\ "packageDependencies": [\ - ["@sentry-internal/browser-utils", "npm:9.14.0"],\ - ["@sentry-internal/replay", "npm:9.14.0"],\ - ["@sentry/core", "npm:9.14.0"]\ + ["@sentry-internal/browser-utils", "npm:10.43.0"],\ + ["@sentry-internal/replay", "npm:10.43.0"],\ + ["@sentry/core", "npm:10.43.0"]\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@sentry-internal/replay-canvas", [\ - ["npm:9.14.0", {\ - "packageLocation": "./.yarn/cache/@sentry-internal-replay-canvas-npm-9.14.0-2a39a7c942-48ebb8057f.zip/node_modules/@sentry-internal/replay-canvas/",\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-internal-replay-canvas-npm-10.43.0-2562a3d960-f0d3df594a.zip/node_modules/@sentry-internal/replay-canvas/",\ + "packageDependencies": [\ + ["@sentry-internal/replay", "npm:10.43.0"],\ + ["@sentry-internal/replay-canvas", "npm:10.43.0"],\ + ["@sentry/core", "npm:10.43.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/babel-plugin-component-annotate", [\ + ["npm:5.1.1", {\ + "packageLocation": "./.yarn/cache/@sentry-babel-plugin-component-annotate-npm-5.1.1-c6783d0521-8c7da826dc.zip/node_modules/@sentry/babel-plugin-component-annotate/",\ "packageDependencies": [\ - ["@sentry-internal/replay", "npm:9.14.0"],\ - ["@sentry-internal/replay-canvas", "npm:9.14.0"],\ - ["@sentry/core", "npm:9.14.0"]\ + ["@sentry/babel-plugin-component-annotate", "npm:5.1.1"]\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@sentry/browser", [\ - ["npm:9.14.0", {\ - "packageLocation": "./.yarn/cache/@sentry-browser-npm-9.14.0-0e2464dc6d-16a4ba1911.zip/node_modules/@sentry/browser/",\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-browser-npm-10.43.0-f9aafbfc79-9b20398756.zip/node_modules/@sentry/browser/",\ + "packageDependencies": [\ + ["@sentry-internal/browser-utils", "npm:10.43.0"],\ + ["@sentry-internal/feedback", "npm:10.43.0"],\ + ["@sentry-internal/replay", "npm:10.43.0"],\ + ["@sentry-internal/replay-canvas", "npm:10.43.0"],\ + ["@sentry/browser", "npm:10.43.0"],\ + ["@sentry/core", "npm:10.43.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/bundler-plugin-core", [\ + ["npm:5.1.1", {\ + "packageLocation": "./.yarn/cache/@sentry-bundler-plugin-core-npm-5.1.1-6cd64e5cf9-6d08a02ed4.zip/node_modules/@sentry/bundler-plugin-core/",\ "packageDependencies": [\ - ["@sentry-internal/browser-utils", "npm:9.14.0"],\ - ["@sentry-internal/feedback", "npm:9.14.0"],\ - ["@sentry-internal/replay", "npm:9.14.0"],\ - ["@sentry-internal/replay-canvas", "npm:9.14.0"],\ - ["@sentry/browser", "npm:9.14.0"],\ - ["@sentry/core", "npm:9.14.0"]\ + ["@babel/core", "npm:7.29.0"],\ + ["@sentry/babel-plugin-component-annotate", "npm:5.1.1"],\ + ["@sentry/bundler-plugin-core", "npm:5.1.1"],\ + ["@sentry/cli", "npm:2.58.5"],\ + ["dotenv", "npm:16.6.1"],\ + ["find-up", "npm:5.0.0"],\ + ["glob", "npm:13.0.6"],\ + ["magic-string", "npm:0.30.21"]\ ],\ "linkType": "HARD"\ }]\ @@ -4697,6 +6253,26 @@ const RAW_RUNTIME_STATE = ["which", "npm:2.0.2"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-npm-2.58.5-8604062248/node_modules/@sentry/cli/",\ + "packageDependencies": [\ + ["@sentry/cli", "npm:2.58.5"],\ + ["@sentry/cli-darwin", "npm:2.58.5"],\ + ["@sentry/cli-linux-arm", "npm:2.58.5"],\ + ["@sentry/cli-linux-arm64", "npm:2.58.5"],\ + ["@sentry/cli-linux-i686", "npm:2.58.5"],\ + ["@sentry/cli-linux-x64", "npm:2.58.5"],\ + ["@sentry/cli-win32-arm64", "npm:2.58.5"],\ + ["@sentry/cli-win32-i686", "npm:2.58.5"],\ + ["@sentry/cli-win32-x64", "npm:2.58.5"],\ + ["https-proxy-agent", "npm:5.0.1"],\ + ["node-fetch", "virtual:d050b4d3a803734bd8c19161b5efbc77bba8046436471258d990195132973149743cdca18aab21399ef652be598dd13c3e96fd37411a57c83597a665f4ff4a28#npm:2.7.0"],\ + ["progress", "npm:2.0.3"],\ + ["proxy-from-env", "npm:1.1.0"],\ + ["which", "npm:2.0.2"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/cli-darwin", [\ @@ -4706,6 +6282,13 @@ const RAW_RUNTIME_STATE = ["@sentry/cli-darwin", "npm:2.45.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-darwin-npm-2.58.5-1f667e3b9d/node_modules/@sentry/cli-darwin/",\ + "packageDependencies": [\ + ["@sentry/cli-darwin", "npm:2.58.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/cli-linux-arm", [\ @@ -4715,6 +6298,13 @@ const RAW_RUNTIME_STATE = ["@sentry/cli-linux-arm", "npm:2.45.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-linux-arm-npm-2.58.5-bdbc5b90d9/node_modules/@sentry/cli-linux-arm/",\ + "packageDependencies": [\ + ["@sentry/cli-linux-arm", "npm:2.58.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/cli-linux-arm64", [\ @@ -4724,6 +6314,13 @@ const RAW_RUNTIME_STATE = ["@sentry/cli-linux-arm64", "npm:2.45.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-linux-arm64-npm-2.58.5-acdabcddcc/node_modules/@sentry/cli-linux-arm64/",\ + "packageDependencies": [\ + ["@sentry/cli-linux-arm64", "npm:2.58.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/cli-linux-i686", [\ @@ -4733,6 +6330,13 @@ const RAW_RUNTIME_STATE = ["@sentry/cli-linux-i686", "npm:2.45.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-linux-i686-npm-2.58.5-cf4bc8f64a/node_modules/@sentry/cli-linux-i686/",\ + "packageDependencies": [\ + ["@sentry/cli-linux-i686", "npm:2.58.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/cli-linux-x64", [\ @@ -4742,6 +6346,13 @@ const RAW_RUNTIME_STATE = ["@sentry/cli-linux-x64", "npm:2.45.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-linux-x64-npm-2.58.5-7cea7778bc/node_modules/@sentry/cli-linux-x64/",\ + "packageDependencies": [\ + ["@sentry/cli-linux-x64", "npm:2.58.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/cli-win32-arm64", [\ @@ -4751,6 +6362,13 @@ const RAW_RUNTIME_STATE = ["@sentry/cli-win32-arm64", "npm:2.45.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-win32-arm64-npm-2.58.5-60a9527f28/node_modules/@sentry/cli-win32-arm64/",\ + "packageDependencies": [\ + ["@sentry/cli-win32-arm64", "npm:2.58.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/cli-win32-i686", [\ @@ -4760,6 +6378,13 @@ const RAW_RUNTIME_STATE = ["@sentry/cli-win32-i686", "npm:2.45.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-win32-i686-npm-2.58.5-d67ad5b5c5/node_modules/@sentry/cli-win32-i686/",\ + "packageDependencies": [\ + ["@sentry/cli-win32-i686", "npm:2.58.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/cli-win32-x64", [\ @@ -4769,13 +6394,279 @@ const RAW_RUNTIME_STATE = ["@sentry/cli-win32-x64", "npm:2.45.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.58.5", {\ + "packageLocation": "./.yarn/unplugged/@sentry-cli-win32-x64-npm-2.58.5-d05f8d090e/node_modules/@sentry/cli-win32-x64/",\ + "packageDependencies": [\ + ["@sentry/cli-win32-x64", "npm:2.58.5"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@sentry/core", [\ - ["npm:9.14.0", {\ - "packageLocation": "./.yarn/cache/@sentry-core-npm-9.14.0-6df7ce92b3-ade3f5248a.zip/node_modules/@sentry/core/",\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-core-npm-10.43.0-0e35cf6983-89ff388771.zip/node_modules/@sentry/core/",\ + "packageDependencies": [\ + ["@sentry/core", "npm:10.43.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/nextjs", [\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-nextjs-npm-10.43.0-522ec5f88f-a22938081b.zip/node_modules/@sentry/nextjs/",\ + "packageDependencies": [\ + ["@sentry/nextjs", "npm:10.43.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:10.43.0", {\ + "packageLocation": "./.yarn/__virtual__/@sentry-nextjs-virtual-b61873f954/0/cache/@sentry-nextjs-npm-10.43.0-522ec5f88f-a22938081b.zip/node_modules/@sentry/nextjs/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/sdk-trace-base", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@rollup/plugin-commonjs", "virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:28.0.1"],\ + ["@sentry-internal/browser-utils", "npm:10.43.0"],\ + ["@sentry/bundler-plugin-core", "npm:5.1.1"],\ + ["@sentry/core", "npm:10.43.0"],\ + ["@sentry/nextjs", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:10.43.0"],\ + ["@sentry/node", "npm:10.43.0"],\ + ["@sentry/opentelemetry", "virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:10.43.0"],\ + ["@sentry/react", "virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:10.43.0"],\ + ["@sentry/vercel-edge", "npm:10.43.0"],\ + ["@sentry/webpack-plugin", "virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:5.1.1"],\ + ["@types/next", null],\ + ["next", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:15.5.10"],\ + ["rollup", "npm:4.59.0"],\ + ["stacktrace-parser", "npm:0.1.11"]\ + ],\ + "packagePeers": [\ + "@types/next",\ + "next"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/node", [\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-node-npm-10.43.0-4c7fcaed43-a0976484c2.zip/node_modules/@sentry/node/",\ + "packageDependencies": [\ + ["@fastify/otel", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.16.0"],\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/context-async-hooks", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-amqplib", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.58.0"],\ + ["@opentelemetry/instrumentation-connect", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.54.0"],\ + ["@opentelemetry/instrumentation-dataloader", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.28.0"],\ + ["@opentelemetry/instrumentation-express", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0"],\ + ["@opentelemetry/instrumentation-fs", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.30.0"],\ + ["@opentelemetry/instrumentation-generic-pool", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.54.0"],\ + ["@opentelemetry/instrumentation-graphql", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.58.0"],\ + ["@opentelemetry/instrumentation-hapi", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0"],\ + ["@opentelemetry/instrumentation-http", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/instrumentation-ioredis", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0"],\ + ["@opentelemetry/instrumentation-kafkajs", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.20.0"],\ + ["@opentelemetry/instrumentation-knex", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.55.0"],\ + ["@opentelemetry/instrumentation-koa", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0"],\ + ["@opentelemetry/instrumentation-lru-memoizer", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.55.0"],\ + ["@opentelemetry/instrumentation-mongodb", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.64.0"],\ + ["@opentelemetry/instrumentation-mongoose", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0"],\ + ["@opentelemetry/instrumentation-mysql", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0"],\ + ["@opentelemetry/instrumentation-mysql2", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.57.0"],\ + ["@opentelemetry/instrumentation-pg", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.63.0"],\ + ["@opentelemetry/instrumentation-redis", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.59.0"],\ + ["@opentelemetry/instrumentation-tedious", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.30.0"],\ + ["@opentelemetry/instrumentation-undici", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.21.0"],\ + ["@opentelemetry/resources", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/sdk-trace-base", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@prisma/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:7.2.0"],\ + ["@sentry/core", "npm:10.43.0"],\ + ["@sentry/node", "npm:10.43.0"],\ + ["@sentry/node-core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:10.43.0"],\ + ["@sentry/opentelemetry", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:10.43.0"],\ + ["import-in-the-middle", "npm:2.0.6"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/node-core", [\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-node-core-npm-10.43.0-e73484e3ce-a18c31a359.zip/node_modules/@sentry/node-core/",\ + "packageDependencies": [\ + ["@sentry/node-core", "npm:10.43.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:10.43.0", {\ + "packageLocation": "./.yarn/__virtual__/@sentry-node-core-virtual-6af87bf57e/0/cache/@sentry-node-core-npm-10.43.0-e73484e3ce-a18c31a359.zip/node_modules/@sentry/node-core/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/context-async-hooks", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/instrumentation", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:0.211.0"],\ + ["@opentelemetry/resources", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/sdk-trace-base", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@sentry/core", "npm:10.43.0"],\ + ["@sentry/node-core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:10.43.0"],\ + ["@sentry/opentelemetry", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:10.43.0"],\ + ["@types/opentelemetry__api", null],\ + ["@types/opentelemetry__context-async-hooks", null],\ + ["@types/opentelemetry__core", null],\ + ["@types/opentelemetry__instrumentation", null],\ + ["@types/opentelemetry__resources", null],\ + ["@types/opentelemetry__sdk-trace-base", null],\ + ["@types/opentelemetry__semantic-conventions", null],\ + ["import-in-the-middle", "npm:2.0.6"]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@opentelemetry/context-async-hooks",\ + "@opentelemetry/core",\ + "@opentelemetry/instrumentation",\ + "@opentelemetry/resources",\ + "@opentelemetry/sdk-trace-base",\ + "@opentelemetry/semantic-conventions",\ + "@types/opentelemetry__api",\ + "@types/opentelemetry__context-async-hooks",\ + "@types/opentelemetry__core",\ + "@types/opentelemetry__instrumentation",\ + "@types/opentelemetry__resources",\ + "@types/opentelemetry__sdk-trace-base",\ + "@types/opentelemetry__semantic-conventions"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/opentelemetry", [\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-opentelemetry-npm-10.43.0-5c9849783e-e108fbd4fe.zip/node_modules/@sentry/opentelemetry/",\ + "packageDependencies": [\ + ["@sentry/opentelemetry", "npm:10.43.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:10.43.0", {\ + "packageLocation": "./.yarn/__virtual__/@sentry-opentelemetry-virtual-43372c6cb9/0/cache/@sentry-opentelemetry-npm-10.43.0-5c9849783e-e108fbd4fe.zip/node_modules/@sentry/opentelemetry/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/context-async-hooks", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/sdk-trace-base", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@sentry/core", "npm:10.43.0"],\ + ["@sentry/opentelemetry", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:10.43.0"],\ + ["@types/opentelemetry__api", null],\ + ["@types/opentelemetry__context-async-hooks", null],\ + ["@types/opentelemetry__core", null],\ + ["@types/opentelemetry__sdk-trace-base", null],\ + ["@types/opentelemetry__semantic-conventions", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@opentelemetry/context-async-hooks",\ + "@opentelemetry/core",\ + "@opentelemetry/sdk-trace-base",\ + "@opentelemetry/semantic-conventions",\ + "@types/opentelemetry__api",\ + "@types/opentelemetry__context-async-hooks",\ + "@types/opentelemetry__core",\ + "@types/opentelemetry__sdk-trace-base",\ + "@types/opentelemetry__semantic-conventions"\ + ],\ + "linkType": "HARD"\ + }],\ + ["virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:10.43.0", {\ + "packageLocation": "./.yarn/__virtual__/@sentry-opentelemetry-virtual-2bb6e4ccb1/0/cache/@sentry-opentelemetry-npm-10.43.0-5c9849783e-e108fbd4fe.zip/node_modules/@sentry/opentelemetry/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/context-async-hooks", null],\ + ["@opentelemetry/core", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/sdk-trace-base", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@opentelemetry/semantic-conventions", "npm:1.40.0"],\ + ["@sentry/core", "npm:10.43.0"],\ + ["@sentry/opentelemetry", "virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:10.43.0"],\ + ["@types/opentelemetry__api", null],\ + ["@types/opentelemetry__context-async-hooks", null],\ + ["@types/opentelemetry__core", null],\ + ["@types/opentelemetry__sdk-trace-base", null],\ + ["@types/opentelemetry__semantic-conventions", null]\ + ],\ + "packagePeers": [\ + "@opentelemetry/api",\ + "@opentelemetry/context-async-hooks",\ + "@opentelemetry/core",\ + "@opentelemetry/sdk-trace-base",\ + "@opentelemetry/semantic-conventions",\ + "@types/opentelemetry__api",\ + "@types/opentelemetry__context-async-hooks",\ + "@types/opentelemetry__core",\ + "@types/opentelemetry__sdk-trace-base",\ + "@types/opentelemetry__semantic-conventions"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/react", [\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-react-npm-10.43.0-e7ea4becc5-098019ae7a.zip/node_modules/@sentry/react/",\ + "packageDependencies": [\ + ["@sentry/react", "npm:10.43.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:10.43.0", {\ + "packageLocation": "./.yarn/__virtual__/@sentry-react-virtual-d2a7fd5839/0/cache/@sentry-react-npm-10.43.0-e7ea4becc5-098019ae7a.zip/node_modules/@sentry/react/",\ + "packageDependencies": [\ + ["@sentry/browser", "npm:10.43.0"],\ + ["@sentry/core", "npm:10.43.0"],\ + ["@sentry/react", "virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:10.43.0"],\ + ["@types/react", null],\ + ["react", null]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/vercel-edge", [\ + ["npm:10.43.0", {\ + "packageLocation": "./.yarn/cache/@sentry-vercel-edge-npm-10.43.0-2730475241-b89ef317a3.zip/node_modules/@sentry/vercel-edge/",\ + "packageDependencies": [\ + ["@opentelemetry/api", "npm:1.9.0"],\ + ["@opentelemetry/resources", "virtual:4c7fcaed4308fae27f8033d6c20aa1c80d5ff2f17ef109545f98d5d67b17a95ef85ddea942192b220a3c6ca21803e1269d862a5521bdee0be8fe08e330e8af71#npm:2.6.0"],\ + ["@sentry/core", "npm:10.43.0"],\ + ["@sentry/vercel-edge", "npm:10.43.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@sentry/webpack-plugin", [\ + ["npm:5.1.1", {\ + "packageLocation": "./.yarn/cache/@sentry-webpack-plugin-npm-5.1.1-8faceba0cb-f1b3ccad6a.zip/node_modules/@sentry/webpack-plugin/",\ + "packageDependencies": [\ + ["@sentry/webpack-plugin", "npm:5.1.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:5.1.1", {\ + "packageLocation": "./.yarn/__virtual__/@sentry-webpack-plugin-virtual-79cb408ea6/0/cache/@sentry-webpack-plugin-npm-5.1.1-8faceba0cb-f1b3ccad6a.zip/node_modules/@sentry/webpack-plugin/",\ "packageDependencies": [\ - ["@sentry/core", "npm:9.14.0"]\ + ["@sentry/bundler-plugin-core", "npm:5.1.1"],\ + ["@sentry/webpack-plugin", "virtual:b61873f9545530461749d78f9bde76fc9a6b6dc92c78702c77f1a1597fec363ac41367267acf9ec6c5179e1f1550f9a2d091fe865cb4d3c0e9f1536897e31401#npm:5.1.1"],\ + ["@types/webpack", null],\ + ["uuid", "npm:9.0.1"],\ + ["webpack", null]\ + ],\ + "packagePeers": [\ + "@types/webpack",\ + "webpack"\ ],\ "linkType": "HARD"\ }]\ @@ -4809,15 +6700,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@stomp/stompjs", [\ - ["npm:7.0.0", {\ - "packageLocation": "./.yarn/cache/@stomp-stompjs-npm-7.0.0-c66e011389-f60db460e0.zip/node_modules/@stomp/stompjs/",\ - "packageDependencies": [\ - ["@stomp/stompjs", "npm:7.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@svgr/babel-plugin-add-jsx-attribute", [\ ["npm:8.0.0", {\ "packageLocation": "./.yarn/cache/@svgr-babel-plugin-add-jsx-attribute-npm-8.0.0-026be9c2be-3fc8e35d16.zip/node_modules/@svgr/babel-plugin-add-jsx-attribute/",\ @@ -5128,27 +7010,27 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@tanstack/query-core", [\ - ["npm:5.28.6", {\ - "packageLocation": "./.yarn/cache/@tanstack-query-core-npm-5.28.6-6bd1f84a2d-e9ae8d80a8.zip/node_modules/@tanstack/query-core/",\ + ["npm:5.90.20", {\ + "packageLocation": "./.yarn/cache/@tanstack-query-core-npm-5.90.20-fe193b58bc-25e38f4382.zip/node_modules/@tanstack/query-core/",\ "packageDependencies": [\ - ["@tanstack/query-core", "npm:5.28.6"]\ + ["@tanstack/query-core", "npm:5.90.20"]\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@tanstack/react-query", [\ - ["npm:5.28.6", {\ - "packageLocation": "./.yarn/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip/node_modules/@tanstack/react-query/",\ + ["npm:5.90.21", {\ + "packageLocation": "./.yarn/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip/node_modules/@tanstack/react-query/",\ "packageDependencies": [\ - ["@tanstack/react-query", "npm:5.28.6"]\ + ["@tanstack/react-query", "npm:5.90.21"]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.28.6", {\ - "packageLocation": "./.yarn/__virtual__/@tanstack-react-query-virtual-147ceec9c5/0/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip/node_modules/@tanstack/react-query/",\ + ["virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.90.21", {\ + "packageLocation": "./.yarn/__virtual__/@tanstack-react-query-virtual-2c374d90ab/0/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip/node_modules/@tanstack/react-query/",\ "packageDependencies": [\ - ["@tanstack/query-core", "npm:5.28.6"],\ - ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.28.6"],\ + ["@tanstack/query-core", "npm:5.90.20"],\ + ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.90.21"],\ ["@types/react", "npm:19.2.10"],\ ["react", "npm:19.2.4"]\ ],\ @@ -5325,6 +7207,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/connect", [\ + ["npm:3.4.38", {\ + "packageLocation": "./.yarn/cache/@types-connect-npm-3.4.38-a8a4c38337-7eb1bc5342.zip/node_modules/@types/connect/",\ + "packageDependencies": [\ + ["@types/connect", "npm:3.4.38"],\ + ["@types/node", "npm:20.11.17"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/estree", [\ ["npm:1.0.8", {\ "packageLocation": "./.yarn/cache/@types-estree-npm-1.0.8-2195bac6d6-25a4c16a67.zip/node_modules/@types/estree/",\ @@ -5456,6 +7348,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/mysql", [\ + ["npm:2.15.27", {\ + "packageLocation": "./.yarn/cache/@types-mysql-npm-2.15.27-76b107f5b9-a8c7435010.zip/node_modules/@types/mysql/",\ + "packageDependencies": [\ + ["@types/mysql", "npm:2.15.27"],\ + ["@types/node", "npm:20.11.17"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/navermaps", [\ ["npm:3.7.4", {\ "packageLocation": "./.yarn/cache/@types-navermaps-npm-3.7.4-67ecab9c7a-c8cbab2b8c.zip/node_modules/@types/navermaps/",\ @@ -5501,6 +7403,38 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/pg", [\ + ["npm:8.15.6", {\ + "packageLocation": "./.yarn/cache/@types-pg-npm-8.15.6-44108e12b9-4bc1bb274e.zip/node_modules/@types/pg/",\ + "packageDependencies": [\ + ["@types/node", "npm:20.11.17"],\ + ["@types/pg", "npm:8.15.6"],\ + ["pg-protocol", "npm:1.13.0"],\ + ["pg-types", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ + }],\ + ["npm:8.18.0", {\ + "packageLocation": "./.yarn/cache/@types-pg-npm-8.18.0-d62c4e6195-fdfcaff97f.zip/node_modules/@types/pg/",\ + "packageDependencies": [\ + ["@types/node", "npm:20.11.17"],\ + ["@types/pg", "npm:8.18.0"],\ + ["pg-protocol", "npm:1.13.0"],\ + ["pg-types", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@types/pg-pool", [\ + ["npm:2.0.7", {\ + "packageLocation": "./.yarn/cache/@types-pg-pool-npm-2.0.7-908dda8b54-b2ac51f1e9.zip/node_modules/@types/pg-pool/",\ + "packageDependencies": [\ + ["@types/pg", "npm:8.18.0"],\ + ["@types/pg-pool", "npm:2.0.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/prop-types", [\ ["npm:15.7.11", {\ "packageLocation": "./.yarn/cache/@types-prop-types-npm-15.7.11-a0a5a0025c-7519ff11d0.zip/node_modules/@types/prop-types/",\ @@ -5596,6 +7530,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/tedious", [\ + ["npm:4.0.14", {\ + "packageLocation": "./.yarn/cache/@types-tedious-npm-4.0.14-11edc4a73d-c8f6480cf6.zip/node_modules/@types/tedious/",\ + "packageDependencies": [\ + ["@types/node", "npm:20.11.17"],\ + ["@types/tedious", "npm:4.0.14"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/testing-library__jest-dom", [\ ["npm:5.14.9", {\ "packageLocation": "./.yarn/cache/@types-testing-library__jest-dom-npm-5.14.9-319d22d764-e257de95a4.zip/node_modules/@types/testing-library__jest-dom/",\ @@ -6056,6 +8000,28 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["acorn-import-attributes", [\ + ["npm:1.9.5", {\ + "packageLocation": "./.yarn/cache/acorn-import-attributes-npm-1.9.5-d1e666eb35-8bfbfbb6e2.zip/node_modules/acorn-import-attributes/",\ + "packageDependencies": [\ + ["acorn-import-attributes", "npm:1.9.5"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:233e5f3db3715a0fa5ba20c83dbd25b39ee13185aca65de8109a05fe6cbee602a48e10dd53b72677a835252db9745d55402a5c7d80e6958c64331b2b770e3388#npm:1.9.5", {\ + "packageLocation": "./.yarn/__virtual__/acorn-import-attributes-virtual-74d656a3b3/0/cache/acorn-import-attributes-npm-1.9.5-d1e666eb35-8bfbfbb6e2.zip/node_modules/acorn-import-attributes/",\ + "packageDependencies": [\ + ["@types/acorn", null],\ + ["acorn", "npm:8.15.0"],\ + ["acorn-import-attributes", "virtual:233e5f3db3715a0fa5ba20c83dbd25b39ee13185aca65de8109a05fe6cbee602a48e10dd53b72677a835252db9745d55402a5c7d80e6958c64331b2b770e3388#npm:1.9.5"]\ + ],\ + "packagePeers": [\ + "@types/acorn",\ + "acorn"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["acorn-jsx", [\ ["npm:5.3.2", {\ "packageLocation": "./.yarn/cache/acorn-jsx-npm-5.3.2-d7594599ea-d4371eaef7.zip/node_modules/acorn-jsx/",\ @@ -6753,6 +8719,13 @@ const RAW_RUNTIME_STATE = ["balanced-match", "npm:2.0.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:4.0.4", {\ + "packageLocation": "./.yarn/cache/balanced-match-npm-4.0.4-fd666b3c7f-fb07bb66a0.zip/node_modules/balanced-match/",\ + "packageDependencies": [\ + ["balanced-match", "npm:4.0.4"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["base64-arraybuffer", [\ @@ -6799,6 +8772,14 @@ const RAW_RUNTIME_STATE = ["brace-expansion", "npm:2.0.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:5.0.4", {\ + "packageLocation": "./.yarn/cache/brace-expansion-npm-5.0.4-acb9332524-cfd57e20d8.zip/node_modules/brace-expansion/",\ + "packageDependencies": [\ + ["balanced-match", "npm:4.0.4"],\ + ["brace-expansion", "npm:5.0.4"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["braces", [\ @@ -7062,6 +9043,13 @@ const RAW_RUNTIME_STATE = ["cjs-module-lexer", "npm:1.4.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.2.0", {\ + "packageLocation": "./.yarn/cache/cjs-module-lexer-npm-2.2.0-a4ea3b2e41-fc8eb5c191.zip/node_modules/cjs-module-lexer/",\ + "packageDependencies": [\ + ["cjs-module-lexer", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["clean-stack", [\ @@ -7214,6 +9202,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["commondir", [\ + ["npm:1.0.1", {\ + "packageLocation": "./.yarn/cache/commondir-npm-1.0.1-291b790340-4620bc4936.zip/node_modules/commondir/",\ + "packageDependencies": [\ + ["commondir", "npm:1.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["concat-map", [\ ["npm:0.0.1", {\ "packageLocation": "./.yarn/cache/concat-map-npm-0.0.1-85a921b7ee-9680699c8e.zip/node_modules/concat-map/",\ @@ -7524,6 +9521,27 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ + ["npm:4.4.3", {\ + "packageLocation": "./.yarn/cache/debug-npm-4.4.3-0105c6123a-9ada3434ea.zip/node_modules/debug/",\ + "packageDependencies": [\ + ["debug", "npm:4.4.3"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:096af9b870a5e180ddb16cfa36c1dc97bc1ba878bc559221fe9184d27f738627139de407e4cc683ebeeaa875d8a403a7f02b4597de5cf2cca18231f15c7f74c8#npm:4.4.3", {\ + "packageLocation": "./.yarn/__virtual__/debug-virtual-8c370a824c/0/cache/debug-npm-4.4.3-0105c6123a-9ada3434ea.zip/node_modules/debug/",\ + "packageDependencies": [\ + ["@types/supports-color", null],\ + ["debug", "virtual:096af9b870a5e180ddb16cfa36c1dc97bc1ba878bc559221fe9184d27f738627139de407e4cc683ebeeaa875d8a403a7f02b4597de5cf2cca18231f15c7f74c8#npm:4.4.3"],\ + ["ms", "npm:2.1.3"],\ + ["supports-color", null]\ + ],\ + "packagePeers": [\ + "@types/supports-color",\ + "supports-color"\ + ],\ + "linkType": "HARD"\ + }],\ ["virtual:2a426afc4b2eef43db12a540d29c2b5476640459bfcd5c24f86bb401cf8cce97e63bd81794d206a5643057e7f662643afd5ce3dfc4d4bfd8e706006c6309c5fa#npm:3.2.7", {\ "packageLocation": "./.yarn/__virtual__/debug-virtual-d2345003b7/0/cache/debug-npm-3.2.7-754e818c7a-d86fd7be2b.zip/node_modules/debug/",\ "packageDependencies": [\ @@ -7847,6 +9865,22 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["dotenv", [\ + ["npm:16.6.1", {\ + "packageLocation": "./.yarn/cache/dotenv-npm-16.6.1-01334288ea-1d18971443.zip/node_modules/dotenv/",\ + "packageDependencies": [\ + ["dotenv", "npm:16.6.1"]\ + ],\ + "linkType": "HARD"\ + }],\ + ["npm:17.2.3", {\ + "packageLocation": "./.yarn/cache/dotenv-npm-17.2.3-2f9ab93ea1-f8b78626eb.zip/node_modules/dotenv/",\ + "packageDependencies": [\ + ["dotenv", "npm:17.2.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["dunder-proto", [\ ["npm:1.0.1", {\ "packageLocation": "./.yarn/cache/dunder-proto-npm-1.0.1-90eb6829db-5add88a3d6.zip/node_modules/dunder-proto/",\ @@ -8276,6 +10310,41 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["esbuild", [\ + ["npm:0.27.2", {\ + "packageLocation": "./.yarn/unplugged/esbuild-npm-0.27.2-7789e62c6d/node_modules/esbuild/",\ + "packageDependencies": [\ + ["@esbuild/aix-ppc64", "npm:0.27.2"],\ + ["@esbuild/android-arm", "npm:0.27.2"],\ + ["@esbuild/android-arm64", "npm:0.27.2"],\ + ["@esbuild/android-x64", "npm:0.27.2"],\ + ["@esbuild/darwin-arm64", "npm:0.27.2"],\ + ["@esbuild/darwin-x64", "npm:0.27.2"],\ + ["@esbuild/freebsd-arm64", "npm:0.27.2"],\ + ["@esbuild/freebsd-x64", "npm:0.27.2"],\ + ["@esbuild/linux-arm", "npm:0.27.2"],\ + ["@esbuild/linux-arm64", "npm:0.27.2"],\ + ["@esbuild/linux-ia32", "npm:0.27.2"],\ + ["@esbuild/linux-loong64", "npm:0.27.2"],\ + ["@esbuild/linux-mips64el", "npm:0.27.2"],\ + ["@esbuild/linux-ppc64", "npm:0.27.2"],\ + ["@esbuild/linux-riscv64", "npm:0.27.2"],\ + ["@esbuild/linux-s390x", "npm:0.27.2"],\ + ["@esbuild/linux-x64", "npm:0.27.2"],\ + ["@esbuild/netbsd-arm64", "npm:0.27.2"],\ + ["@esbuild/netbsd-x64", "npm:0.27.2"],\ + ["@esbuild/openbsd-arm64", "npm:0.27.2"],\ + ["@esbuild/openbsd-x64", "npm:0.27.2"],\ + ["@esbuild/openharmony-arm64", "npm:0.27.2"],\ + ["@esbuild/sunos-x64", "npm:0.27.2"],\ + ["@esbuild/win32-arm64", "npm:0.27.2"],\ + ["@esbuild/win32-ia32", "npm:0.27.2"],\ + ["@esbuild/win32-x64", "npm:0.27.2"],\ + ["esbuild", "npm:0.27.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["escalade", [\ ["npm:3.2.0", {\ "packageLocation": "./.yarn/cache/escalade-npm-3.2.0-19b50dd48f-9d7169e396.zip/node_modules/escalade/",\ @@ -8756,6 +10825,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["estree-walker", [\ + ["npm:2.0.2", {\ + "packageLocation": "./.yarn/cache/estree-walker-npm-2.0.2-dfab42f65c-b02109c5d4.zip/node_modules/estree-walker/",\ + "packageDependencies": [\ + ["estree-walker", "npm:2.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["esutils", [\ ["npm:2.0.3", {\ "packageLocation": "./.yarn/cache/esutils-npm-2.0.3-f865beafd5-b23acd2479.zip/node_modules/esutils/",\ @@ -8946,11 +11024,11 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0e783aadbd2b4b8e6f6056033c0b290501892d23bc7c5dad5477e00e48ad8bd3e4434c3962a52dd75a58e06dbb7218094a494bac954ef2f7f6fdb65d9717e5f4#npm:6.5.0", {\ - "packageLocation": "./.yarn/__virtual__/fdir-virtual-abd4ab2082/0/cache/fdir-npm-6.5.0-8814a0dec7-14ca1c9f0a.zip/node_modules/fdir/",\ + ["virtual:7080bd85426952f99562f3b207534c3738369d7585127ef68f72f62f38cfeb3fa02f2210d4b3dbaa7b672e7d677090d1dfa93aa4924900fc190ba59b1d57b99c#npm:6.5.0", {\ + "packageLocation": "./.yarn/__virtual__/fdir-virtual-e2dda2f638/0/cache/fdir-npm-6.5.0-8814a0dec7-14ca1c9f0a.zip/node_modules/fdir/",\ "packageDependencies": [\ ["@types/picomatch", null],\ - ["fdir", "virtual:0e783aadbd2b4b8e6f6056033c0b290501892d23bc7c5dad5477e00e48ad8bd3e4434c3962a52dd75a58e06dbb7218094a494bac954ef2f7f6fdb65d9717e5f4#npm:6.5.0"],\ + ["fdir", "virtual:7080bd85426952f99562f3b207534c3738369d7585127ef68f72f62f38cfeb3fa02f2210d4b3dbaa7b672e7d677090d1dfa93aa4924900fc190ba59b1d57b99c#npm:6.5.0"],\ ["picomatch", "npm:4.0.3"]\ ],\ "packagePeers": [\ @@ -9111,6 +11189,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["forwarded-parse", [\ + ["npm:2.1.2", {\ + "packageLocation": "./.yarn/cache/forwarded-parse-npm-2.1.2-8cf38fd641-fca4df8898.zip/node_modules/forwarded-parse/",\ + "packageDependencies": [\ + ["forwarded-parse", "npm:2.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["framer-motion", [\ ["npm:12.4.4", {\ "packageLocation": "./.yarn/cache/framer-motion-npm-12.4.4-23a597f2e4-400923b74d.zip/node_modules/framer-motion/",\ @@ -9345,6 +11432,14 @@ const RAW_RUNTIME_STATE = ["resolve-pkg-maps", "npm:1.0.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:4.13.1", {\ + "packageLocation": "./.yarn/cache/get-tsconfig-npm-4.13.1-10a4d287d1-a21b037241.zip/node_modules/get-tsconfig/",\ + "packageDependencies": [\ + ["get-tsconfig", "npm:4.13.1"],\ + ["resolve-pkg-maps", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["glob", [\ @@ -9360,6 +11455,16 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ + ["npm:13.0.6", {\ + "packageLocation": "./.yarn/cache/glob-npm-13.0.6-864eb0cece-201ad69e5f.zip/node_modules/glob/",\ + "packageDependencies": [\ + ["glob", "npm:13.0.6"],\ + ["minimatch", "npm:10.2.4"],\ + ["minipass", "npm:7.1.3"],\ + ["path-scurry", "npm:2.0.2"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:7.2.3", {\ "packageLocation": "./.yarn/cache/glob-npm-7.2.3-2d866d17a5-59452a9202.zip/node_modules/glob/",\ "packageDependencies": [\ @@ -9802,6 +11907,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["idb", [\ + ["npm:8.0.3", {\ + "packageLocation": "./.yarn/cache/idb-npm-8.0.3-e9b0a844f6-e2beccb0be.zip/node_modules/idb/",\ + "packageDependencies": [\ + ["idb", "npm:8.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["ignore", [\ ["npm:5.3.1", {\ "packageLocation": "./.yarn/cache/ignore-npm-5.3.1-f6947c5df7-0a884c2fbc.zip/node_modules/ignore/",\ @@ -9838,6 +11952,19 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["import-in-the-middle", [\ + ["npm:2.0.6", {\ + "packageLocation": "./.yarn/cache/import-in-the-middle-npm-2.0.6-233e5f3db3-8be80d7f2d.zip/node_modules/import-in-the-middle/",\ + "packageDependencies": [\ + ["acorn", "npm:8.15.0"],\ + ["acorn-import-attributes", "virtual:233e5f3db3715a0fa5ba20c83dbd25b39ee13185aca65de8109a05fe6cbee602a48e10dd53b72677a835252db9745d55402a5c7d80e6958c64331b2b770e3388#npm:1.9.5"],\ + ["cjs-module-lexer", "npm:2.2.0"],\ + ["import-in-the-middle", "npm:2.0.6"],\ + ["module-details-from-path", "npm:1.0.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["import-lazy", [\ ["npm:4.0.0", {\ "packageLocation": "./.yarn/cache/import-lazy-npm-4.0.0-3215653869-943309cc8e.zip/node_modules/import-lazy/",\ @@ -10263,6 +12390,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["is-reference", [\ + ["npm:1.2.1", {\ + "packageLocation": "./.yarn/cache/is-reference-npm-1.2.1-87ca1743c8-e7b48149f8.zip/node_modules/is-reference/",\ + "packageDependencies": [\ + ["@types/estree", "npm:1.0.8"],\ + ["is-reference", "npm:1.2.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["is-regex", [\ ["npm:1.1.4", {\ "packageLocation": "./.yarn/cache/is-regex-npm-1.1.4-cca193ef11-36d9174d16.zip/node_modules/is-regex/",\ @@ -11306,11 +13443,11 @@ const RAW_RUNTIME_STATE = ["@bcsdlab/utils", "npm:0.0.15"],\ ["@next/eslint-plugin-next", "npm:16.0.0"],\ ["@next/third-parties", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:15.5.2"],\ - ["@sentry/browser", "npm:9.14.0"],\ + ["@notionhq/client", "npm:5.9.0"],\ ["@sentry/cli", "npm:2.45.0"],\ - ["@stomp/stompjs", "npm:7.0.0"],\ + ["@sentry/nextjs", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:10.43.0"],\ ["@svgr/webpack", "npm:8.1.0"],\ - ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.28.6"],\ + ["@tanstack/react-query", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.90.21"],\ ["@testing-library/jest-dom", "npm:5.17.0"],\ ["@testing-library/react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:13.4.0"],\ ["@testing-library/user-event", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:13.5.0"],\ @@ -11324,6 +13461,7 @@ const RAW_RUNTIME_STATE = ["@types/react-window", "npm:1.8.8"],\ ["axios", "npm:0.27.2"],\ ["dayjs", "npm:1.11.12"],\ + ["dotenv", "npm:17.2.3"],\ ["embla-carousel-autoplay", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:8.0.4"],\ ["embla-carousel-react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:8.0.4"],\ ["eslint", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:9.38.0"],\ @@ -11339,6 +13477,7 @@ const RAW_RUNTIME_STATE = ["globals", "npm:16.4.0"],\ ["html-react-parser", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.1.10"],\ ["html2canvas", "npm:1.4.1"],\ + ["idb", "npm:8.0.3"],\ ["jest", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:29.7.0"],\ ["koin_web_recode", "workspace:."],\ ["lottie-react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:2.4.1"],\ @@ -11359,6 +13498,7 @@ const RAW_RUNTIME_STATE = ["stylelint-config-standard", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:26.0.0"],\ ["stylelint-config-standard-scss", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:4.0.0"],\ ["stylelint-selector-bem-pattern", "npm:2.1.1"],\ + ["tsx", "npm:4.21.0"],\ ["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5"],\ ["typescript-eslint", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:8.46.2"],\ ["web-vitals", "npm:2.1.4"],\ @@ -11560,6 +13700,13 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ + ["npm:11.2.6", {\ + "packageLocation": "./.yarn/cache/lru-cache-npm-11.2.6-acb7d4323e-91222bbd59.zip/node_modules/lru-cache/",\ + "packageDependencies": [\ + ["lru-cache", "npm:11.2.6"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:5.1.1", {\ "packageLocation": "./.yarn/cache/lru-cache-npm-5.1.1-f475882a51-951d2673dc.zip/node_modules/lru-cache/",\ "packageDependencies": [\ @@ -11586,6 +13733,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["magic-string", [\ + ["npm:0.30.21", {\ + "packageLocation": "./.yarn/cache/magic-string-npm-0.30.21-9a226cb21e-57d5691f41.zip/node_modules/magic-string/",\ + "packageDependencies": [\ + ["@jridgewell/sourcemap-codec", "npm:1.5.5"],\ + ["magic-string", "npm:0.30.21"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["make-dir", [\ ["npm:4.0.0", {\ "packageLocation": "./.yarn/cache/make-dir-npm-4.0.0-ec3cd921cc-bf0731a2dd.zip/node_modules/make-dir/",\ @@ -11780,6 +13937,14 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["minimatch", [\ + ["npm:10.2.4", {\ + "packageLocation": "./.yarn/cache/minimatch-npm-10.2.4-11f0605299-aea4874e52.zip/node_modules/minimatch/",\ + "packageDependencies": [\ + ["brace-expansion", "npm:5.0.4"],\ + ["minimatch", "npm:10.2.4"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:3.1.2", {\ "packageLocation": "./.yarn/cache/minimatch-npm-3.1.2-9405269906-e0b25b04cd.zip/node_modules/minimatch/",\ "packageDependencies": [\ @@ -11848,6 +14013,13 @@ const RAW_RUNTIME_STATE = ["minipass", "npm:7.0.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.1.3", {\ + "packageLocation": "./.yarn/cache/minipass-npm-7.1.3-b73a16498d-175e4d5e20.zip/node_modules/minipass/",\ + "packageDependencies": [\ + ["minipass", "npm:7.1.3"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["minipass-collect", [\ @@ -11923,6 +14095,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["module-details-from-path", [\ + ["npm:1.0.4", {\ + "packageLocation": "./.yarn/cache/module-details-from-path-npm-1.0.4-c3d0545459-2ebfada535.zip/node_modules/module-details-from-path/",\ + "packageDependencies": [\ + ["module-details-from-path", "npm:1.0.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["motion-dom", [\ ["npm:12.4.4", {\ "packageLocation": "./.yarn/cache/motion-dom-npm-12.4.4-d68ef9ec84-75d81438be.zip/node_modules/motion-dom/",\ @@ -12516,6 +14697,15 @@ const RAW_RUNTIME_STATE = ["path-scurry", "npm:1.10.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.0.2", {\ + "packageLocation": "./.yarn/cache/path-scurry-npm-2.0.2-f10aa6a77e-2b4257422b.zip/node_modules/path-scurry/",\ + "packageDependencies": [\ + ["lru-cache", "npm:11.2.6"],\ + ["minipass", "npm:7.1.3"],\ + ["path-scurry", "npm:2.0.2"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["path-type", [\ @@ -12527,6 +14717,38 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["pg-int8", [\ + ["npm:1.0.1", {\ + "packageLocation": "./.yarn/cache/pg-int8-npm-1.0.1-5cd67f3e22-a1e3a05a69.zip/node_modules/pg-int8/",\ + "packageDependencies": [\ + ["pg-int8", "npm:1.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["pg-protocol", [\ + ["npm:1.13.0", {\ + "packageLocation": "./.yarn/cache/pg-protocol-npm-1.13.0-d380339def-302cd3920d.zip/node_modules/pg-protocol/",\ + "packageDependencies": [\ + ["pg-protocol", "npm:1.13.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["pg-types", [\ + ["npm:2.2.0", {\ + "packageLocation": "./.yarn/cache/pg-types-npm-2.2.0-a3360226c4-87a84d4baa.zip/node_modules/pg-types/",\ + "packageDependencies": [\ + ["pg-int8", "npm:1.0.1"],\ + ["pg-types", "npm:2.2.0"],\ + ["postgres-array", "npm:2.0.0"],\ + ["postgres-bytea", "npm:1.0.1"],\ + ["postgres-date", "npm:1.0.7"],\ + ["postgres-interval", "npm:1.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["picocolors", [\ ["npm:0.2.1", {\ "packageLocation": "./.yarn/cache/picocolors-npm-0.2.1-fa0e648c44-3b0f441f00.zip/node_modules/picocolors/",\ @@ -12756,6 +14978,43 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["postgres-array", [\ + ["npm:2.0.0", {\ + "packageLocation": "./.yarn/cache/postgres-array-npm-2.0.0-4f49dc1389-aff99e7971.zip/node_modules/postgres-array/",\ + "packageDependencies": [\ + ["postgres-array", "npm:2.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["postgres-bytea", [\ + ["npm:1.0.1", {\ + "packageLocation": "./.yarn/cache/postgres-bytea-npm-1.0.1-33f7758ac9-fc5fa49f59.zip/node_modules/postgres-bytea/",\ + "packageDependencies": [\ + ["postgres-bytea", "npm:1.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["postgres-date", [\ + ["npm:1.0.7", {\ + "packageLocation": "./.yarn/cache/postgres-date-npm-1.0.7-aadfe5531e-571ef45bec.zip/node_modules/postgres-date/",\ + "packageDependencies": [\ + ["postgres-date", "npm:1.0.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["postgres-interval", [\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/cache/postgres-interval-npm-1.2.0-ca6414744d-746b71f938.zip/node_modules/postgres-interval/",\ + "packageDependencies": [\ + ["postgres-interval", "npm:1.2.0"],\ + ["xtend", "npm:4.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["prelude-ls", [\ ["npm:1.2.1", {\ "packageLocation": "./.yarn/cache/prelude-ls-npm-1.2.1-3e4d272a55-0b9d2c7680.zip/node_modules/prelude-ls/",\ @@ -13343,6 +15602,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["require-in-the-middle", [\ + ["npm:8.0.1", {\ + "packageLocation": "./.yarn/cache/require-in-the-middle-npm-8.0.1-096af9b870-4ce98c6814.zip/node_modules/require-in-the-middle/",\ + "packageDependencies": [\ + ["debug", "virtual:096af9b870a5e180ddb16cfa36c1dc97bc1ba878bc559221fe9184d27f738627139de407e4cc683ebeeaa875d8a403a7f02b4597de5cf2cca18231f15c7f74c8#npm:4.4.3"],\ + ["module-details-from-path", "npm:1.0.4"],\ + ["require-in-the-middle", "npm:8.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["reset-css", [\ ["npm:5.0.2", {\ "packageLocation": "./.yarn/cache/reset-css-npm-5.0.2-56958a3b8c-d6955ea4d8.zip/node_modules/reset-css/",\ @@ -13464,6 +15734,42 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["rollup", [\ + ["npm:4.59.0", {\ + "packageLocation": "./.yarn/cache/rollup-npm-4.59.0-3b10f603ec-728237932a.zip/node_modules/rollup/",\ + "packageDependencies": [\ + ["@rollup/rollup-android-arm-eabi", "npm:4.59.0"],\ + ["@rollup/rollup-android-arm64", "npm:4.59.0"],\ + ["@rollup/rollup-darwin-arm64", "npm:4.59.0"],\ + ["@rollup/rollup-darwin-x64", "npm:4.59.0"],\ + ["@rollup/rollup-freebsd-arm64", "npm:4.59.0"],\ + ["@rollup/rollup-freebsd-x64", "npm:4.59.0"],\ + ["@rollup/rollup-linux-arm-gnueabihf", "npm:4.59.0"],\ + ["@rollup/rollup-linux-arm-musleabihf", "npm:4.59.0"],\ + ["@rollup/rollup-linux-arm64-gnu", "npm:4.59.0"],\ + ["@rollup/rollup-linux-arm64-musl", "npm:4.59.0"],\ + ["@rollup/rollup-linux-loong64-gnu", "npm:4.59.0"],\ + ["@rollup/rollup-linux-loong64-musl", "npm:4.59.0"],\ + ["@rollup/rollup-linux-ppc64-gnu", "npm:4.59.0"],\ + ["@rollup/rollup-linux-ppc64-musl", "npm:4.59.0"],\ + ["@rollup/rollup-linux-riscv64-gnu", "npm:4.59.0"],\ + ["@rollup/rollup-linux-riscv64-musl", "npm:4.59.0"],\ + ["@rollup/rollup-linux-s390x-gnu", "npm:4.59.0"],\ + ["@rollup/rollup-linux-x64-gnu", "npm:4.59.0"],\ + ["@rollup/rollup-linux-x64-musl", "npm:4.59.0"],\ + ["@rollup/rollup-openbsd-x64", "npm:4.59.0"],\ + ["@rollup/rollup-openharmony-arm64", "npm:4.59.0"],\ + ["@rollup/rollup-win32-arm64-msvc", "npm:4.59.0"],\ + ["@rollup/rollup-win32-ia32-msvc", "npm:4.59.0"],\ + ["@rollup/rollup-win32-x64-gnu", "npm:4.59.0"],\ + ["@rollup/rollup-win32-x64-msvc", "npm:4.59.0"],\ + ["@types/estree", "npm:1.0.8"],\ + ["fsevents", "patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1"],\ + ["rollup", "npm:4.59.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["run-parallel", [\ ["npm:1.2.0", {\ "packageLocation": "./.yarn/cache/run-parallel-npm-1.2.0-3f47ff2034-cb4f97ad25.zip/node_modules/run-parallel/",\ @@ -13999,6 +16305,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["stacktrace-parser", [\ + ["npm:0.1.11", {\ + "packageLocation": "./.yarn/cache/stacktrace-parser-npm-0.1.11-2d5238cd3f-1120cf7166.zip/node_modules/stacktrace-parser/",\ + "packageDependencies": [\ + ["stacktrace-parser", "npm:0.1.11"],\ + ["type-fest", "npm:0.7.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["stop-iteration-iterator", [\ ["npm:1.0.0", {\ "packageLocation": "./.yarn/cache/stop-iteration-iterator-npm-1.0.0-ea451e1609-2a23a36f4f.zip/node_modules/stop-iteration-iterator/",\ @@ -14718,7 +17034,7 @@ const RAW_RUNTIME_STATE = ["npm:0.2.15", {\ "packageLocation": "./.yarn/cache/tinyglobby-npm-0.2.15-0e783aadbd-d72bd826a8.zip/node_modules/tinyglobby/",\ "packageDependencies": [\ - ["fdir", "virtual:0e783aadbd2b4b8e6f6056033c0b290501892d23bc7c5dad5477e00e48ad8bd3e4434c3962a52dd75a58e06dbb7218094a494bac954ef2f7f6fdb65d9717e5f4#npm:6.5.0"],\ + ["fdir", "virtual:7080bd85426952f99562f3b207534c3738369d7585127ef68f72f62f38cfeb3fa02f2210d4b3dbaa7b672e7d677090d1dfa93aa4924900fc190ba59b1d57b99c#npm:6.5.0"],\ ["picomatch", "npm:4.0.3"],\ ["tinyglobby", "npm:0.2.15"]\ ],\ @@ -14822,6 +17138,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["tsx", [\ + ["npm:4.21.0", {\ + "packageLocation": "./.yarn/cache/tsx-npm-4.21.0-3bc9626d81-7afedeff85.zip/node_modules/tsx/",\ + "packageDependencies": [\ + ["esbuild", "npm:0.27.2"],\ + ["fsevents", "patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1"],\ + ["get-tsconfig", "npm:4.13.1"],\ + ["tsx", "npm:4.21.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["type-check", [\ ["npm:0.4.0", {\ "packageLocation": "./.yarn/cache/type-check-npm-0.4.0-60565800ce-1468777647.zip/node_modules/type-check/",\ @@ -14863,6 +17191,13 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ + ["npm:0.7.1", {\ + "packageLocation": "./.yarn/cache/type-fest-npm-0.7.1-7b37912923-0699b6011b.zip/node_modules/type-fest/",\ + "packageDependencies": [\ + ["type-fest", "npm:0.7.1"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:0.8.1", {\ "packageLocation": "./.yarn/cache/type-fest-npm-0.8.1-351ad028fe-fd4a91bfb7.zip/node_modules/type-fest/",\ "packageDependencies": [\ @@ -15227,6 +17562,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["uuid", [\ + ["npm:9.0.1", {\ + "packageLocation": "./.yarn/cache/uuid-npm-9.0.1-39a8442bc6-9d0b6adb72.zip/node_modules/uuid/",\ + "packageDependencies": [\ + ["uuid", "npm:9.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["v8-compile-cache", [\ ["npm:2.4.0", {\ "packageLocation": "./.yarn/cache/v8-compile-cache-npm-2.4.0-5979f8e405-49e726d7b2.zip/node_modules/v8-compile-cache/",\ @@ -15475,6 +17819,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["xtend", [\ + ["npm:4.0.2", {\ + "packageLocation": "./.yarn/cache/xtend-npm-4.0.2-7f2375736e-ac5dfa738b.zip/node_modules/xtend/",\ + "packageDependencies": [\ + ["xtend", "npm:4.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["y18n", [\ ["npm:5.0.8", {\ "packageLocation": "./.yarn/cache/y18n-npm-5.0.8-5f3a0a7e62-5f1b5f95e3.zip/node_modules/y18n/",\ diff --git a/.stylelint.json b/.stylelint.json index 8cd313a15..6d9e1fd23 100644 --- a/.stylelint.json +++ b/.stylelint.json @@ -1,11 +1,7 @@ { - "extends": [ - "stylelint-config-standard-scss" - ], - "plugin": [ - "stylelint-selector-bem-pattern" - ], - "rules": { - "selector-class-pattern": null + "extends": ["stylelint-config-standard-scss"], + "plugin": ["stylelint-selector-bem-pattern"], + "rules": { + "selector-class-pattern": null } } diff --git a/.yarn/cache/@babel-code-frame-npm-7.29.0-6c4947d913-199e15ff89.zip b/.yarn/cache/@babel-code-frame-npm-7.29.0-6c4947d913-199e15ff89.zip new file mode 100644 index 000000000..97cb32280 Binary files /dev/null and b/.yarn/cache/@babel-code-frame-npm-7.29.0-6c4947d913-199e15ff89.zip differ diff --git a/.yarn/cache/@babel-compat-data-npm-7.29.0-6b4382e79f-7f21beedb9.zip b/.yarn/cache/@babel-compat-data-npm-7.29.0-6b4382e79f-7f21beedb9.zip new file mode 100644 index 000000000..cde8b7e3c Binary files /dev/null and b/.yarn/cache/@babel-compat-data-npm-7.29.0-6b4382e79f-7f21beedb9.zip differ diff --git a/.yarn/cache/@babel-core-npm-7.29.0-a74bfc561b-25f4e91688.zip b/.yarn/cache/@babel-core-npm-7.29.0-a74bfc561b-25f4e91688.zip new file mode 100644 index 000000000..624e6b04e Binary files /dev/null and b/.yarn/cache/@babel-core-npm-7.29.0-a74bfc561b-25f4e91688.zip differ diff --git a/.yarn/cache/@babel-generator-npm-7.29.1-b1bf16fe79-61fe4ddd6e.zip b/.yarn/cache/@babel-generator-npm-7.29.1-b1bf16fe79-61fe4ddd6e.zip new file mode 100644 index 000000000..dac522116 Binary files /dev/null and b/.yarn/cache/@babel-generator-npm-7.29.1-b1bf16fe79-61fe4ddd6e.zip differ diff --git a/.yarn/cache/@babel-helper-compilation-targets-npm-7.28.6-8880f389c9-f512a5aeee.zip b/.yarn/cache/@babel-helper-compilation-targets-npm-7.28.6-8880f389c9-f512a5aeee.zip new file mode 100644 index 000000000..ff63e1b68 Binary files /dev/null and b/.yarn/cache/@babel-helper-compilation-targets-npm-7.28.6-8880f389c9-f512a5aeee.zip differ diff --git a/.yarn/cache/@babel-helper-module-imports-npm-7.28.6-5b95b9145c-64b1380d74.zip b/.yarn/cache/@babel-helper-module-imports-npm-7.28.6-5b95b9145c-64b1380d74.zip new file mode 100644 index 000000000..6b77ecc2c Binary files /dev/null and b/.yarn/cache/@babel-helper-module-imports-npm-7.28.6-5b95b9145c-64b1380d74.zip differ diff --git a/.yarn/cache/@babel-helper-module-transforms-npm-7.28.6-5923cf5a95-2e421c7db7.zip b/.yarn/cache/@babel-helper-module-transforms-npm-7.28.6-5923cf5a95-2e421c7db7.zip new file mode 100644 index 000000000..48651a8a4 Binary files /dev/null and b/.yarn/cache/@babel-helper-module-transforms-npm-7.28.6-5923cf5a95-2e421c7db7.zip differ diff --git a/.yarn/cache/@babel-helper-validator-identifier-npm-7.28.5-1953d49d2b-8e5d9b0133.zip b/.yarn/cache/@babel-helper-validator-identifier-npm-7.28.5-1953d49d2b-8e5d9b0133.zip new file mode 100644 index 000000000..c67a0ac56 Binary files /dev/null and b/.yarn/cache/@babel-helper-validator-identifier-npm-7.28.5-1953d49d2b-8e5d9b0133.zip differ diff --git a/.yarn/cache/@babel-helpers-npm-7.28.6-682df48628-213485cdff.zip b/.yarn/cache/@babel-helpers-npm-7.28.6-682df48628-213485cdff.zip new file mode 100644 index 000000000..c894d4a28 Binary files /dev/null and b/.yarn/cache/@babel-helpers-npm-7.28.6-682df48628-213485cdff.zip differ diff --git a/.yarn/cache/@babel-parser-npm-7.29.0-c605c63e8b-b1576dca41.zip b/.yarn/cache/@babel-parser-npm-7.29.0-c605c63e8b-b1576dca41.zip new file mode 100644 index 000000000..e643b275e Binary files /dev/null and b/.yarn/cache/@babel-parser-npm-7.29.0-c605c63e8b-b1576dca41.zip differ diff --git a/.yarn/cache/@babel-template-npm-7.28.6-bff3bc3923-0ad6e32bf1.zip b/.yarn/cache/@babel-template-npm-7.28.6-bff3bc3923-0ad6e32bf1.zip new file mode 100644 index 000000000..970194132 Binary files /dev/null and b/.yarn/cache/@babel-template-npm-7.28.6-bff3bc3923-0ad6e32bf1.zip differ diff --git a/.yarn/cache/@babel-traverse-npm-7.29.0-85d5d916b6-3a0d0438f1.zip b/.yarn/cache/@babel-traverse-npm-7.29.0-85d5d916b6-3a0d0438f1.zip new file mode 100644 index 000000000..d45bb446b Binary files /dev/null and b/.yarn/cache/@babel-traverse-npm-7.29.0-85d5d916b6-3a0d0438f1.zip differ diff --git a/.yarn/cache/@babel-types-npm-7.29.0-6c2fa77581-bfc2b21121.zip b/.yarn/cache/@babel-types-npm-7.29.0-6c2fa77581-bfc2b21121.zip new file mode 100644 index 000000000..04c1b9173 Binary files /dev/null and b/.yarn/cache/@babel-types-npm-7.29.0-6c2fa77581-bfc2b21121.zip differ diff --git a/.yarn/cache/@esbuild-darwin-arm64-npm-0.27.2-d675c4a521-10.zip b/.yarn/cache/@esbuild-darwin-arm64-npm-0.27.2-d675c4a521-10.zip new file mode 100644 index 000000000..df4de2225 Binary files /dev/null and b/.yarn/cache/@esbuild-darwin-arm64-npm-0.27.2-d675c4a521-10.zip differ diff --git a/.yarn/cache/@fastify-otel-npm-0.16.0-d2ae32e4f2-b8a4e12285.zip b/.yarn/cache/@fastify-otel-npm-0.16.0-d2ae32e4f2-b8a4e12285.zip new file mode 100644 index 000000000..96d1b334d Binary files /dev/null and b/.yarn/cache/@fastify-otel-npm-0.16.0-d2ae32e4f2-b8a4e12285.zip differ diff --git a/.yarn/cache/@notionhq-client-npm-5.9.0-ed780ff571-337e2377fb.zip b/.yarn/cache/@notionhq-client-npm-5.9.0-ed780ff571-337e2377fb.zip new file mode 100644 index 000000000..3d1b10c0a Binary files /dev/null and b/.yarn/cache/@notionhq-client-npm-5.9.0-ed780ff571-337e2377fb.zip differ diff --git a/.yarn/cache/@opentelemetry-api-logs-npm-0.207.0-e198d835d9-d50251e34f.zip b/.yarn/cache/@opentelemetry-api-logs-npm-0.207.0-e198d835d9-d50251e34f.zip new file mode 100644 index 000000000..cfaca0bbd Binary files /dev/null and b/.yarn/cache/@opentelemetry-api-logs-npm-0.207.0-e198d835d9-d50251e34f.zip differ diff --git a/.yarn/cache/@opentelemetry-api-logs-npm-0.208.0-3af8bf5803-ae339416a2.zip b/.yarn/cache/@opentelemetry-api-logs-npm-0.208.0-3af8bf5803-ae339416a2.zip new file mode 100644 index 000000000..e7bd8ea0a Binary files /dev/null and b/.yarn/cache/@opentelemetry-api-logs-npm-0.208.0-3af8bf5803-ae339416a2.zip differ diff --git a/.yarn/cache/@opentelemetry-api-logs-npm-0.211.0-1661a3c311-d2e7ac9d99.zip b/.yarn/cache/@opentelemetry-api-logs-npm-0.211.0-1661a3c311-d2e7ac9d99.zip new file mode 100644 index 000000000..78fd7fc90 Binary files /dev/null and b/.yarn/cache/@opentelemetry-api-logs-npm-0.211.0-1661a3c311-d2e7ac9d99.zip differ diff --git a/.yarn/cache/@opentelemetry-api-npm-1.9.0-7d0560d0dd-a607f0eef9.zip b/.yarn/cache/@opentelemetry-api-npm-1.9.0-7d0560d0dd-a607f0eef9.zip new file mode 100644 index 000000000..168198e9b Binary files /dev/null and b/.yarn/cache/@opentelemetry-api-npm-1.9.0-7d0560d0dd-a607f0eef9.zip differ diff --git a/.yarn/cache/@opentelemetry-context-async-hooks-npm-2.6.0-18d63e1e37-a1f746fb9b.zip b/.yarn/cache/@opentelemetry-context-async-hooks-npm-2.6.0-18d63e1e37-a1f746fb9b.zip new file mode 100644 index 000000000..1b893e7bb Binary files /dev/null and b/.yarn/cache/@opentelemetry-context-async-hooks-npm-2.6.0-18d63e1e37-a1f746fb9b.zip differ diff --git a/.yarn/cache/@opentelemetry-core-npm-2.5.0-00e0987751-62cb4721af.zip b/.yarn/cache/@opentelemetry-core-npm-2.5.0-00e0987751-62cb4721af.zip new file mode 100644 index 000000000..f4723f4af Binary files /dev/null and b/.yarn/cache/@opentelemetry-core-npm-2.5.0-00e0987751-62cb4721af.zip differ diff --git a/.yarn/cache/@opentelemetry-core-npm-2.6.0-bf4a2b221e-21c017cc68.zip b/.yarn/cache/@opentelemetry-core-npm-2.6.0-bf4a2b221e-21c017cc68.zip new file mode 100644 index 000000000..c4ab8ca6b Binary files /dev/null and b/.yarn/cache/@opentelemetry-core-npm-2.6.0-bf4a2b221e-21c017cc68.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-amqplib-npm-0.58.0-4a0bde0e6c-de6d6cd239.zip b/.yarn/cache/@opentelemetry-instrumentation-amqplib-npm-0.58.0-4a0bde0e6c-de6d6cd239.zip new file mode 100644 index 000000000..e89c2ba6a Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-amqplib-npm-0.58.0-4a0bde0e6c-de6d6cd239.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-connect-npm-0.54.0-5f669c7fc1-335e42610a.zip b/.yarn/cache/@opentelemetry-instrumentation-connect-npm-0.54.0-5f669c7fc1-335e42610a.zip new file mode 100644 index 000000000..754b44b6b Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-connect-npm-0.54.0-5f669c7fc1-335e42610a.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-dataloader-npm-0.28.0-90e63b72ad-e2c30ee9d4.zip b/.yarn/cache/@opentelemetry-instrumentation-dataloader-npm-0.28.0-90e63b72ad-e2c30ee9d4.zip new file mode 100644 index 000000000..75ce76519 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-dataloader-npm-0.28.0-90e63b72ad-e2c30ee9d4.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-express-npm-0.59.0-4c871e3957-a7da4e4942.zip b/.yarn/cache/@opentelemetry-instrumentation-express-npm-0.59.0-4c871e3957-a7da4e4942.zip new file mode 100644 index 000000000..283fc1bf5 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-express-npm-0.59.0-4c871e3957-a7da4e4942.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-fs-npm-0.30.0-3f3c0c2d74-5bae626d15.zip b/.yarn/cache/@opentelemetry-instrumentation-fs-npm-0.30.0-3f3c0c2d74-5bae626d15.zip new file mode 100644 index 000000000..5a98d7616 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-fs-npm-0.30.0-3f3c0c2d74-5bae626d15.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-generic-pool-npm-0.54.0-f61269cfac-3a92649c14.zip b/.yarn/cache/@opentelemetry-instrumentation-generic-pool-npm-0.54.0-f61269cfac-3a92649c14.zip new file mode 100644 index 000000000..bbb4843c5 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-generic-pool-npm-0.54.0-f61269cfac-3a92649c14.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-graphql-npm-0.58.0-f8600b827a-a87490490a.zip b/.yarn/cache/@opentelemetry-instrumentation-graphql-npm-0.58.0-f8600b827a-a87490490a.zip new file mode 100644 index 000000000..c2c6308e4 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-graphql-npm-0.58.0-f8600b827a-a87490490a.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-hapi-npm-0.57.0-8c15961caf-142684b85a.zip b/.yarn/cache/@opentelemetry-instrumentation-hapi-npm-0.57.0-8c15961caf-142684b85a.zip new file mode 100644 index 000000000..2940fe280 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-hapi-npm-0.57.0-8c15961caf-142684b85a.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-http-npm-0.211.0-daf732bd8d-f20ca2e9f4.zip b/.yarn/cache/@opentelemetry-instrumentation-http-npm-0.211.0-daf732bd8d-f20ca2e9f4.zip new file mode 100644 index 000000000..8c4a62ea3 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-http-npm-0.211.0-daf732bd8d-f20ca2e9f4.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-ioredis-npm-0.59.0-428da74c41-59ba1ccf7f.zip b/.yarn/cache/@opentelemetry-instrumentation-ioredis-npm-0.59.0-428da74c41-59ba1ccf7f.zip new file mode 100644 index 000000000..c27bc515d Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-ioredis-npm-0.59.0-428da74c41-59ba1ccf7f.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-kafkajs-npm-0.20.0-acbf67f0ec-f6d67ab1fc.zip b/.yarn/cache/@opentelemetry-instrumentation-kafkajs-npm-0.20.0-acbf67f0ec-f6d67ab1fc.zip new file mode 100644 index 000000000..09450976d Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-kafkajs-npm-0.20.0-acbf67f0ec-f6d67ab1fc.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-knex-npm-0.55.0-ba0c29fdf4-02d9a3c194.zip b/.yarn/cache/@opentelemetry-instrumentation-knex-npm-0.55.0-ba0c29fdf4-02d9a3c194.zip new file mode 100644 index 000000000..4cdf5daff Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-knex-npm-0.55.0-ba0c29fdf4-02d9a3c194.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-koa-npm-0.59.0-9edebdc2da-604468baf6.zip b/.yarn/cache/@opentelemetry-instrumentation-koa-npm-0.59.0-9edebdc2da-604468baf6.zip new file mode 100644 index 000000000..e1992d7e0 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-koa-npm-0.59.0-9edebdc2da-604468baf6.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-lru-memoizer-npm-0.55.0-cdc9d61fba-6c2594032e.zip b/.yarn/cache/@opentelemetry-instrumentation-lru-memoizer-npm-0.55.0-cdc9d61fba-6c2594032e.zip new file mode 100644 index 000000000..d88ed92ac Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-lru-memoizer-npm-0.55.0-cdc9d61fba-6c2594032e.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-mongodb-npm-0.64.0-1c30b58c91-ecaef6f687.zip b/.yarn/cache/@opentelemetry-instrumentation-mongodb-npm-0.64.0-1c30b58c91-ecaef6f687.zip new file mode 100644 index 000000000..21699aa13 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-mongodb-npm-0.64.0-1c30b58c91-ecaef6f687.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-mongoose-npm-0.57.0-aec108899e-a64878d3a6.zip b/.yarn/cache/@opentelemetry-instrumentation-mongoose-npm-0.57.0-aec108899e-a64878d3a6.zip new file mode 100644 index 000000000..ef267848e Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-mongoose-npm-0.57.0-aec108899e-a64878d3a6.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-mysql-npm-0.57.0-efcfbde38c-d36cfca4f0.zip b/.yarn/cache/@opentelemetry-instrumentation-mysql-npm-0.57.0-efcfbde38c-d36cfca4f0.zip new file mode 100644 index 000000000..1768e14fa Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-mysql-npm-0.57.0-efcfbde38c-d36cfca4f0.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-mysql2-npm-0.57.0-530b730937-b1d120318c.zip b/.yarn/cache/@opentelemetry-instrumentation-mysql2-npm-0.57.0-530b730937-b1d120318c.zip new file mode 100644 index 000000000..cb62c682a Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-mysql2-npm-0.57.0-530b730937-b1d120318c.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-npm-0.207.0-d254f2d9c8-ea9b9a7324.zip b/.yarn/cache/@opentelemetry-instrumentation-npm-0.207.0-d254f2d9c8-ea9b9a7324.zip new file mode 100644 index 000000000..390bf412c Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-npm-0.207.0-d254f2d9c8-ea9b9a7324.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-npm-0.208.0-3730549526-0591121c1b.zip b/.yarn/cache/@opentelemetry-instrumentation-npm-0.208.0-3730549526-0591121c1b.zip new file mode 100644 index 000000000..d95e2a41a Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-npm-0.208.0-3730549526-0591121c1b.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-npm-0.211.0-1431153acb-d9efa3bf91.zip b/.yarn/cache/@opentelemetry-instrumentation-npm-0.211.0-1431153acb-d9efa3bf91.zip new file mode 100644 index 000000000..4ecfec45e Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-npm-0.211.0-1431153acb-d9efa3bf91.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-pg-npm-0.63.0-c7392f516f-f937e1e535.zip b/.yarn/cache/@opentelemetry-instrumentation-pg-npm-0.63.0-c7392f516f-f937e1e535.zip new file mode 100644 index 000000000..4d020f75f Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-pg-npm-0.63.0-c7392f516f-f937e1e535.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-redis-npm-0.59.0-5877bd7b80-b5f455901d.zip b/.yarn/cache/@opentelemetry-instrumentation-redis-npm-0.59.0-5877bd7b80-b5f455901d.zip new file mode 100644 index 000000000..127a4c808 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-redis-npm-0.59.0-5877bd7b80-b5f455901d.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-tedious-npm-0.30.0-8766617d06-d8f62caa55.zip b/.yarn/cache/@opentelemetry-instrumentation-tedious-npm-0.30.0-8766617d06-d8f62caa55.zip new file mode 100644 index 000000000..a5500a840 Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-tedious-npm-0.30.0-8766617d06-d8f62caa55.zip differ diff --git a/.yarn/cache/@opentelemetry-instrumentation-undici-npm-0.21.0-c2e5951e5b-e2e39d7ef3.zip b/.yarn/cache/@opentelemetry-instrumentation-undici-npm-0.21.0-c2e5951e5b-e2e39d7ef3.zip new file mode 100644 index 000000000..83d3cbefb Binary files /dev/null and b/.yarn/cache/@opentelemetry-instrumentation-undici-npm-0.21.0-c2e5951e5b-e2e39d7ef3.zip differ diff --git a/.yarn/cache/@opentelemetry-redis-common-npm-0.38.2-2a4cd967c7-2a4f992572.zip b/.yarn/cache/@opentelemetry-redis-common-npm-0.38.2-2a4cd967c7-2a4f992572.zip new file mode 100644 index 000000000..dfaac21eb Binary files /dev/null and b/.yarn/cache/@opentelemetry-redis-common-npm-0.38.2-2a4cd967c7-2a4f992572.zip differ diff --git a/.yarn/cache/@opentelemetry-resources-npm-2.6.0-100fb3be54-837e76911d.zip b/.yarn/cache/@opentelemetry-resources-npm-2.6.0-100fb3be54-837e76911d.zip new file mode 100644 index 000000000..af6c01409 Binary files /dev/null and b/.yarn/cache/@opentelemetry-resources-npm-2.6.0-100fb3be54-837e76911d.zip differ diff --git a/.yarn/cache/@opentelemetry-sdk-trace-base-npm-2.6.0-e7da6f6e16-8ca3c1c4d7.zip b/.yarn/cache/@opentelemetry-sdk-trace-base-npm-2.6.0-e7da6f6e16-8ca3c1c4d7.zip new file mode 100644 index 000000000..1ac04305e Binary files /dev/null and b/.yarn/cache/@opentelemetry-sdk-trace-base-npm-2.6.0-e7da6f6e16-8ca3c1c4d7.zip differ diff --git a/.yarn/cache/@opentelemetry-semantic-conventions-npm-1.40.0-d0b94a9adb-edb5889459.zip b/.yarn/cache/@opentelemetry-semantic-conventions-npm-1.40.0-d0b94a9adb-edb5889459.zip new file mode 100644 index 000000000..6d1c454d1 Binary files /dev/null and b/.yarn/cache/@opentelemetry-semantic-conventions-npm-1.40.0-d0b94a9adb-edb5889459.zip differ diff --git a/.yarn/cache/@opentelemetry-sql-common-npm-0.41.2-1bbdc61904-3d57d5162c.zip b/.yarn/cache/@opentelemetry-sql-common-npm-0.41.2-1bbdc61904-3d57d5162c.zip new file mode 100644 index 000000000..583e82917 Binary files /dev/null and b/.yarn/cache/@opentelemetry-sql-common-npm-0.41.2-1bbdc61904-3d57d5162c.zip differ diff --git a/.yarn/cache/@prisma-instrumentation-npm-7.2.0-3bb371cc25-a02d16543f.zip b/.yarn/cache/@prisma-instrumentation-npm-7.2.0-3bb371cc25-a02d16543f.zip new file mode 100644 index 000000000..d35e61c71 Binary files /dev/null and b/.yarn/cache/@prisma-instrumentation-npm-7.2.0-3bb371cc25-a02d16543f.zip differ diff --git a/.yarn/cache/@rollup-plugin-commonjs-npm-28.0.1-5224cbb009-e01d26ce41.zip b/.yarn/cache/@rollup-plugin-commonjs-npm-28.0.1-5224cbb009-e01d26ce41.zip new file mode 100644 index 000000000..2438aa2da Binary files /dev/null and b/.yarn/cache/@rollup-plugin-commonjs-npm-28.0.1-5224cbb009-e01d26ce41.zip differ diff --git a/.yarn/cache/@rollup-pluginutils-npm-5.3.0-41141e497e-6c7dbab90e.zip b/.yarn/cache/@rollup-pluginutils-npm-5.3.0-41141e497e-6c7dbab90e.zip new file mode 100644 index 000000000..45bf24f8c Binary files /dev/null and b/.yarn/cache/@rollup-pluginutils-npm-5.3.0-41141e497e-6c7dbab90e.zip differ diff --git a/.yarn/cache/@rollup-rollup-darwin-arm64-npm-4.59.0-db3495ba42-10.zip b/.yarn/cache/@rollup-rollup-darwin-arm64-npm-4.59.0-db3495ba42-10.zip new file mode 100644 index 000000000..1c87f6f86 Binary files /dev/null and b/.yarn/cache/@rollup-rollup-darwin-arm64-npm-4.59.0-db3495ba42-10.zip differ diff --git a/.yarn/cache/@sentry-babel-plugin-component-annotate-npm-5.1.1-c6783d0521-8c7da826dc.zip b/.yarn/cache/@sentry-babel-plugin-component-annotate-npm-5.1.1-c6783d0521-8c7da826dc.zip new file mode 100644 index 000000000..ec73815b7 Binary files /dev/null and b/.yarn/cache/@sentry-babel-plugin-component-annotate-npm-5.1.1-c6783d0521-8c7da826dc.zip differ diff --git a/.yarn/cache/@sentry-browser-npm-10.43.0-f9aafbfc79-9b20398756.zip b/.yarn/cache/@sentry-browser-npm-10.43.0-f9aafbfc79-9b20398756.zip new file mode 100644 index 000000000..3d11a73af Binary files /dev/null and b/.yarn/cache/@sentry-browser-npm-10.43.0-f9aafbfc79-9b20398756.zip differ diff --git a/.yarn/cache/@sentry-browser-npm-9.14.0-0e2464dc6d-16a4ba1911.zip b/.yarn/cache/@sentry-browser-npm-9.14.0-0e2464dc6d-16a4ba1911.zip deleted file mode 100644 index f09390379..000000000 Binary files a/.yarn/cache/@sentry-browser-npm-9.14.0-0e2464dc6d-16a4ba1911.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-bundler-plugin-core-npm-5.1.1-6cd64e5cf9-6d08a02ed4.zip b/.yarn/cache/@sentry-bundler-plugin-core-npm-5.1.1-6cd64e5cf9-6d08a02ed4.zip new file mode 100644 index 000000000..7cb194da1 Binary files /dev/null and b/.yarn/cache/@sentry-bundler-plugin-core-npm-5.1.1-6cd64e5cf9-6d08a02ed4.zip differ diff --git a/.yarn/cache/@sentry-cli-darwin-npm-2.58.5-1f667e3b9d-10.zip b/.yarn/cache/@sentry-cli-darwin-npm-2.58.5-1f667e3b9d-10.zip new file mode 100644 index 000000000..83e80e3c7 Binary files /dev/null and b/.yarn/cache/@sentry-cli-darwin-npm-2.58.5-1f667e3b9d-10.zip differ diff --git a/.yarn/cache/@sentry-cli-npm-2.58.5-8604062248-347fb8236b.zip b/.yarn/cache/@sentry-cli-npm-2.58.5-8604062248-347fb8236b.zip new file mode 100644 index 000000000..0a35c87f4 Binary files /dev/null and b/.yarn/cache/@sentry-cli-npm-2.58.5-8604062248-347fb8236b.zip differ diff --git a/.yarn/cache/@sentry-core-npm-10.43.0-0e35cf6983-89ff388771.zip b/.yarn/cache/@sentry-core-npm-10.43.0-0e35cf6983-89ff388771.zip new file mode 100644 index 000000000..3257aef24 Binary files /dev/null and b/.yarn/cache/@sentry-core-npm-10.43.0-0e35cf6983-89ff388771.zip differ diff --git a/.yarn/cache/@sentry-core-npm-9.14.0-6df7ce92b3-ade3f5248a.zip b/.yarn/cache/@sentry-core-npm-9.14.0-6df7ce92b3-ade3f5248a.zip deleted file mode 100644 index 682510d92..000000000 Binary files a/.yarn/cache/@sentry-core-npm-9.14.0-6df7ce92b3-ade3f5248a.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-internal-browser-utils-npm-10.43.0-5faac97225-23de31117b.zip b/.yarn/cache/@sentry-internal-browser-utils-npm-10.43.0-5faac97225-23de31117b.zip new file mode 100644 index 000000000..2fe629e60 Binary files /dev/null and b/.yarn/cache/@sentry-internal-browser-utils-npm-10.43.0-5faac97225-23de31117b.zip differ diff --git a/.yarn/cache/@sentry-internal-browser-utils-npm-9.14.0-cb7c00e1d8-d3c9e336c2.zip b/.yarn/cache/@sentry-internal-browser-utils-npm-9.14.0-cb7c00e1d8-d3c9e336c2.zip deleted file mode 100644 index c50f72d85..000000000 Binary files a/.yarn/cache/@sentry-internal-browser-utils-npm-9.14.0-cb7c00e1d8-d3c9e336c2.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-internal-feedback-npm-10.43.0-03476b28f7-394fd58d0a.zip b/.yarn/cache/@sentry-internal-feedback-npm-10.43.0-03476b28f7-394fd58d0a.zip new file mode 100644 index 000000000..b0c44db99 Binary files /dev/null and b/.yarn/cache/@sentry-internal-feedback-npm-10.43.0-03476b28f7-394fd58d0a.zip differ diff --git a/.yarn/cache/@sentry-internal-feedback-npm-9.14.0-2cf0ca7c6d-aed56b19d7.zip b/.yarn/cache/@sentry-internal-feedback-npm-9.14.0-2cf0ca7c6d-aed56b19d7.zip deleted file mode 100644 index e23f84cc5..000000000 Binary files a/.yarn/cache/@sentry-internal-feedback-npm-9.14.0-2cf0ca7c6d-aed56b19d7.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-internal-replay-canvas-npm-10.43.0-2562a3d960-f0d3df594a.zip b/.yarn/cache/@sentry-internal-replay-canvas-npm-10.43.0-2562a3d960-f0d3df594a.zip new file mode 100644 index 000000000..900e0fb3e Binary files /dev/null and b/.yarn/cache/@sentry-internal-replay-canvas-npm-10.43.0-2562a3d960-f0d3df594a.zip differ diff --git a/.yarn/cache/@sentry-internal-replay-canvas-npm-9.14.0-2a39a7c942-48ebb8057f.zip b/.yarn/cache/@sentry-internal-replay-canvas-npm-9.14.0-2a39a7c942-48ebb8057f.zip deleted file mode 100644 index 0de0155ae..000000000 Binary files a/.yarn/cache/@sentry-internal-replay-canvas-npm-9.14.0-2a39a7c942-48ebb8057f.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-internal-replay-npm-10.43.0-82533134d0-dd56b9c358.zip b/.yarn/cache/@sentry-internal-replay-npm-10.43.0-82533134d0-dd56b9c358.zip new file mode 100644 index 000000000..e02c0a297 Binary files /dev/null and b/.yarn/cache/@sentry-internal-replay-npm-10.43.0-82533134d0-dd56b9c358.zip differ diff --git a/.yarn/cache/@sentry-internal-replay-npm-9.14.0-d6681f4f5f-86bd17b32e.zip b/.yarn/cache/@sentry-internal-replay-npm-9.14.0-d6681f4f5f-86bd17b32e.zip deleted file mode 100644 index 4404bbecf..000000000 Binary files a/.yarn/cache/@sentry-internal-replay-npm-9.14.0-d6681f4f5f-86bd17b32e.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-nextjs-npm-10.43.0-522ec5f88f-a22938081b.zip b/.yarn/cache/@sentry-nextjs-npm-10.43.0-522ec5f88f-a22938081b.zip new file mode 100644 index 000000000..ac3b29ac0 Binary files /dev/null and b/.yarn/cache/@sentry-nextjs-npm-10.43.0-522ec5f88f-a22938081b.zip differ diff --git a/.yarn/cache/@sentry-node-core-npm-10.43.0-e73484e3ce-a18c31a359.zip b/.yarn/cache/@sentry-node-core-npm-10.43.0-e73484e3ce-a18c31a359.zip new file mode 100644 index 000000000..d38916cbb Binary files /dev/null and b/.yarn/cache/@sentry-node-core-npm-10.43.0-e73484e3ce-a18c31a359.zip differ diff --git a/.yarn/cache/@sentry-node-npm-10.43.0-4c7fcaed43-a0976484c2.zip b/.yarn/cache/@sentry-node-npm-10.43.0-4c7fcaed43-a0976484c2.zip new file mode 100644 index 000000000..8c849687f Binary files /dev/null and b/.yarn/cache/@sentry-node-npm-10.43.0-4c7fcaed43-a0976484c2.zip differ diff --git a/.yarn/cache/@sentry-opentelemetry-npm-10.43.0-5c9849783e-e108fbd4fe.zip b/.yarn/cache/@sentry-opentelemetry-npm-10.43.0-5c9849783e-e108fbd4fe.zip new file mode 100644 index 000000000..450a605dd Binary files /dev/null and b/.yarn/cache/@sentry-opentelemetry-npm-10.43.0-5c9849783e-e108fbd4fe.zip differ diff --git a/.yarn/cache/@sentry-react-npm-10.43.0-e7ea4becc5-098019ae7a.zip b/.yarn/cache/@sentry-react-npm-10.43.0-e7ea4becc5-098019ae7a.zip new file mode 100644 index 000000000..c4406738f Binary files /dev/null and b/.yarn/cache/@sentry-react-npm-10.43.0-e7ea4becc5-098019ae7a.zip differ diff --git a/.yarn/cache/@sentry-vercel-edge-npm-10.43.0-2730475241-b89ef317a3.zip b/.yarn/cache/@sentry-vercel-edge-npm-10.43.0-2730475241-b89ef317a3.zip new file mode 100644 index 000000000..ddde6e593 Binary files /dev/null and b/.yarn/cache/@sentry-vercel-edge-npm-10.43.0-2730475241-b89ef317a3.zip differ diff --git a/.yarn/cache/@sentry-webpack-plugin-npm-5.1.1-8faceba0cb-f1b3ccad6a.zip b/.yarn/cache/@sentry-webpack-plugin-npm-5.1.1-8faceba0cb-f1b3ccad6a.zip new file mode 100644 index 000000000..ba3b32634 Binary files /dev/null and b/.yarn/cache/@sentry-webpack-plugin-npm-5.1.1-8faceba0cb-f1b3ccad6a.zip differ diff --git a/.yarn/cache/@stomp-stompjs-npm-7.0.0-c66e011389-f60db460e0.zip b/.yarn/cache/@stomp-stompjs-npm-7.0.0-c66e011389-f60db460e0.zip deleted file mode 100644 index ca7f8d1a0..000000000 Binary files a/.yarn/cache/@stomp-stompjs-npm-7.0.0-c66e011389-f60db460e0.zip and /dev/null differ diff --git a/.yarn/cache/@tanstack-query-core-npm-5.28.6-6bd1f84a2d-e9ae8d80a8.zip b/.yarn/cache/@tanstack-query-core-npm-5.28.6-6bd1f84a2d-e9ae8d80a8.zip deleted file mode 100644 index ab2efd2ba..000000000 Binary files a/.yarn/cache/@tanstack-query-core-npm-5.28.6-6bd1f84a2d-e9ae8d80a8.zip and /dev/null differ diff --git a/.yarn/cache/@tanstack-query-core-npm-5.90.20-fe193b58bc-25e38f4382.zip b/.yarn/cache/@tanstack-query-core-npm-5.90.20-fe193b58bc-25e38f4382.zip new file mode 100644 index 000000000..71d5708a6 Binary files /dev/null and b/.yarn/cache/@tanstack-query-core-npm-5.90.20-fe193b58bc-25e38f4382.zip differ diff --git a/.yarn/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip b/.yarn/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip deleted file mode 100644 index 84118e0e8..000000000 Binary files a/.yarn/cache/@tanstack-react-query-npm-5.28.6-ea0a1ece1c-f7706485f3.zip and /dev/null differ diff --git a/.yarn/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip b/.yarn/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip new file mode 100644 index 000000000..b1b63cefe Binary files /dev/null and b/.yarn/cache/@tanstack-react-query-npm-5.90.21-4400cf02c2-5bb4b6be7a.zip differ diff --git a/.yarn/cache/@types-connect-npm-3.4.38-a8a4c38337-7eb1bc5342.zip b/.yarn/cache/@types-connect-npm-3.4.38-a8a4c38337-7eb1bc5342.zip new file mode 100644 index 000000000..f943dcaa9 Binary files /dev/null and b/.yarn/cache/@types-connect-npm-3.4.38-a8a4c38337-7eb1bc5342.zip differ diff --git a/.yarn/cache/@types-mysql-npm-2.15.27-76b107f5b9-a8c7435010.zip b/.yarn/cache/@types-mysql-npm-2.15.27-76b107f5b9-a8c7435010.zip new file mode 100644 index 000000000..3b37996e2 Binary files /dev/null and b/.yarn/cache/@types-mysql-npm-2.15.27-76b107f5b9-a8c7435010.zip differ diff --git a/.yarn/cache/@types-pg-npm-8.15.6-44108e12b9-4bc1bb274e.zip b/.yarn/cache/@types-pg-npm-8.15.6-44108e12b9-4bc1bb274e.zip new file mode 100644 index 000000000..165dae5ec Binary files /dev/null and b/.yarn/cache/@types-pg-npm-8.15.6-44108e12b9-4bc1bb274e.zip differ diff --git a/.yarn/cache/@types-pg-npm-8.18.0-d62c4e6195-fdfcaff97f.zip b/.yarn/cache/@types-pg-npm-8.18.0-d62c4e6195-fdfcaff97f.zip new file mode 100644 index 000000000..8a64a20df Binary files /dev/null and b/.yarn/cache/@types-pg-npm-8.18.0-d62c4e6195-fdfcaff97f.zip differ diff --git a/.yarn/cache/@types-pg-pool-npm-2.0.7-908dda8b54-b2ac51f1e9.zip b/.yarn/cache/@types-pg-pool-npm-2.0.7-908dda8b54-b2ac51f1e9.zip new file mode 100644 index 000000000..13eb76501 Binary files /dev/null and b/.yarn/cache/@types-pg-pool-npm-2.0.7-908dda8b54-b2ac51f1e9.zip differ diff --git a/.yarn/cache/@types-tedious-npm-4.0.14-11edc4a73d-c8f6480cf6.zip b/.yarn/cache/@types-tedious-npm-4.0.14-11edc4a73d-c8f6480cf6.zip new file mode 100644 index 000000000..16f25e800 Binary files /dev/null and b/.yarn/cache/@types-tedious-npm-4.0.14-11edc4a73d-c8f6480cf6.zip differ diff --git a/.yarn/cache/acorn-import-attributes-npm-1.9.5-d1e666eb35-8bfbfbb6e2.zip b/.yarn/cache/acorn-import-attributes-npm-1.9.5-d1e666eb35-8bfbfbb6e2.zip new file mode 100644 index 000000000..9a210b13d Binary files /dev/null and b/.yarn/cache/acorn-import-attributes-npm-1.9.5-d1e666eb35-8bfbfbb6e2.zip differ diff --git a/.yarn/cache/balanced-match-npm-4.0.4-fd666b3c7f-fb07bb66a0.zip b/.yarn/cache/balanced-match-npm-4.0.4-fd666b3c7f-fb07bb66a0.zip new file mode 100644 index 000000000..35e2154ef Binary files /dev/null and b/.yarn/cache/balanced-match-npm-4.0.4-fd666b3c7f-fb07bb66a0.zip differ diff --git a/.yarn/cache/brace-expansion-npm-5.0.4-acb9332524-cfd57e20d8.zip b/.yarn/cache/brace-expansion-npm-5.0.4-acb9332524-cfd57e20d8.zip new file mode 100644 index 000000000..57c99436e Binary files /dev/null and b/.yarn/cache/brace-expansion-npm-5.0.4-acb9332524-cfd57e20d8.zip differ diff --git a/.yarn/cache/cjs-module-lexer-npm-2.2.0-a4ea3b2e41-fc8eb5c191.zip b/.yarn/cache/cjs-module-lexer-npm-2.2.0-a4ea3b2e41-fc8eb5c191.zip new file mode 100644 index 000000000..b6790532d Binary files /dev/null and b/.yarn/cache/cjs-module-lexer-npm-2.2.0-a4ea3b2e41-fc8eb5c191.zip differ diff --git a/.yarn/cache/commondir-npm-1.0.1-291b790340-4620bc4936.zip b/.yarn/cache/commondir-npm-1.0.1-291b790340-4620bc4936.zip new file mode 100644 index 000000000..99574e49d Binary files /dev/null and b/.yarn/cache/commondir-npm-1.0.1-291b790340-4620bc4936.zip differ diff --git a/.yarn/cache/debug-npm-4.4.3-0105c6123a-9ada3434ea.zip b/.yarn/cache/debug-npm-4.4.3-0105c6123a-9ada3434ea.zip new file mode 100644 index 000000000..b8f325d47 Binary files /dev/null and b/.yarn/cache/debug-npm-4.4.3-0105c6123a-9ada3434ea.zip differ diff --git a/.yarn/cache/dotenv-npm-16.6.1-01334288ea-1d18971443.zip b/.yarn/cache/dotenv-npm-16.6.1-01334288ea-1d18971443.zip new file mode 100644 index 000000000..7820c73e7 Binary files /dev/null and b/.yarn/cache/dotenv-npm-16.6.1-01334288ea-1d18971443.zip differ diff --git a/.yarn/cache/dotenv-npm-17.2.3-2f9ab93ea1-f8b78626eb.zip b/.yarn/cache/dotenv-npm-17.2.3-2f9ab93ea1-f8b78626eb.zip new file mode 100644 index 000000000..ae3318596 Binary files /dev/null and b/.yarn/cache/dotenv-npm-17.2.3-2f9ab93ea1-f8b78626eb.zip differ diff --git a/.yarn/cache/esbuild-npm-0.27.2-7789e62c6d-7f1229328b.zip b/.yarn/cache/esbuild-npm-0.27.2-7789e62c6d-7f1229328b.zip new file mode 100644 index 000000000..18b308c08 Binary files /dev/null and b/.yarn/cache/esbuild-npm-0.27.2-7789e62c6d-7f1229328b.zip differ diff --git a/.yarn/cache/estree-walker-npm-2.0.2-dfab42f65c-b02109c5d4.zip b/.yarn/cache/estree-walker-npm-2.0.2-dfab42f65c-b02109c5d4.zip new file mode 100644 index 000000000..08560cf02 Binary files /dev/null and b/.yarn/cache/estree-walker-npm-2.0.2-dfab42f65c-b02109c5d4.zip differ diff --git a/.yarn/cache/forwarded-parse-npm-2.1.2-8cf38fd641-fca4df8898.zip b/.yarn/cache/forwarded-parse-npm-2.1.2-8cf38fd641-fca4df8898.zip new file mode 100644 index 000000000..d54ee3620 Binary files /dev/null and b/.yarn/cache/forwarded-parse-npm-2.1.2-8cf38fd641-fca4df8898.zip differ diff --git a/.yarn/cache/get-tsconfig-npm-4.13.1-10a4d287d1-a21b037241.zip b/.yarn/cache/get-tsconfig-npm-4.13.1-10a4d287d1-a21b037241.zip new file mode 100644 index 000000000..60edc6e68 Binary files /dev/null and b/.yarn/cache/get-tsconfig-npm-4.13.1-10a4d287d1-a21b037241.zip differ diff --git a/.yarn/cache/glob-npm-13.0.6-864eb0cece-201ad69e5f.zip b/.yarn/cache/glob-npm-13.0.6-864eb0cece-201ad69e5f.zip new file mode 100644 index 000000000..e0ddf4765 Binary files /dev/null and b/.yarn/cache/glob-npm-13.0.6-864eb0cece-201ad69e5f.zip differ diff --git a/.yarn/cache/idb-npm-8.0.3-e9b0a844f6-e2beccb0be.zip b/.yarn/cache/idb-npm-8.0.3-e9b0a844f6-e2beccb0be.zip new file mode 100644 index 000000000..b609f9a84 Binary files /dev/null and b/.yarn/cache/idb-npm-8.0.3-e9b0a844f6-e2beccb0be.zip differ diff --git a/.yarn/cache/import-in-the-middle-npm-2.0.6-233e5f3db3-8be80d7f2d.zip b/.yarn/cache/import-in-the-middle-npm-2.0.6-233e5f3db3-8be80d7f2d.zip new file mode 100644 index 000000000..6ac5d7f8d Binary files /dev/null and b/.yarn/cache/import-in-the-middle-npm-2.0.6-233e5f3db3-8be80d7f2d.zip differ diff --git a/.yarn/cache/is-reference-npm-1.2.1-87ca1743c8-e7b48149f8.zip b/.yarn/cache/is-reference-npm-1.2.1-87ca1743c8-e7b48149f8.zip new file mode 100644 index 000000000..bae17ee68 Binary files /dev/null and b/.yarn/cache/is-reference-npm-1.2.1-87ca1743c8-e7b48149f8.zip differ diff --git a/.yarn/cache/lru-cache-npm-11.2.6-acb7d4323e-91222bbd59.zip b/.yarn/cache/lru-cache-npm-11.2.6-acb7d4323e-91222bbd59.zip new file mode 100644 index 000000000..6fc6b60c9 Binary files /dev/null and b/.yarn/cache/lru-cache-npm-11.2.6-acb7d4323e-91222bbd59.zip differ diff --git a/.yarn/cache/magic-string-npm-0.30.21-9a226cb21e-57d5691f41.zip b/.yarn/cache/magic-string-npm-0.30.21-9a226cb21e-57d5691f41.zip new file mode 100644 index 000000000..53485dc72 Binary files /dev/null and b/.yarn/cache/magic-string-npm-0.30.21-9a226cb21e-57d5691f41.zip differ diff --git a/.yarn/cache/minimatch-npm-10.2.4-11f0605299-aea4874e52.zip b/.yarn/cache/minimatch-npm-10.2.4-11f0605299-aea4874e52.zip new file mode 100644 index 000000000..d014644bc Binary files /dev/null and b/.yarn/cache/minimatch-npm-10.2.4-11f0605299-aea4874e52.zip differ diff --git a/.yarn/cache/minipass-npm-7.1.3-b73a16498d-175e4d5e20.zip b/.yarn/cache/minipass-npm-7.1.3-b73a16498d-175e4d5e20.zip new file mode 100644 index 000000000..a9d6f9742 Binary files /dev/null and b/.yarn/cache/minipass-npm-7.1.3-b73a16498d-175e4d5e20.zip differ diff --git a/.yarn/cache/module-details-from-path-npm-1.0.4-c3d0545459-2ebfada535.zip b/.yarn/cache/module-details-from-path-npm-1.0.4-c3d0545459-2ebfada535.zip new file mode 100644 index 000000000..9368724b2 Binary files /dev/null and b/.yarn/cache/module-details-from-path-npm-1.0.4-c3d0545459-2ebfada535.zip differ diff --git a/.yarn/cache/path-scurry-npm-2.0.2-f10aa6a77e-2b4257422b.zip b/.yarn/cache/path-scurry-npm-2.0.2-f10aa6a77e-2b4257422b.zip new file mode 100644 index 000000000..9d8e4e22d Binary files /dev/null and b/.yarn/cache/path-scurry-npm-2.0.2-f10aa6a77e-2b4257422b.zip differ diff --git a/.yarn/cache/pg-int8-npm-1.0.1-5cd67f3e22-a1e3a05a69.zip b/.yarn/cache/pg-int8-npm-1.0.1-5cd67f3e22-a1e3a05a69.zip new file mode 100644 index 000000000..600179612 Binary files /dev/null and b/.yarn/cache/pg-int8-npm-1.0.1-5cd67f3e22-a1e3a05a69.zip differ diff --git a/.yarn/cache/pg-protocol-npm-1.13.0-d380339def-302cd3920d.zip b/.yarn/cache/pg-protocol-npm-1.13.0-d380339def-302cd3920d.zip new file mode 100644 index 000000000..eba0abc32 Binary files /dev/null and b/.yarn/cache/pg-protocol-npm-1.13.0-d380339def-302cd3920d.zip differ diff --git a/.yarn/cache/pg-types-npm-2.2.0-a3360226c4-87a84d4baa.zip b/.yarn/cache/pg-types-npm-2.2.0-a3360226c4-87a84d4baa.zip new file mode 100644 index 000000000..1eea97665 Binary files /dev/null and b/.yarn/cache/pg-types-npm-2.2.0-a3360226c4-87a84d4baa.zip differ diff --git a/.yarn/cache/postgres-array-npm-2.0.0-4f49dc1389-aff99e7971.zip b/.yarn/cache/postgres-array-npm-2.0.0-4f49dc1389-aff99e7971.zip new file mode 100644 index 000000000..fb9daa269 Binary files /dev/null and b/.yarn/cache/postgres-array-npm-2.0.0-4f49dc1389-aff99e7971.zip differ diff --git a/.yarn/cache/postgres-bytea-npm-1.0.1-33f7758ac9-fc5fa49f59.zip b/.yarn/cache/postgres-bytea-npm-1.0.1-33f7758ac9-fc5fa49f59.zip new file mode 100644 index 000000000..105ac9966 Binary files /dev/null and b/.yarn/cache/postgres-bytea-npm-1.0.1-33f7758ac9-fc5fa49f59.zip differ diff --git a/.yarn/cache/postgres-date-npm-1.0.7-aadfe5531e-571ef45bec.zip b/.yarn/cache/postgres-date-npm-1.0.7-aadfe5531e-571ef45bec.zip new file mode 100644 index 000000000..05bedfde1 Binary files /dev/null and b/.yarn/cache/postgres-date-npm-1.0.7-aadfe5531e-571ef45bec.zip differ diff --git a/.yarn/cache/postgres-interval-npm-1.2.0-ca6414744d-746b71f938.zip b/.yarn/cache/postgres-interval-npm-1.2.0-ca6414744d-746b71f938.zip new file mode 100644 index 000000000..ed2ddbe9d Binary files /dev/null and b/.yarn/cache/postgres-interval-npm-1.2.0-ca6414744d-746b71f938.zip differ diff --git a/.yarn/cache/require-in-the-middle-npm-8.0.1-096af9b870-4ce98c6814.zip b/.yarn/cache/require-in-the-middle-npm-8.0.1-096af9b870-4ce98c6814.zip new file mode 100644 index 000000000..2792fceca Binary files /dev/null and b/.yarn/cache/require-in-the-middle-npm-8.0.1-096af9b870-4ce98c6814.zip differ diff --git a/.yarn/cache/rollup-npm-4.59.0-3b10f603ec-728237932a.zip b/.yarn/cache/rollup-npm-4.59.0-3b10f603ec-728237932a.zip new file mode 100644 index 000000000..62d8db942 Binary files /dev/null and b/.yarn/cache/rollup-npm-4.59.0-3b10f603ec-728237932a.zip differ diff --git a/.yarn/cache/stacktrace-parser-npm-0.1.11-2d5238cd3f-1120cf7166.zip b/.yarn/cache/stacktrace-parser-npm-0.1.11-2d5238cd3f-1120cf7166.zip new file mode 100644 index 000000000..0b7181840 Binary files /dev/null and b/.yarn/cache/stacktrace-parser-npm-0.1.11-2d5238cd3f-1120cf7166.zip differ diff --git a/.yarn/cache/tsx-npm-4.21.0-3bc9626d81-7afedeff85.zip b/.yarn/cache/tsx-npm-4.21.0-3bc9626d81-7afedeff85.zip new file mode 100644 index 000000000..27615df9a Binary files /dev/null and b/.yarn/cache/tsx-npm-4.21.0-3bc9626d81-7afedeff85.zip differ diff --git a/.yarn/cache/type-fest-npm-0.7.1-7b37912923-0699b6011b.zip b/.yarn/cache/type-fest-npm-0.7.1-7b37912923-0699b6011b.zip new file mode 100644 index 000000000..0e056acb7 Binary files /dev/null and b/.yarn/cache/type-fest-npm-0.7.1-7b37912923-0699b6011b.zip differ diff --git a/.yarn/cache/uuid-npm-9.0.1-39a8442bc6-9d0b6adb72.zip b/.yarn/cache/uuid-npm-9.0.1-39a8442bc6-9d0b6adb72.zip new file mode 100644 index 000000000..8fd27d39f Binary files /dev/null and b/.yarn/cache/uuid-npm-9.0.1-39a8442bc6-9d0b6adb72.zip differ diff --git a/.yarn/cache/xtend-npm-4.0.2-7f2375736e-ac5dfa738b.zip b/.yarn/cache/xtend-npm-4.0.2-7f2375736e-ac5dfa738b.zip new file mode 100644 index 000000000..1090c6863 Binary files /dev/null and b/.yarn/cache/xtend-npm-4.0.2-7f2375736e-ac5dfa738b.zip differ diff --git a/.yarnrc.yml b/.yarnrc.yml index 4800a0554..766a2564e 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -3,3 +3,9 @@ compressionLevel: mixed enableGlobalCache: false yarnPath: .yarn/releases/yarn-4.10.3.cjs + +packageExtensions: + "@sentry/nextjs@*": + dependencies: + "@opentelemetry/core": "*" + "@opentelemetry/sdk-trace-base": "*" diff --git a/eslint.config.mjs b/eslint.config.mjs index 8c3462ace..37cdbf905 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ export default [ '**/dist/**', '**/.yarn/**', 'node_modules/**', + 'scripts/**', '**/*.d.ts', '**/.pnp.*', 'prettier.config.js', diff --git a/next.config.mjs b/next.config.mjs index 5b40d81fb..86a3a9488 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,3 +1,4 @@ +import { withSentryConfig } from '@sentry/nextjs'; /** @type {import('next').NextConfig} */ const nextConfig = { webpack(config) { @@ -58,4 +59,12 @@ const nextConfig = { }, }; -export default nextConfig; +export default withSentryConfig(nextConfig, { + org: process.env.SENTRY_ORG || 'bcsd', + project: process.env.SENTRY_PROJECT || 'koin-prod', + release: { name: process.env.NEXT_PUBLIC_SENTRY_RELEASE }, + silent: !process.env.CI, + widenClientFileUpload: false, + tunnelRoute: '/monitoring', + disableLogger: true, +}); diff --git a/package.json b/package.json index 3e5f0ef5c..2de139b93 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,8 @@ "@bcsdlab/koin": "^0.0.15", "@bcsdlab/utils": "^0.0.15", "@next/third-parties": "latest", - "@sentry/browser": "^9.14.0", - "@stomp/stompjs": "^7.0.0", - "@tanstack/react-query": "^5.28.6", + "@tanstack/react-query": "^5.90.21", + "@sentry/nextjs": "^10", "axios": "^0.27.2", "dayjs": "^1.11.12", "embla-carousel-autoplay": "^8.0.4", @@ -17,6 +16,7 @@ "framer-motion": "^12.4.4", "html-react-parser": "^5.1.10", "html2canvas": "^1.4.1", + "idb": "^8.0.3", "lottie-react": "^2.4.1", "next": "15.5.10", "react": "19.2.4", @@ -39,7 +39,8 @@ "sourcemap:clean": "find ./build -name '*.map' -type f -delete", "lint": "yarn lint:eslint && yarn lint:stylelint", "lint:eslint": "eslint src/", - "lint:stylelint": "stylelint \"src/**/*.scss\" --config .stylelint.json" + "lint:stylelint": "stylelint \"src/**/*.scss\" --config .stylelint.json", + "log": "tsx scripts/notion/fetch-logging-spec.ts" }, "browserslist": { "production": [ @@ -55,6 +56,7 @@ }, "devDependencies": { "@next/eslint-plugin-next": "^16.0.0", + "@notionhq/client": "^5.9.0", "@sentry/cli": "^2.45.0", "@svgr/webpack": "^8.1.0", "@testing-library/jest-dom": "^5.14.1", @@ -68,6 +70,7 @@ "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", "@types/react-window": "^1.8.5", + "dotenv": "^17.2.3", "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", @@ -85,6 +88,7 @@ "stylelint-config-standard": "^26.0.0", "stylelint-config-standard-scss": "^4.0.0", "stylelint-selector-bem-pattern": "^2.1.1", + "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.2" }, diff --git a/scripts/notion/fetch-logging-spec.ts b/scripts/notion/fetch-logging-spec.ts new file mode 100644 index 000000000..305058600 --- /dev/null +++ b/scripts/notion/fetch-logging-spec.ts @@ -0,0 +1,91 @@ +/** + * Notion 로깅 스펙 데이터베이스를 조회하여 TypeScript 로깅 훅을 자동 생성하는 CLI 스크립트. + * 사용법: yarn tsx scripts/notion/fetch-logging-spec.ts + * 선행조건: NOTION_TOKEN 환경 변수 설정 (.env) + * + * Output: + * - analytics.events.json (프로젝트 루트) — 이벤트 스펙 JSON + * - src/generated/analytics/useLogger.ts — 생성된 로거 훅 + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import readline from 'node:readline'; + +import { DATABASES } from './lib/types.js'; +import type { DatabaseConfig } from './lib/types.js'; +import { formatAsUUID, fetchLoggingEvents } from './lib/notion-fetcher.js'; +import { generateLoggerHookCode } from './lib/hook-generator.js'; + +// ─── CLI Helpers ───────────────────────────────────────── + +/** readline을 사용하여 사용자와 질문 및 응답 */ +function promptUser(question: string): Promise { + return new Promise((resolve) => { + const readlineInterface = readline.createInterface({ input: process.stdin, output: process.stdout }); + readlineInterface.question(question, (answer) => { + readlineInterface.close(); + resolve(answer.trim()); + }); + }); +} + +/** 데이터베이스 목록을 표시 및 사용자가 선택 */ +async function promptDatabaseSelection(): Promise { + console.log('\n--- DB를 선택하세요 ---'); + DATABASES.forEach((database, index) => console.log(`${index + 1}) ${database.name}`)); + const answer = await promptUser('번호 입력: '); + const selectedIndex = Number(answer) - 1; + if (selectedIndex < 0 || selectedIndex >= DATABASES.length || Number.isNaN(selectedIndex)) { + throw new Error(`잘못된 번호입니다: ${answer}`); + } + return DATABASES[selectedIndex]; +} + +// ─── Main ──────────────────────────────────────────────── + +async function main() { + if (!process.env.NOTION_TOKEN) throw new Error('Missing NOTION_TOKEN'); + + const database = await promptDatabaseSelection(); + const databaseId = formatAsUUID(database.id); + const hookName = await promptUser('생성할 훅 이름을 입력하세요 (예: Common): '); + + if (!hookName) throw new Error('훅 이름이 비어 있습니다.'); + + console.log(`\n선택된 DB: ${database.name} (${databaseId})`); + console.log(`Team: ${database.team}`); + console.log(`Hook: use${hookName}Logger\n`); + + // 데이터 조회 및 파싱 + const { events, errors } = await fetchLoggingEvents(databaseId); + + // JSON 작성 + const jsonOut = { + generatedAt: new Date().toISOString(), + databaseId, + team: database.team, + total: events.length, + events, + errors, + }; + + const jsonPath = path.join(process.cwd(), 'analytics.events.json'); + fs.writeFileSync(jsonPath, JSON.stringify(jsonOut, null, 2), 'utf8'); + console.log(`JSON -> ${jsonPath}`); + + // 로깅 훅 생성 + const hookCode = generateLoggerHookCode(events, database.team, hookName); + const hookDir = path.join(process.cwd(), 'src', 'generated', 'analytics'); + const hookPath = path.join(hookDir, `use${hookName}Logger.ts`); + fs.mkdirSync(hookDir, { recursive: true }); + fs.writeFileSync(hookPath, hookCode, 'utf8'); + console.log(`Hook -> ${hookPath}`); + + console.log(`\ntotal=${events.length} (deduplicated), errors=${errors.length}`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/notion/lib/hook-generator.ts b/scripts/notion/lib/hook-generator.ts new file mode 100644 index 000000000..8ddd6b1fa --- /dev/null +++ b/scripts/notion/lib/hook-generator.ts @@ -0,0 +1,112 @@ +/** + * EventSpecification 배열을 받아 TypeScript 로거 훅 코드를 생성하는 모듈. + * 생성되는 훅은 `useLogger`를 래핑하여 각 이벤트에 대한 타입 안전한 로깅 함수를 제공합니다. + */ + +import type { EventSpecification } from './types.js'; +import { EXTRA_PARAM_TYPE_MAP } from './types.js'; +import { snakeToCamelCase, snakeToPascalCase } from './notion-fetcher.js'; + +/** extraParams 배열로부터 TypeScript 인라인 객체 타입 문자열을 생성 */ +function buildExtraParamsType(extraParams: string[]): string { + const fields = extraParams.map((key) => `${key}?: ${EXTRA_PARAM_TYPE_MAP[key] ?? 'string'}`).join('; '); + return `{ ${fields} }`; +} + +/** + * 이벤트 스펙 목록으로부터 TypeScript 로거 훅 코드 문자열을 생성 + * + * 생성 구조: + * 1. EVENTS 상수 객체 (각 이벤트의 label, category, value 정의) + * 2. dynamic value를 가진 이벤트의 타입 정의 + * 3. useLogger를 래핑하는 커스텀 훅 함수 + * + * @param events - 이벤트 스펙 목록 + * @param team - 팀 식별자 (예: 'CAMPUS') + * @param hookName - 생성할 훅의 이름 (예: 'Article' → `useArticleLogger`) + * @returns 완성된 TypeScript 코드 문자열 + */ +export function generateLoggerHookCode(events: EventSpecification[], team: string, hookName: string): string { + const lines: string[] = []; + + lines.push(`// Auto-generated by fetch-logging-spec.ts — ${new Date().toISOString()}`); + lines.push("import useLogger from 'utils/hooks/analytics/useLogger';"); + lines.push(''); + lines.push(`const TEAM = '${team}';`); + lines.push(''); + + // EVENTS 상수 + lines.push('const EVENTS = {'); + for (const event of events) { + const key = snakeToCamelCase(event.event_label); + const categoryPart = event.event_category !== 'click' ? `, event_category: '${event.event_category}'` : ''; + if (event.value_type === 'fixed') { + lines.push(` ${key}: { event_label: '${event.event_label}'${categoryPart}, value: '${event.value}' },`); + } else if (event.values.length) { + const valuesStr = event.values.map((value) => `'${value}'`).join(', '); + lines.push(` ${key}: { event_label: '${event.event_label}'${categoryPart}, values: [${valuesStr}] },`); + } else { + lines.push(` ${key}: { event_label: '${event.event_label}'${categoryPart} },`); + } + } + lines.push('} as const;'); + lines.push(''); + + // 타입 정의 + const dynamicEvents = events.filter((event) => event.value_type === 'dynamic'); + for (const event of dynamicEvents) { + const typeName = `${snakeToPascalCase(event.event_label)}Value`; + const key = snakeToCamelCase(event.event_label); + if (event.values.length) { + lines.push(`export type ${typeName} = (typeof EVENTS.${key}.values)[number];`); + } else { + lines.push(`export type ${typeName} = string;`); + } + } + if (dynamicEvents.length) lines.push(''); + + // 훅 + lines.push(`export const use${hookName}Logger = () => {`); + lines.push(' const logger = useLogger();'); + lines.push(''); + events.forEach((event, index) => { + const key = snakeToCamelCase(event.event_label); + const fnName = `log${snakeToPascalCase(event.event_label)}`; + const hasExtra = event.extraParams.length > 0; + if (index > 0) lines.push(''); + lines.push(` /** ${event.titles.join(' / ')} */`); + + if (event.value_type === 'fixed' && !hasExtra) { + lines.push(` const ${fnName} = () => logger.actionEventClick({ team: TEAM, ...EVENTS.${key} });`); + } else if (event.value_type === 'fixed' && hasExtra) { + const paramsType = buildExtraParamsType(event.extraParams); + lines.push( + ` const ${fnName} = (params: ${paramsType}) => logger.actionEventClick({ team: TEAM, ...EVENTS.${key}, ...params });`, + ); + } else if (!hasExtra) { + const typeName = `${snakeToPascalCase(event.event_label)}Value`; + lines.push( + ` const ${fnName} = (value: ${typeName}) => logger.actionEventClick({ team: TEAM, ...EVENTS.${key}, value });`, + ); + } else { + const typeName = `${snakeToPascalCase(event.event_label)}Value`; + const paramsType = buildExtraParamsType(event.extraParams); + lines.push( + ` const ${fnName} = (value: ${typeName}, params: ${paramsType}) => logger.actionEventClick({ team: TEAM, ...EVENTS.${key}, value, ...params });`, + ); + } + }); + lines.push(''); + + // return + lines.push(' return {'); + for (const event of events) { + const fnName = `log${snakeToPascalCase(event.event_label)}`; + lines.push(` ${fnName},`); + } + lines.push(' };'); + lines.push('};'); + lines.push(''); + + return lines.join('\n'); +} diff --git a/scripts/notion/lib/notion-fetcher.ts b/scripts/notion/lib/notion-fetcher.ts new file mode 100644 index 000000000..62d410215 --- /dev/null +++ b/scripts/notion/lib/notion-fetcher.ts @@ -0,0 +1,403 @@ +/** + * Notion API를 사용하여 로깅 스펙 데이터베이스를 조회하고, + * 페이지 블록 텍스트를 파싱하여 이벤트 스펙을 추출하는 모듈. + */ + +import 'dotenv/config'; +import { Client } from '@notionhq/client'; + +import type { ParsedSpecification, RawEvent, EventSpecification } from './types.js'; +import { EXTRA_PARAM_TYPE_MAP } from './types.js'; + +const notionClient = new Client({ + auth: process.env.NOTION_TOKEN, + notionVersion: '2025-09-03', +}); + +// ─── Text Utilities ────────────────────────────────────── + +/** + * 하이픈 없는 hex 문자열을 UUID 형식으로 변환한다. + * @param hexString - 32자리 hex 문자열 (하이픈 포함 가능) + * @returns 8-4-4-4-12 형식의 UUID + */ +export function formatAsUUID(hexString: string): string { + const hex = hexString.replace(/-/g, ''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +/** + * snake_case 문자열을 camelCase로 변환한다. + * @param text - snake_case 문자열 + */ +export function snakeToCamelCase(text: string): string { + return text.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); +} + +/** + * snake_case 문자열을 PascalCase로 변환한다. + * @param text - snake_case 문자열 + */ +export function snakeToPascalCase(text: string): string { + const camel = snakeToCamelCase(text); + return camel.charAt(0).toUpperCase() + camel.slice(1); +} + +/** + * Notion rich_text 배열을 plain text로 변환 + * @param richTextArray - Notion API의 rich_text 배열 + */ +export function richTextToPlainText(richTextArray: any[] | undefined): string { + return (richTextArray ?? []).map((token) => token.plain_text ?? '').join(''); +} + +/** + * 페이지 프로퍼티에서 타이틀 텍스트를 추출 + * @param properties - Notion 페이지의 properties 객체 + */ +export function extractPageTitle(properties: any): string { + const name = properties?.['이름']; + if (name?.type === 'title') return richTextToPlainText(name.title); + for (const property of Object.values(properties ?? {})) { + if ((property as any)?.type === 'title') return richTextToPlainText((property as any).title); + } + return ''; +} + +/** + * 유니코드 스마트 따옴표를 일반 따옴표로 변환하고, 양끝 따옴표를 제거 + * @param text - 정규화할 문자열 + */ +export function normalizeQuotes(text: string): string { + return text + .replace(/[\u201c\u201d\u201e\u201f]/g, '"') + .replace(/[\u2018\u2019\u201a\u201b]/g, "'") + .replace(/^["'](.*)["']$/, '$1') + .trim(); +} + +/** + * value 문자열에서 enum 값 목록을 추출 + * 지원 패턴: + * - `{"v1", "v2", ...} optional text` + * - `'v1' or 'v2'` + * - `"v1", "v2"` + * @param raw - 원시 value 문자열 + * @returns 추출된 enum 값 배열 (2개 미만이면 빈 배열) + */ +export function extractEnumValues(raw: string): string[] { + const text = raw.replace(/[\u201c\u201d\u201e\u201f]/g, '"').replace(/[\u2018\u2019\u201a\u201b]/g, "'"); + + // 패턴 1: {v1, v2, ...} description + const braceMatch = text.match(/^\{(.+?)\}/); + if (braceMatch) { + return braceMatch[1] + .split(',') + .map((value) => value.trim().replace(/^["']+|["']+$/g, '')) + .filter(Boolean); + } + + // 패턴 2: 'v1' or 'v2' or 'v3' + if (/\bor\b/.test(text)) { + return text + .split(/\s+or\s+/) + .map((value) => value.trim().replace(/^["']+|["']+$/g, '')) + .filter(Boolean); + } + + // 패턴 3: "v1", "v2" (쉼표로 구분된 따옴표 문자열) + const quoted = [...text.matchAll(/["']([^"']+)["']/g)]; + if (quoted.length >= 2) { + return quoted.map((match) => match[1].trim()).filter(Boolean); + } + + return []; +} + +// ─── Spec Parsing ──────────────────────────────────────── + +/** + * key:value 형식의 텍스트를 ParsedSpecification으로 파싱 + * value 필드에서 enum 값을 자동 추출하여 value_type과 values를 결정 + * @param text - 줄바꿈으로 구분된 key:value 텍스트 + */ +export function parseLoggingSpec(text: string): ParsedSpecification { + const spec: ParsedSpecification = {}; + const lines = text + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + + for (const line of lines) { + const match = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/); + if (!match) continue; + + const key = match[1]; + const rawVal = match[2].trim(); + + if (key === 'value') { + spec[key] = rawVal; + } else { + spec[key] = normalizeQuotes(rawVal); + } + } + + const rawValue = typeof spec.value === 'string' ? spec.value : ''; + const enumValues = extractEnumValues(rawValue); + + if (enumValues.length >= 2) { + spec.value_type = 'dynamic'; + spec.values = enumValues; + spec.value = ''; + } else { + spec.value = normalizeQuotes(rawValue); + spec.values = []; + spec.value_type = spec.value ? 'fixed' : 'dynamic'; + } + + return spec; +} + +// ─── Notion API ────────────────────────────────────────── + +/** + * Notion database_id로부터 dataSources API에서 사용할 data_source_id를 추출 + * + * @param databaseId - UUID 형식의 Notion 데이터베이스 ID + * @returns data_source_id + */ +async function resolveDataSourceId(databaseId: string): Promise { + const database = await notionClient.databases.retrieve({ database_id: databaseId }); + return (database as any).data_sources[0].id; +} + +/** + * 데이터베이스의 모든 페이지를 조회 + * 상태가 '로깅 필요' 또는 '진행중'이고 platform에 'WEB'이 포함된 페이지만 필터링 + * + * @param databaseId - UUID 형식의 Notion 데이터베이스 ID + * @returns 조회된 페이지 목록 + */ +async function fetchAllPages(databaseId: string): Promise { + const dataSourceId = await resolveDataSourceId(databaseId); + const results: any[] = []; + let cursor: string | undefined; + + while (true) { + const response = await (notionClient as any).dataSources.query({ + data_source_id: dataSourceId, + start_cursor: cursor, + page_size: 100, + filter: { + and: [ + { + or: [ + { property: '상태', status: { equals: '로깅 필요' } }, + { property: '상태', status: { equals: '진행 중' } }, + ], + }, + { property: 'platform', multi_select: { contains: 'WEB' } }, + ], + }, + }); + + results.push(...response.results); + + if (!response.has_more) break; + cursor = response.next_cursor ?? undefined; + } + + return results; +} + +/** + * 블록의 모든 자식 블록을 재귀적으로 조회 + * @param blockId - 조회할 블록(또는 페이지)의 ID + * @returns 모든 하위 블록 목록 (중첩 포함) + */ +async function fetchAllBlocksRecursively(blockId: string): Promise { + const blocks: any[] = []; + let cursor: string | undefined; + + while (true) { + const response = await notionClient.blocks.children.list({ + block_id: blockId, + start_cursor: cursor, + page_size: 100, + }); + + blocks.push(...response.results); + + if (!response.has_more) break; + cursor = response.next_cursor ?? undefined; + } + + const expanded: any[] = []; + for (const block of blocks) { + expanded.push(block); + if (block.has_children) { + const children = await fetchAllBlocksRecursively(block.id); + expanded.push(...children); + } + } + return expanded; +} + +/** 블록에서 텍스트를 추출 */ +function extractBlockText(block: any): string { + const blockType = block.type; + const data = block[blockType]; + if (!data) return ''; + if (data.rich_text) return richTextToPlainText(data.rich_text); + if (blockType === 'code') return richTextToPlainText(data.rich_text); + return ''; +} + +/** + * 페이지의 모든 블록을 조회하고 텍스트를 파싱하여 스펙을 추출 + * @param pageId - Notion 페이지 ID + * @returns 추출된 텍스트와 파싱된 스펙 + */ +async function fetchPageSpecification(pageId: string): Promise<{ text: string; spec: ParsedSpecification }> { + const blocks = await fetchAllBlocksRecursively(pageId); + const text = blocks + .map(extractBlockText) + .map((line) => line.trim()) + .filter(Boolean) + .join('\n'); + const spec = parseLoggingSpec(text); + return { text, spec }; +} + +// ─── Deduplication ─────────────────────────────────────── + +/** + * "11-1.xxx" 형식의 타이틀에서 넘버링 숫자를 추출 + * @returns [주번호, 부번호] 튜플 + */ +function extractTitleNumbers(title: string): [number, number] { + const match = title.match(/(\d+)-(\d+)/); + return match ? [Number(match[1]), Number(match[2])] : [Infinity, Infinity]; +} + +/** 타이틀 넘버링 기준으로 비교 */ +function compareTitlesByNumber(a: string, b: string): number { + const [a1, a2] = extractTitleNumbers(a); + const [b1, b2] = extractTitleNumbers(b); + return a1 - b1 || a2 - b2; +} + +/** + * event_label 기준으로 이벤트를 그룹화하고 중복을 제거한 + * 같은 event_label을 가진 여러 페이지의 values를 병합하고, + * 동적/고정 value_type을 결정 + * + * @param rawEvents - 원시 이벤트 목록 + * @returns 중복 제거되고 정렬된 이벤트 스펙 목록 + */ +function deduplicateEventsByLabel(rawEvents: RawEvent[]): EventSpecification[] { + const grouped = new Map(); + + for (const event of rawEvents) { + const arr = grouped.get(event.event_label) ?? []; + arr.push(event); + grouped.set(event.event_label, arr); + } + + const results = Array.from(grouped.entries()).map(([label, pages]) => { + const eventCategory = pages[0].event_category; + const titles = pages.map((page) => page.title).sort((a, b) => compareTitlesByNumber(a, b)); + + const allValues = new Set(); + for (const page of pages) { + if (page.values.length) { + page.values.forEach((value) => allValues.add(value)); + } else if (page.value) { + allValues.add(page.value); + } + } + + const mergedExtraParams = new Set(); + for (const page of pages) { + page.extraParams.forEach((param) => mergedExtraParams.add(param)); + } + const extraParams = Array.from(mergedExtraParams); + + const values = Array.from(allValues); + const hasDynamic = pages.some((page) => page.value_type === 'dynamic'); + const hasMultipleDistinctValues = values.length > 1; + + if (hasDynamic || hasMultipleDistinctValues) { + return { + event_label: label, + event_category: eventCategory, + value: '', + value_type: 'dynamic' as const, + values, + titles, + extraParams, + }; + } + + return { + event_label: label, + event_category: eventCategory, + value: values[0] ?? '', + value_type: 'fixed' as const, + values: [], + titles, + extraParams, + }; + }); + + return results.sort((a, b) => compareTitlesByNumber(a.titles[0], b.titles[0])); +} + +// ─── Public Entry Point ────────────────────────────────── + +/** + * Notion 데이터베이스에서 로깅 이벤트 스펙을 조회하고 가공하여 반환 + * + * 전체 흐름: + * 1. database_id → data_source_id 변환 (SDK v5 요구사항) + * 2. 필터링된 전체 페이지 조회 + * 3. 각 페이지의 블록 텍스트에서 스펙 파싱 + * 4. event_label 기준 중복 제거 및 정렬 + * + * @param databaseId - UUID 형식의 Notion 데이터베이스 ID + * @returns 이벤트 스펙 목록과 파싱 에러 목록 + */ +export async function fetchLoggingEvents(databaseId: string): Promise<{ + events: EventSpecification[]; + errors: { pageId: string; title: string; reason: string; parsed: ParsedSpecification }[]; +}> { + const pages = await fetchAllPages(databaseId); + const rawEvents: RawEvent[] = []; + const errors: { pageId: string; title: string; reason: string; parsed: ParsedSpecification }[] = []; + + for (const page of pages) { + const properties = (page as any).properties; + const title = extractPageTitle(properties); + const { spec } = await fetchPageSpecification(page.id); + + if (!spec.event_label) { + errors.push({ pageId: page.id, title, reason: 'missing event_label', parsed: spec }); + continue; + } + + const extraParams = Object.keys(EXTRA_PARAM_TYPE_MAP).filter((key) => key in spec); + + rawEvents.push({ + title, + event_label: spec.event_label, + event_category: spec.event_category || 'click', + value: spec.value || '', + value_type: (spec.value_type as 'fixed' | 'dynamic') || 'fixed', + values: spec.values || [], + extraParams, + }); + } + + const events = deduplicateEventsByLabel(rawEvents); + return { events, errors }; +} diff --git a/scripts/notion/lib/types.ts b/scripts/notion/lib/types.ts new file mode 100644 index 000000000..97774ad90 --- /dev/null +++ b/scripts/notion/lib/types.ts @@ -0,0 +1,74 @@ +/** Notion 데이터베이스 설정 */ +export interface DatabaseConfig { + /** 데이터베이스 표시 이름 */ + name: string; + /** Notion 데이터베이스 ID (하이픈 없는 hex 문자열) */ + id: string; + /** 로깅 이벤트에 사용할 팀 식별자 */ + team: 'CAMPUS' | 'BUSINESS' | 'USER'; +} + +/** 페이지 블록 텍스트에서 파싱된 key-value 스펙 */ +export interface ParsedSpecification { + event_label?: string; + event_category?: string; + value?: string; + value_type?: 'fixed' | 'dynamic'; + values?: string[]; + [key: string]: any; +} + +/** 페이지 하나에서 추출된 원시 이벤트 데이터 */ +export interface RawEvent { + title: string; + event_label: string; + event_category: string; + value: string; + value_type: 'fixed' | 'dynamic'; + values: string[]; + /** 스펙에 명시된 추가 파라미터 키 목록 (예: duration_time, previous_page 등) */ + extraParams: string[]; +} + +/** 중복 제거 후 최종 이벤트 스펙 */ +export interface EventSpecification { + event_label: string; + event_category: string; + value: string; + value_type: 'fixed' | 'dynamic'; + values: string[]; + titles: string[]; + /** 스펙에 명시된 추가 파라미터 키 목록 */ + extraParams: string[]; +} + +/** + * ActionLoggerProps에서 지원하는 추가 파라미터와 TypeScript 타입 매핑. + * 스펙에 이 키가 존재하면 생성되는 훅 함수의 파라미터에 포함 + */ +export const EXTRA_PARAM_TYPE_MAP: Record = { + duration_time: 'number', + previous_page: 'string', + current_page: 'string', + custom_session_id: 'string', +}; + +/** Notion 데이터베이스 목록 */ +export const DATABASES: readonly DatabaseConfig[] = [ + { name: '공지사항', id: '1c39ae650b598199a799e73df1dd9aeb', team: 'CAMPUS' }, + { name: '식단', id: '1c39ae650b59812dbe78e2a371b7bf0a', team: 'CAMPUS' }, + { name: '버스', id: '1c39ae650b5981ecada6e03385f34894', team: 'CAMPUS' }, + { name: '배너', id: '1c29ae650b59818db23edd65c05a39b2', team: 'CAMPUS' }, + { name: '동아리', id: '1fd9ae650b5981778e7dff63a4ae7ae4', team: 'CAMPUS' }, + + { name: '주변상점', id: '1be9ae650b5981b39b25f4c7c58ac3c0', team: 'BUSINESS' }, + { name: '복덕방', id: '1be9ae650b5981368084e913e80539fe', team: 'BUSINESS' }, + { name: '사장님', id: '1be9ae650b59819e82f5f9e493ece713', team: 'BUSINESS' }, + { name: '뉴 주변상점', id: '2ae9ae650b598184a689cb5881851875', team: 'BUSINESS' }, + + { name: '로그인/회원가입/정보수정', id: '1be9ae650b59811bba53ed2619e04c1f', team: 'USER' }, + { name: '로그인/회원가입/정보수정 v2', id: '21d9ae650b59810fb634dc5addb30884', team: 'USER' }, + { name: '시간표', id: '1be9ae650b5981a1b284ffcf5f90f7ab', team: 'USER' }, + { name: '모의수강신청', id: '2f39ae650b598011a2a3d544b87e858c', team: 'USER' }, + { name: '졸업학점계산기', id: '1be9ae650b5981ea8e7cc0e31a5cebe6', team: 'USER' }, +] as const; diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts new file mode 100644 index 000000000..bbe7d3ff8 --- /dev/null +++ b/sentry.edge.config.ts @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/nextjs'; + +const environment = process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT; +const isProduction = environment === 'production'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + enabled: process.env.NODE_ENV === 'production', + environment, + release: process.env.NEXT_PUBLIC_SENTRY_RELEASE, + + tracesSampleRate: isProduction ? 0.7 : 0.1, + sendDefaultPii: true, +}); diff --git a/sentry.server.config.ts b/sentry.server.config.ts new file mode 100644 index 000000000..bbe7d3ff8 --- /dev/null +++ b/sentry.server.config.ts @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/nextjs'; + +const environment = process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT; +const isProduction = environment === 'production'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + enabled: process.env.NODE_ENV === 'production', + environment, + release: process.env.NEXT_PUBLIC_SENTRY_RELEASE, + + tracesSampleRate: isProduction ? 0.7 : 0.1, + sendDefaultPii: true, +}); diff --git a/src/api/abTest/queries.ts b/src/api/abTest/queries.ts new file mode 100644 index 000000000..00a3befd5 --- /dev/null +++ b/src/api/abTest/queries.ts @@ -0,0 +1,30 @@ +import { queryOptions } from '@tanstack/react-query'; +import { ABTestAssignResponse } from './entity'; +import { abTestAssign } from './index'; + +type DefaultABTestAssignResponse = ABTestAssignResponse | { access_history_id: null; variable_name: string }; + +const getDefaultABTestResponse = (): DefaultABTestAssignResponse => ({ + access_history_id: null, + variable_name: 'default', +}); + +export const abTestQueryKeys = { + all: ['ab-test'] as const, + assign: (title: string, authorization?: string, accessHistoryId?: string | number | null) => + [...abTestQueryKeys.all, 'assign', title, authorization ?? '', accessHistoryId ?? ''] as const, +}; + +export const abTestQueries = { + assign: (title: string, authorization?: string, accessHistoryId?: string | number | null) => + queryOptions({ + queryKey: abTestQueryKeys.assign(title, authorization, accessHistoryId), + queryFn: async () => { + try { + return await abTestAssign(title, authorization || undefined, accessHistoryId); + } catch { + return getDefaultABTestResponse(); + } + }, + }), +}; diff --git a/src/api/articles/APIDetail.ts b/src/api/articles/APIDetail.ts index 6aebe0ed8..171ce269c 100644 --- a/src/api/articles/APIDetail.ts +++ b/src/api/articles/APIDetail.ts @@ -10,11 +10,15 @@ import { LostItemArticlesPostResponseDTO, ReportItemArticleRequestDTO, ReportItemArticleResponseDTO, - ItemArticleRequestDTO, LostItemChatroomPostResponse, LostItemChatroomListResponse, LostItemChatroomDetailResponse, LostItemChatroomDetailMessagesResponse, + LostItemStatResponse, + LostItemArticlesRequest, + UpdateLostItemArticleRequestDTO, + SearchLostItemArticleResponse, + SearchLostItemArticleRequest, } from './entity'; export class GetArticles implements APIRequest { @@ -29,8 +33,9 @@ export class GetArticles implements APIRequest { constructor( public authorization: string, page: string | undefined, + boardId: number = 4, ) { - this.path = `/articles?page=${page}&limit=10`; + this.path = `/articles?boardId=${boardId}&page=${page}&limit=10`; } } @@ -57,7 +62,9 @@ export class GetHotArticles implements APIRequest export class GetLostItemArticles implements APIRequest { method = HTTP_METHOD.GET; - path: string; + path = '/articles/lost-item/v2'; + + params: LostItemArticlesRequest; response!: R; @@ -65,9 +72,9 @@ export class GetLostItemArticles implemen constructor( public authorization: string, - public data: ItemArticleRequestDTO, + params: LostItemArticlesRequest, ) { - this.path = '/articles/lost-item'; + this.params = params; } } @@ -82,7 +89,7 @@ export class GetSingleLostItemArticle imp constructor( public authorization: string, public data: LostItemArticlesRequestDTO, - ) {} + ) { } } export class DeleteLostItemArticle implements APIRequest { @@ -161,7 +168,7 @@ export class GetLostItemChatroomList imp auth = true; - constructor(public authorization: string) {} + constructor(public authorization: string) { } } export class GetLostItemChatroomDetail implements APIRequest { @@ -183,8 +190,7 @@ export class GetLostItemChatroomDetail } export class GetLostItemChatroomDetailMessages - implements APIRequest -{ + implements APIRequest { method = HTTP_METHOD.GET; path: string; @@ -219,3 +225,68 @@ export class PostBlockLostItemChatroom implements APIRequest implements APIRequest { + method = HTTP_METHOD.GET; + + path = '/articles/lost-item/stats'; + + response!: R; + + auth = false; +} + +export class PostFoundLostItem implements APIRequest { + method = HTTP_METHOD.POST; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + id: number, + ) { + this.path = `/articles/lost-item/${id}/found`; + } +} + +export class PutLostItemArticle implements APIRequest { + method = HTTP_METHOD.PUT; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + id: number, + public data: UpdateLostItemArticleRequestDTO, + ) { + this.path = `/articles/lost-item/${id}`; + } +} + +export class GetLostItemSearch implements APIRequest { + method = HTTP_METHOD.GET; + + path = '/articles/lost-item/search'; + + params: { + query: string; + page?: number; + limit?: number; + }; + + response!: R; + + auth = false; + + constructor(params: SearchLostItemArticleRequest) { + this.params = params; + } +} diff --git a/src/api/articles/ChatAPIDetailV2.ts b/src/api/articles/ChatAPIDetailV2.ts new file mode 100644 index 000000000..94733227c --- /dev/null +++ b/src/api/articles/ChatAPIDetailV2.ts @@ -0,0 +1,60 @@ +import { APIRequest, HTTP_METHOD } from 'interfaces/APIRequest'; +import { LostItemChatroomDetailMessagesResponse, LostItemChatroomDetailMessage } from './entity'; + +export class GetLostItemChatroomMessagesV2 implements APIRequest { + method = HTTP_METHOD.GET; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + articleId: number, + chatRoomId: number, + ) { + this.path = `/v2/chatroom/lost-item/${articleId}/${chatRoomId}/messages`; + } +} + +export class PostLostItemChatroomMessageV2 implements APIRequest { + method = HTTP_METHOD.POST; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + articleId: number, + chatRoomId: number, + public data: { + content: string; + is_image: boolean; + }, + ) { + this.path = `/v2/chatroom/lost-item/${articleId}/${chatRoomId}/messages`; + } +} + +export class PostLeaveLostItemChatroomV2 implements APIRequest { + method = HTTP_METHOD.POST; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + articleId: number, + chatRoomId: number, + ) { + this.path = `/v2/chatroom/lost-item/${articleId}/${chatRoomId}/leave`; + } +} diff --git a/src/api/articles/entity.ts b/src/api/articles/entity.ts index ff5f722a9..9f06dae29 100644 --- a/src/api/articles/entity.ts +++ b/src/api/articles/entity.ts @@ -16,6 +16,14 @@ export interface Article { is_reported: boolean; } +export interface ArticleWithNew extends Article { + isNew: boolean; +} + +export function isArticleWithNew(article: Article | ArticleWithNew): article is ArticleWithNew { + return 'isNew' in article && typeof (article as ArticleWithNew).isNew === 'boolean'; +} + export interface Attachment { id: 1; name: string; @@ -45,23 +53,27 @@ export interface ArticleResponse extends Article, APIResponse { next_id: number; } +export interface ArticleResponseWithNew extends ArticleResponse { + isNew: boolean; +} + export type HotArticle = Article; export type HotArticlesResponse = HotArticle[]; -// GET /articles/lost-item -interface LostItemArticleForGetDTO { +// GET /articles/lost-item/v2 +export interface LostItemArticleForGetDTO { id: number; board_id: number; type: string; category: string; found_place: string; found_date: string; - content: string; + content: string | null; author: string; registered_at: string; - updated_at: string; is_reported: boolean; + is_found: boolean; } export interface LostItemArticlesResponseDTO extends APIResponse { @@ -72,11 +84,34 @@ export interface LostItemArticlesResponseDTO extends APIResponse { current_page: number; } -interface ImageDTO { +export type LostItemType = 'LOST' | 'FOUND'; +export type LostItemCategory = 'ALL' | 'CARD' | 'ID' | 'WALLET' | 'ELECTRONICS' | 'ETC'; +export type LostItemFoundStatus = 'ALL' | 'FOUND' | 'NOT_FOUND'; +export type LostItemSort = 'LATEST' | 'OLDEST'; +export type LostItemAuthor = 'ALL' | 'MY'; + +export interface LostItemArticlesRequest { + [key: string]: unknown; + type?: LostItemType; + page?: number; + limit?: number; + category?: LostItemCategory[]; + foundStatus?: LostItemFoundStatus; + sort?: LostItemSort; + author?: LostItemAuthor; + title?: string; +} + +export interface LostItemImageDTO { id: number; image_url: string; } +interface Organization { + name: string; + location: string; +} + export interface SingleLostItemArticleResponseDTO extends APIResponse { id: number; board_id: number; @@ -84,11 +119,12 @@ export interface SingleLostItemArticleResponseDTO extends APIResponse { category: string; found_place: string; found_date: string; - content: string; + content: string | null; author: string; - is_council: boolean; + organization: Organization | null; is_mine: boolean; - images: ImageDTO[]; + is_found: boolean; + images: LostItemImageDTO[]; prev_id: number | null; next_id: number | null; registered_at: string; // yyyy-MM-dd @@ -103,7 +139,7 @@ export interface LostItemResponse extends APIResponse { found_date: string; content: string; author: string; - images: ImageDTO[]; + images: LostItemImageDTO[]; prev_id: number | null; next_id: number | null; registered_at: string; @@ -144,11 +180,6 @@ export interface LostItemArticlesPostResponseDTO { updated_at: string; } -interface LostItemImageDTO { - id: number; - image_url: string; -} - export interface LostItemArticleResponse { article: LostItemArticlesPostResponseDTO; } @@ -162,12 +193,6 @@ export interface ReportItemArticleRequestDTO { } export type ReportItemArticleResponseDTO = APIResponse; - -export interface ItemArticleRequestDTO { - boardId: number; - page: number; - limit: number; -} export interface LostItemChatroomDetailResponse { article_id: number; chat_room_id: number; @@ -199,3 +224,33 @@ export interface LostItemChatroomDetailMessage { } export type LostItemChatroomDetailMessagesResponse = LostItemChatroomDetailMessage[]; + +export interface LostItemStatResponse { + found_count: number; + not_found_count: number; +} + +// PUT /articles/lost-item/{id} +export interface UpdateLostItemArticleRequestDTO { + category: string; + found_place: string; + found_date: string; // yyyy-MM-dd + content: string; + new_images: string[]; + delete_image_ids: number[]; +} + +// GET /articles/lost-item/search +export interface SearchLostItemArticleRequest { + query: string; + page?: number; + limit?: number; +} + +export interface SearchLostItemArticleResponse { + articles: LostItemArticleForGetDTO[]; + total_count: number; + current_count: number; + total_page: number; + current_page: number; +} diff --git a/src/api/articles/index.ts b/src/api/articles/index.ts index a1db03d3f..fb03d99ae 100644 --- a/src/api/articles/index.ts +++ b/src/api/articles/index.ts @@ -1,3 +1,8 @@ +import { + GetLostItemChatroomMessagesV2, + PostLeaveLostItemChatroomV2, + PostLostItemChatroomMessageV2, +} from 'api/articles/ChatAPIDetailV2'; import APIClient from 'utils/ts/apiClient'; import { GetArticles, @@ -13,6 +18,10 @@ import { GetLostItemChatroomList, GetLostItemChatroomDetail, GetLostItemChatroomDetailMessages, + GetLostItemStat, + PostFoundLostItem, + PutLostItemArticle, + GetLostItemSearch, } from './APIDetail'; export const getArticles = (token: string, page: string) => APIClient.of(GetArticles)(token, page); @@ -40,3 +49,17 @@ export const getLostItemChatroomDetail = APIClient.of(GetLostItemChatroomDetail) export const getLostItemChatroomDetailMessages = APIClient.of(GetLostItemChatroomDetailMessages); export const postBlockLostItemChatroom = APIClient.of(PostBlockLostItemChatroom); + +export const getLostItemStat = APIClient.of(GetLostItemStat); + +export const postFoundLostItem = APIClient.of(PostFoundLostItem); + +export const putLostItemArticle = APIClient.of(PutLostItemArticle); + +export const getLostItemSearch = APIClient.of(GetLostItemSearch); + +export const getLostItemChatroomMessagesV2 = APIClient.of(GetLostItemChatroomMessagesV2); + +export const postLostItemChatroomMessageV2 = APIClient.of(PostLostItemChatroomMessageV2); + +export const postLeaveLostItemChatroomV2 = APIClient.of(PostLeaveLostItemChatroomV2); diff --git a/src/api/articles/mutations.ts b/src/api/articles/mutations.ts new file mode 100644 index 000000000..5d61d07b3 --- /dev/null +++ b/src/api/articles/mutations.ts @@ -0,0 +1,77 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { + LostItemArticlesRequestDTO, + ReportItemArticleRequestDTO, + UpdateLostItemArticleRequestDTO, +} from './entity'; +import { articleQueryKeys } from './queries'; +import { + deleteLostItemArticle, + postBlockLostItemChatroom, + postFoundLostItem, + postLostItemArticle, + postLostItemChatroom, + postReportLostItemArticle, + putLostItemArticle, +} from './index'; + +const invalidateLostItemAll = (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: articleQueryKeys.lostItemAll }); + +const invalidateLostItemChatroomAll = (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: articleQueryKeys.lostItemChatroomAll }); + +export const articleMutations = { + createLostItem: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: async (data: LostItemArticlesRequestDTO) => { + const response = await postLostItemArticle(token, data); + return response.id; + }, + onSuccess: () => invalidateLostItemAll(queryClient), + }), + + updateLostItem: (queryClient: QueryClient, token: string, articleId: number) => + mutationOptions({ + mutationFn: async (data: UpdateLostItemArticleRequestDTO) => { + const response = await putLostItemArticle(token, articleId, data); + return response.id; + }, + onSuccess: () => invalidateLostItemAll(queryClient), + }), + + deleteLostItem: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (articleId: number) => deleteLostItemArticle(token, articleId), + onSuccess: () => invalidateLostItemAll(queryClient), + }), + + reportLostItem: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: ({ articleId, reports }: { articleId: number; reports: ReportItemArticleRequestDTO['reports'] }) => + postReportLostItemArticle(token, articleId, { reports }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: articleQueryKeys.all }); + await invalidateLostItemAll(queryClient); + }, + }), + + toggleLostItemFound: (queryClient: QueryClient, token: string, articleId: number) => + mutationOptions({ + mutationFn: () => postFoundLostItem(token, articleId), + onSuccess: () => queryClient.invalidateQueries({ queryKey: articleQueryKeys.lostItemDetail(articleId) }), + }), + + createLostItemChatroom: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (articleId: number) => postLostItemChatroom(token, articleId), + onSuccess: () => invalidateLostItemChatroomAll(queryClient), + }), + + blockLostItemChatroom: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: ({ articleId, chatroomId }: { articleId: number; chatroomId: number }) => + postBlockLostItemChatroom(token, articleId, chatroomId), + onSuccess: () => invalidateLostItemChatroomAll(queryClient), + }), +}; diff --git a/src/api/articles/queries.ts b/src/api/articles/queries.ts new file mode 100644 index 000000000..ce0423f05 --- /dev/null +++ b/src/api/articles/queries.ts @@ -0,0 +1,120 @@ +import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; +import { LostItemArticlesRequest, SearchLostItemArticleRequest } from './entity'; +import { + getArticle, + getArticles, + getLostItemChatroomDetail, + getLostItemChatroomList, + getLostItemChatroomMessagesV2, + getHotArticles, + getLostItemArticles, + getLostItemSearch, + getLostItemStat, + getSingleLostItemArticle, +} from './index'; + +type LostItemInfiniteListParams = Omit; + +type LostItemSearchParams = Required> & { + page: number; + limit: number; +}; + +export const articleQueryKeys = { + all: ['articles'] as const, + listRoot: ['articles', 'list'] as const, + list: (page: string) => [...articleQueryKeys.listRoot, page] as const, + hot: ['articles', 'hot'] as const, + detail: (id: string) => ['articles', 'detail', id] as const, + lostItemAll: ['lostItem'] as const, + lostItemListRoot: ['lostItem', 'list'] as const, + lostItemList: (params: LostItemArticlesRequest) => [...articleQueryKeys.lostItemListRoot, params] as const, + lostItemInfiniteListRoot: ['lostItem', 'infinite-list'] as const, + lostItemInfiniteList: (params: LostItemInfiniteListParams) => + [...articleQueryKeys.lostItemInfiniteListRoot, params] as const, + lostItemDetail: (articleId: number) => ['lostItem', 'detail', articleId] as const, + lostItemSearch: (params: LostItemSearchParams) => ['lostItem', 'search', params] as const, + lostItemStat: ['lostItem', 'stat'] as const, + lostItemChatroomAll: ['chatroom', 'lost-item'] as const, + lostItemChatroomList: ['chatroom', 'lost-item', 'list'] as const, + lostItemChatroomDetail: (articleId: number | string | null, chatroomId: number | string | null) => + ['chatroom', 'lost-item', 'detail', articleId, chatroomId] as const, + lostItemChatroomMessages: (articleId: number | string | null, chatroomId: number | string | null) => + ['chatroom', 'lost-item', 'messages', articleId, chatroomId] as const, +}; + +export const articleQueries = { + list: (token: string, page: string) => + queryOptions({ + queryKey: articleQueryKeys.list(page), + queryFn: () => getArticles(token, page), + }), + + hot: () => + queryOptions({ + queryKey: articleQueryKeys.hot, + queryFn: getHotArticles, + }), + + detail: (id: string) => + queryOptions({ + queryKey: articleQueryKeys.detail(id), + queryFn: () => getArticle(id), + }), + + lostItemList: (token: string, params: LostItemArticlesRequest) => + queryOptions({ + queryKey: articleQueryKeys.lostItemList(params), + queryFn: () => getLostItemArticles(token, params), + }), + + lostItemInfiniteList: (token: string, params: LostItemInfiniteListParams) => + infiniteQueryOptions({ + queryKey: articleQueryKeys.lostItemInfiniteList(params), + initialPageParam: 1, + queryFn: ({ pageParam }) => getLostItemArticles(token, { ...params, page: pageParam }), + getNextPageParam: (lastPage) => { + if (lastPage.total_page > lastPage.current_page) { + return lastPage.current_page + 1; + } + + return undefined; + }, + }), + + lostItemDetail: (token: string, articleId: number) => + queryOptions({ + queryKey: articleQueryKeys.lostItemDetail(articleId), + queryFn: () => getSingleLostItemArticle(token, articleId), + }), + + lostItemSearch: (params: LostItemSearchParams) => + queryOptions({ + queryKey: articleQueryKeys.lostItemSearch(params), + queryFn: () => getLostItemSearch(params), + }), + + lostItemStat: () => + queryOptions({ + queryKey: articleQueryKeys.lostItemStat, + queryFn: getLostItemStat, + }), + + lostItemChatroomList: (token: string) => + queryOptions({ + queryKey: articleQueryKeys.lostItemChatroomList, + queryFn: () => getLostItemChatroomList(token), + }), + + lostItemChatroomDetail: (token: string, articleId: number, chatroomId: number) => + queryOptions({ + queryKey: articleQueryKeys.lostItemChatroomDetail(articleId, chatroomId), + queryFn: () => getLostItemChatroomDetail(token, articleId, chatroomId), + }), + + lostItemChatroomMessages: (token: string, articleId: number, chatroomId: number) => + queryOptions({ + queryKey: articleQueryKeys.lostItemChatroomMessages(articleId, chatroomId), + queryFn: () => getLostItemChatroomMessagesV2(token, articleId, chatroomId), + }), +}; diff --git a/src/api/auth/queries.ts b/src/api/auth/queries.ts new file mode 100644 index 000000000..b90e28d2c --- /dev/null +++ b/src/api/auth/queries.ts @@ -0,0 +1,34 @@ +import { queryOptions } from '@tanstack/react-query'; +import { GeneralUserResponse, UserAcademicInfoResponse, UserResponse } from './entity'; +import { getGeneralUser, getUser, getUserAcademicInfo } from './index'; + +type AuthUserType = 'STUDENT' | 'GENERAL'; +type AuthUserInfoResponse = UserResponse | GeneralUserResponse; + +const getUserInfo = (token: string, userType: AuthUserType): Promise => { + if (userType === 'STUDENT') { + return getUser(token); + } + + return getGeneralUser(token); +}; + +export const authQueryKeys = { + all: ['auth'] as const, + userInfo: (token: string, userType: AuthUserType) => [...authQueryKeys.all, 'user-info', token, userType] as const, + userAcademicInfo: (token: string) => [...authQueryKeys.all, 'user-academic-info', token] as const, +}; + +export const authQueries = { + userInfo: (token: string, userType: AuthUserType) => + queryOptions({ + queryKey: authQueryKeys.userInfo(token, userType), + queryFn: () => (token ? getUserInfo(token, userType) : null), + }), + + userAcademicInfo: (token: string) => + queryOptions({ + queryKey: authQueryKeys.userAcademicInfo(token), + queryFn: () => (token ? getUserAcademicInfo(token) : null), + }), +}; diff --git a/src/api/banner/queries.ts b/src/api/banner/queries.ts new file mode 100644 index 000000000..594b28bad --- /dev/null +++ b/src/api/banner/queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getBannerCategoryList, getBanners } from './index'; + +export const bannerQueryKeys = { + all: ['banner'] as const, + categories: () => [...bannerQueryKeys.all, 'categories'] as const, + list: (categoryId: number) => [...bannerQueryKeys.all, 'list', categoryId] as const, +}; + +export const bannerQueries = { + categories: () => + queryOptions({ + queryKey: bannerQueryKeys.categories(), + queryFn: getBannerCategoryList, + }), + + list: (categoryId: number) => + queryOptions({ + queryKey: bannerQueryKeys.list(categoryId), + queryFn: () => getBanners(categoryId), + }), +}; diff --git a/src/api/bus/queries.ts b/src/api/bus/queries.ts new file mode 100644 index 000000000..37857df3f --- /dev/null +++ b/src/api/bus/queries.ts @@ -0,0 +1,92 @@ +import { queryOptions, skipToken } from '@tanstack/react-query'; +import { + BusRouteParams, + CityBusParams, + Depart, + Arrival, + ExpressCourse, + ShuttleCourse, +} from './entity'; +import { + getBusNoticeInfo, + getBusRouteInfo, + getBusTimetableInfo, + getCityBusTimetableInfo, + getShuttleCourseInfo, + getShuttleTimetableDetailInfo, +} from './index'; + +export interface BusRouteQueryParams extends Omit { + depart: Depart | ''; + arrival: Arrival | ''; +} + +export const busQueryKeys = { + all: ['bus'] as const, + notice: () => [...busQueryKeys.all, 'notice'] as const, + shuttleCourse: () => [...busQueryKeys.all, 'courses', 'shuttle'] as const, + timetable: ['bus', 'timetable'] as const, + shuttleTimetable: (course: ShuttleCourse) => + [...busQueryKeys.timetable, 'shuttle', course.bus_type, course.direction, course.region] as const, + expressTimetable: (course: ExpressCourse) => + [...busQueryKeys.timetable, 'express', course.bus_type, course.direction, course.region] as const, + cityTimetable: (course: CityBusParams) => + [...busQueryKeys.timetable, 'city', course.bus_number, course.direction] as const, + shuttleTimetableDetail: (id: string | null) => [...busQueryKeys.all, 'shuttle', 'timetable', id] as const, + route: (params: BusRouteQueryParams) => [...busQueryKeys.all, 'route', JSON.stringify(params)] as const, +}; + +export const busQueries = { + notice: () => + queryOptions({ + queryKey: busQueryKeys.notice(), + queryFn: getBusNoticeInfo, + }), + + shuttleCourse: () => + queryOptions({ + queryKey: busQueryKeys.shuttleCourse(), + queryFn: getShuttleCourseInfo, + }), + + shuttleTimetable: (course: ShuttleCourse) => + queryOptions({ + queryKey: busQueryKeys.shuttleTimetable(course), + queryFn: () => getBusTimetableInfo(course), + }), + + expressTimetable: (course: ExpressCourse) => + queryOptions({ + queryKey: busQueryKeys.expressTimetable(course), + queryFn: () => getBusTimetableInfo(course), + }), + + cityTimetable: (course: CityBusParams) => + queryOptions({ + queryKey: busQueryKeys.cityTimetable(course), + queryFn: () => getCityBusTimetableInfo(course), + }), + + shuttleTimetableDetail: (id: string | null) => + queryOptions({ + queryKey: busQueryKeys.shuttleTimetableDetail(id), + queryFn: id ? () => getShuttleTimetableDetailInfo({ id }) : skipToken, + }), + + route: (params: BusRouteQueryParams) => { + const { depart, arrival, ...rest } = params; + + return queryOptions({ + queryKey: busQueryKeys.route(params), + queryFn: + depart && arrival + ? () => + getBusRouteInfo({ + ...rest, + depart: depart as Depart, + arrival: arrival as Arrival, + }) + : skipToken, + }); + }, +}; diff --git a/src/api/cafeteria/index.ts b/src/api/cafeteria/index.ts index a6d45c105..a063fecf8 100644 --- a/src/api/cafeteria/index.ts +++ b/src/api/cafeteria/index.ts @@ -1,8 +1,8 @@ import APIClient from 'utils/ts/apiClient'; import DiningResponse, { DiningLikePatcher, CancelDiningLikePatcher } from './APIDetail'; -export const like = APIClient.of(DiningLikePatcher); +export const getCafeteriaDinings = APIClient.of(DiningResponse); -export const cancelLike = APIClient.of(CancelDiningLikePatcher); +export const likeCafeteriaDining = APIClient.of(DiningLikePatcher); -export default APIClient.of(DiningResponse); +export const cancelCafeteriaDiningLike = APIClient.of(CancelDiningLikePatcher); diff --git a/src/api/cafeteria/mutations.ts b/src/api/cafeteria/mutations.ts new file mode 100644 index 000000000..fe0c46e69 --- /dev/null +++ b/src/api/cafeteria/mutations.ts @@ -0,0 +1,20 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { cafeteriaQueryKeys } from './queries'; +import { cancelCafeteriaDiningLike, likeCafeteriaDining } from './index'; + +const invalidateDinings = (queryClient: QueryClient, date: string) => + queryClient.invalidateQueries({ queryKey: cafeteriaQueryKeys.dinings(date) }); + +export const cafeteriaMutations = { + likeDining: (queryClient: QueryClient, token: string, date: string) => + mutationOptions({ + mutationFn: (diningId: number) => likeCafeteriaDining(diningId, token), + onSuccess: () => invalidateDinings(queryClient, date), + }), + + cancelLikeDining: (queryClient: QueryClient, token: string, date: string) => + mutationOptions({ + mutationFn: (diningId: number) => cancelCafeteriaDiningLike(diningId, token), + onSuccess: () => invalidateDinings(queryClient, date), + }), +}; diff --git a/src/api/cafeteria/queries.ts b/src/api/cafeteria/queries.ts new file mode 100644 index 000000000..1394b83d8 --- /dev/null +++ b/src/api/cafeteria/queries.ts @@ -0,0 +1,15 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getCafeteriaDinings } from './index'; + +export const cafeteriaQueryKeys = { + all: ['cafeteria'] as const, + dinings: (date: string) => [...cafeteriaQueryKeys.all, 'dinings', date] as const, +}; + +export const cafeteriaQueries = { + dinings: (date: string) => + queryOptions({ + queryKey: cafeteriaQueryKeys.dinings(date), + queryFn: () => getCafeteriaDinings(date), + }), +}; diff --git a/src/api/callvan/APIDetail.ts b/src/api/callvan/APIDetail.ts new file mode 100644 index 000000000..1fa249767 --- /dev/null +++ b/src/api/callvan/APIDetail.ts @@ -0,0 +1,264 @@ +import { APIRequest, HTTP_METHOD } from 'interfaces/APIRequest'; +import { + CallvanChatResponse, + CallvanListRequest, + CallvanListResponse, + CallvanNotificationsResponse, + CallvanPostDetail, + CallvanReportRequest, + CreateCallvanRequest, + CreateCallvanResponse, + SendChatRequest, +} from './entity'; + +export class GetCallvanList implements APIRequest { + method = HTTP_METHOD.GET; + + path = '/callvan'; + + params: CallvanListRequest; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + params: CallvanListRequest, + ) { + this.params = params; + } +} + +export class GetCallvanNotifications implements APIRequest { + method = HTTP_METHOD.GET; + + path = '/callvan/notifications'; + + response!: R; + + auth = true; + + constructor(public authorization: string) {} +} + +export class PostMarkAllNotificationsRead implements APIRequest { + method = HTTP_METHOD.POST; + + path = '/callvan/notifications/mark-all-read'; + + response!: R; + + auth = true; + + constructor(public authorization: string) {} +} + +export class PostMarkNotificationRead implements APIRequest { + method = HTTP_METHOD.POST; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + notificationId: number, + ) { + this.path = `/callvan/notifications/${notificationId}/read`; + } +} + +export class PostCallvan implements APIRequest { + method = HTTP_METHOD.POST; + + path = '/callvan'; + + data: CreateCallvanRequest; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + data: CreateCallvanRequest, + ) { + this.data = data; + } +} + +export class DeleteAllNotifications implements APIRequest { + method = HTTP_METHOD.DELETE; + + path = '/callvan/notifications'; + + response!: R; + + auth = true; + + constructor(public authorization: string) {} +} + +export class GetCallvanPostDetail implements APIRequest { + method = HTTP_METHOD.GET; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + ) { + this.path = `/callvan/posts/${postId}`; + } +} + +export class PostJoinCallvan implements APIRequest { + method = HTTP_METHOD.POST; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + ) { + this.path = `/callvan/posts/${postId}/participants`; + } +} + +export class DeleteCancelCallvan implements APIRequest { + method = HTTP_METHOD.DELETE; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + ) { + this.path = `/callvan/posts/${postId}/participants`; + } +} + +export class GetCallvanChat implements APIRequest { + method = HTTP_METHOD.GET; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + ) { + this.path = `/callvan/posts/${postId}/chat`; + } +} + +export class PostCallvanChat implements APIRequest { + method = HTTP_METHOD.POST; + + path: string; + + data: SendChatRequest; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + data: SendChatRequest, + ) { + this.path = `/callvan/posts/${postId}/chat`; + this.data = data; + } +} + +export class PutCloseCallvanPost implements APIRequest { + method = HTTP_METHOD.PUT; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + ) { + this.path = `/callvan/posts/${postId}/close`; + } +} + +export class PutReopenCallvanPost implements APIRequest { + method = HTTP_METHOD.PUT; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + ) { + this.path = `/callvan/posts/${postId}/reopen`; + } +} + +export class PutCompleteCallvanPost implements APIRequest { + method = HTTP_METHOD.PUT; + + path: string; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + ) { + this.path = `/callvan/posts/${postId}/complete`; + } +} + +export class PostCallvanReport implements APIRequest { + method = HTTP_METHOD.POST; + + path: string; + + data: CallvanReportRequest; + + response!: R; + + auth = true; + + constructor( + public authorization: string, + postId: number, + data: CallvanReportRequest, + ) { + this.path = `/callvan/posts/${postId}/reports`; + this.data = data; + } +} diff --git a/src/api/callvan/entity.ts b/src/api/callvan/entity.ts new file mode 100644 index 000000000..5bdacb133 --- /dev/null +++ b/src/api/callvan/entity.ts @@ -0,0 +1,223 @@ +import { APIResponse } from 'interfaces/APIResponse'; + +export type CallvanStatus = 'RECRUITING' | 'CLOSED' | 'COMPLETED'; +export type CallvanSort = 'DEPARTURE_ASC' | 'DEPARTURE_DESC' | 'LATEST_ASC' | 'LATEST_DESC'; +export type CallvanAuthor = 'ALL' | 'MY'; +export type CallvanLocation = + | 'FRONT_GATE' + | 'BACK_GATE' + | 'TENNIS_COURT' + | 'DORMITORY_MAIN' + | 'DORMITORY_SUB' + | 'TERMINAL' + | 'STATION' + | 'ASAN_STATION' + | 'CUSTOM'; + +export const CALLVAN_LOCATION_LABEL: Record = { + FRONT_GATE: '정문', + BACK_GATE: '후문', + TENNIS_COURT: '테니스장', + DORMITORY_MAIN: '본관동', + DORMITORY_SUB: '별관동', + TERMINAL: '천안 터미널', + STATION: '천안역', + ASAN_STATION: '천안아산역', + CUSTOM: '직접입력', +}; + +export const CALLVAN_LOCATIONS: CallvanLocation[] = [ + 'FRONT_GATE', + 'BACK_GATE', + 'TENNIS_COURT', + 'DORMITORY_MAIN', + 'DORMITORY_SUB', + 'TERMINAL', + 'STATION', + 'ASAN_STATION', +]; + +export interface CallvanListRequest { + [key: string]: unknown; + author?: CallvanAuthor; + statuses?: CallvanStatus[]; + departures?: CallvanLocation[]; + departure_keyword?: string; + arrivals?: CallvanLocation[]; + arrival_keyword?: string; + title?: string; + sort?: CallvanSort; + joined?: boolean; + page?: number; + limit?: number; +} + +export interface CallvanPost { + id: number; + title: string; + departure: string; + arrival: string; + departure_date: string; + departure_time: string; + author_nickname: string; + current_participants: number; + max_participants: number; + status: CallvanStatus; + is_joined: boolean; + is_author: boolean; +} + +export interface CallvanListResponse extends APIResponse { + posts: CallvanPost[]; + total_count: number; + current_page: number; + total_page: number; +} + +export type CallvanNotificationType = + | 'RECRUITMENT_COMPLETE' + | 'NEW_MESSAGE' + | 'PARTICIPANT_JOINED' + | 'DEPARTURE_UPCOMING'; + +export interface CallvanNotification { + id: number; + type: CallvanNotificationType; + message_preview: string | null; + is_read: boolean; + created_at: string; + post_id: number; + departure: string; + arrival: string; + departure_date: string; + departure_time: string; + current_participants: number; + max_participants: number; + sender_nickname: string | null; + joined_member_nickname: string | null; +} + +export type CallvanNotificationsResponse = CallvanNotification[]; + +export type CallvanPostLocationType = + | 'FRONT_GATE' + | 'BACK_GATE' + | 'TENNIS_COURT' + | 'DORMITORY_MAIN' + | 'DORMITORY_SUB' + | 'TERMINAL' + | 'STATION' + | 'ASAN_STATION' + | 'CUSTOM'; + +export const CALLVAN_POST_LOCATION_LABEL: Record = { + FRONT_GATE: '정문', + BACK_GATE: '후문', + TENNIS_COURT: '테니스장', + DORMITORY_MAIN: '본관동', + DORMITORY_SUB: '별관동', + TERMINAL: '천안 터미널', + STATION: '천안역', + ASAN_STATION: '천안아산역', + CUSTOM: '직접입력', +}; + +export const CALLVAN_POST_LOCATIONS: CallvanPostLocationType[] = [ + 'FRONT_GATE', + 'BACK_GATE', + 'TENNIS_COURT', + 'DORMITORY_MAIN', + 'DORMITORY_SUB', + 'TERMINAL', + 'STATION', + 'ASAN_STATION', + 'CUSTOM', +]; + +export interface CallvanParticipant { + user_id: number; + nickname: string; + is_me: boolean; +} + +export interface CallvanPostDetail { + id: number; + title: string; + departure: string; + arrival: string; + departure_date: string; + departure_time: string; + current_participants: number; + max_participants: number; + status: CallvanStatus; + participants: CallvanParticipant[]; +} + +export type CallvanReportReasonCode = 'NO_SHOW' | 'NON_PAYMENT' | 'PROFANITY' | 'OTHER'; + +export interface CallvanReportReason { + reason_code: CallvanReportReasonCode; + custom_text?: string; +} + +export type CallvanReportAttachmentType = 'IMAGE'; + +export interface CallvanReportAttachment { + attachment_type: CallvanReportAttachmentType; + url: string; +} + +export interface CallvanReportRequest { + reported_user_id: number; + description?: string; + reasons: CallvanReportReason[]; + attachments?: CallvanReportAttachment[]; +} + +export interface CreateCallvanRequest { + departure_type: CallvanPostLocationType; + departure_custom_name?: string | null; + arrival_type: CallvanPostLocationType; + arrival_custom_name?: string | null; + departure_date: string; + departure_time: string; + max_participants: number; +} + +export interface CreateCallvanResponse { + id: number; + author: string; + departure_type: CallvanPostLocationType; + departure_custom_name: string; + arrival_type: CallvanPostLocationType; + arrival_custom_name: string; + departure_date: string; + departure_time: string; + max_participants: number; + current_participants: number; + status: CallvanStatus; + created_at: string; + updated_at: string; +} + +export interface CallvanChatMessage { + user_id: number; + sender_nickname: string; + content: string; + date: string; + time: string; + unread_count: number; + is_image: boolean; + is_left_user: boolean; + is_mine: boolean; +} + +export interface CallvanChatResponse { + room_name: string; + messages: CallvanChatMessage[]; +} + +export interface SendChatRequest { + is_image: boolean; + content: string; +} diff --git a/src/api/callvan/index.ts b/src/api/callvan/index.ts new file mode 100644 index 000000000..c74248975 --- /dev/null +++ b/src/api/callvan/index.ts @@ -0,0 +1,34 @@ +import APIClient from 'utils/ts/apiClient'; +import { + DeleteAllNotifications, + DeleteCancelCallvan, + GetCallvanChat, + GetCallvanList, + GetCallvanNotifications, + GetCallvanPostDetail, + PostCallvan, + PostCallvanReport, + PostCallvanChat, + PostJoinCallvan, + PostMarkAllNotificationsRead, + PostMarkNotificationRead, + PutCloseCallvanPost, + PutCompleteCallvanPost, + PutReopenCallvanPost, +} from './APIDetail'; + +export const getCallvanList = APIClient.of(GetCallvanList); +export const getCallvanPostDetail = APIClient.of(GetCallvanPostDetail); +export const getCallvanNotifications = APIClient.of(GetCallvanNotifications); +export const markAllNotificationsRead = APIClient.of(PostMarkAllNotificationsRead); +export const markNotificationRead = APIClient.of(PostMarkNotificationRead); +export const deleteAllNotifications = APIClient.of(DeleteAllNotifications); +export const createCallvan = APIClient.of(PostCallvan); +export const closeCallvanPost = APIClient.of(PutCloseCallvanPost); +export const reopenCallvanPost = APIClient.of(PutReopenCallvanPost); +export const completeCallvanPost = APIClient.of(PutCompleteCallvanPost); +export const reportCallvanParticipant = APIClient.of(PostCallvanReport); +export const joinCallvan = APIClient.of(PostJoinCallvan); +export const cancelCallvan = APIClient.of(DeleteCancelCallvan); +export const getCallvanChat = APIClient.of(GetCallvanChat); +export const sendCallvanChat = APIClient.of(PostCallvanChat); diff --git a/src/api/callvan/mutations.ts b/src/api/callvan/mutations.ts new file mode 100644 index 000000000..34fdf3ac4 --- /dev/null +++ b/src/api/callvan/mutations.ts @@ -0,0 +1,90 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { CallvanReportRequest, CreateCallvanRequest, SendChatRequest } from './entity'; +import { callvanQueryKeys } from './queries'; +import { + cancelCallvan, + closeCallvanPost, + completeCallvanPost, + createCallvan, + deleteAllNotifications, + joinCallvan, + markAllNotificationsRead, + markNotificationRead, + reopenCallvanPost, + reportCallvanParticipant, + sendCallvanChat, +} from './index'; + +const invalidateCallvanInfiniteList = (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: callvanQueryKeys.infiniteListRoot }); + +const invalidateCallvanNotifications = (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: callvanQueryKeys.notifications }); + +export const callvanMutations = { + create: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (data: CreateCallvanRequest) => createCallvan(token, data), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + join: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => joinCallvan(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + cancel: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => cancelCallvan(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + close: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => closeCallvanPost(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + reopen: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => reopenCallvanPost(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + complete: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (postId: number) => completeCallvanPost(token, postId), + onSuccess: () => invalidateCallvanInfiniteList(queryClient), + }), + + report: (queryClient: QueryClient, token: string, postId: number) => + mutationOptions({ + mutationFn: (data: CallvanReportRequest) => reportCallvanParticipant(token, postId, data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: callvanQueryKeys.postDetail(postId) }), + }), + + markAllNotificationsRead: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: () => markAllNotificationsRead(token), + onSuccess: () => invalidateCallvanNotifications(queryClient), + }), + + markNotificationRead: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (notificationId: number) => markNotificationRead(token, notificationId), + onSuccess: () => invalidateCallvanNotifications(queryClient), + }), + + deleteAllNotifications: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: () => deleteAllNotifications(token), + onSuccess: () => invalidateCallvanNotifications(queryClient), + }), + + sendChat: (queryClient: QueryClient, token: string, postId: number) => + mutationOptions({ + mutationFn: (data: SendChatRequest) => sendCallvanChat(token, postId, data), + onSuccess: () => queryClient.invalidateQueries({ queryKey: callvanQueryKeys.chat(postId) }), + }), +}; diff --git a/src/api/callvan/queries.ts b/src/api/callvan/queries.ts new file mode 100644 index 000000000..1ca9fe369 --- /dev/null +++ b/src/api/callvan/queries.ts @@ -0,0 +1,66 @@ +import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; +import { CallvanListRequest } from './entity'; +import { getCallvanChat, getCallvanList, getCallvanNotifications, getCallvanPostDetail } from './index'; + +const CALLVAN_LIST_LIMIT = 10; + +type CallvanInfiniteListParams = Omit; + +export const callvanQueryKeys = { + all: ['callvan'] as const, + listRoot: ['callvan', 'list'] as const, + list: (params: CallvanListRequest) => [...callvanQueryKeys.listRoot, params] as const, + infiniteListRoot: ['callvan', 'infinite-list'] as const, + infiniteList: (params: CallvanInfiniteListParams) => [...callvanQueryKeys.infiniteListRoot, params] as const, + notifications: ['callvan', 'notifications'] as const, + postDetail: (postId: number) => ['callvan', 'post-detail', postId] as const, + chat: (postId: number) => ['callvan', 'chat', postId] as const, +}; + +export const callvanQueries = { + list: (token: string, params: CallvanListRequest) => + queryOptions({ + queryKey: callvanQueryKeys.list(params), + queryFn: () => getCallvanList(token, params), + }), + + infiniteList: (token: string, params: CallvanInfiniteListParams) => + infiniteQueryOptions({ + queryKey: callvanQueryKeys.infiniteList(params), + initialPageParam: 1, + queryFn: ({ pageParam }) => + getCallvanList(token, { + ...params, + page: pageParam, + limit: CALLVAN_LIST_LIMIT, + }), + getNextPageParam: (lastPage) => { + if (lastPage.current_page < lastPage.total_page) { + return lastPage.current_page + 1; + } + + return undefined; + }, + }), + + notifications: (token: string) => + queryOptions({ + queryKey: callvanQueryKeys.notifications, + queryFn: () => getCallvanNotifications(token), + }), + + postDetail: (token: string, postId: number) => + queryOptions({ + queryKey: callvanQueryKeys.postDetail(postId), + queryFn: () => getCallvanPostDetail(token, postId), + staleTime: 60000, + }), + + chat: (token: string, postId: number) => + queryOptions({ + queryKey: callvanQueryKeys.chat(postId), + queryFn: () => getCallvanChat(token, postId), + staleTime: 0, + refetchInterval: 1000, + }), +}; diff --git a/src/api/club/mutations.ts b/src/api/club/mutations.ts new file mode 100644 index 000000000..b9c69d343 --- /dev/null +++ b/src/api/club/mutations.ts @@ -0,0 +1,226 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { clubQueryKeys } from './queries'; +import type { ClubEventRequest, ClubRecruitmentRequest, NewClubData, NewClubManager } from './entity'; +import { + deleteClubEvent, + deleteClubEventNotification, + deleteClubLike, + deleteClubRecruitment, + deleteClubRecruitmentNotification, + postClub, + postClubEvent, + postClubEventNotification, + postClubRecruitment, + postClubRecruitmentNotification, + putClubDetail, + putClubEvent, + putClubLike, + putClubRecruitment, + putNewClubManager, +} from './index'; + +interface ClubMutationCallbacks { + onSuccess?: () => void | Promise; +} + +const invalidateClubListQueries = async (queryClient: QueryClient, includeHot = false) => { + const tasks = [queryClient.invalidateQueries({ queryKey: clubQueryKeys.listRoot() })]; + + if (includeHot) { + tasks.push(queryClient.invalidateQueries({ queryKey: clubQueryKeys.hot() })); + } + + await Promise.all(tasks); +}; + +const invalidateClubDetailAndListQueries = async (queryClient: QueryClient, clubId: number, includeHot = false) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: clubQueryKeys.detailRoot(clubId) }), + invalidateClubListQueries(queryClient, includeHot), + ]); +}; + +const invalidateRecruitmentQueries = async (queryClient: QueryClient, clubId: number) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: clubQueryKeys.recruitment(clubId) }), + queryClient.invalidateQueries({ queryKey: clubQueryKeys.listRoot() }), + ]); +}; + +const invalidateEventListQueries = async (queryClient: QueryClient, clubId: number | string) => { + await queryClient.invalidateQueries({ queryKey: clubQueryKeys.eventListRoot(clubId) }); +}; + +export const clubMutations = { + toggleLikeForList: (queryClient: QueryClient, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: ({ token, clubId, isLiked }: { token: string; clubId: number; isLiked: boolean }) => + isLiked ? deleteClubLike(token, clubId) : putClubLike(token, clubId), + onSuccess: async () => { + await invalidateClubListQueries(queryClient); + await callbacks.onSuccess?.(); + }, + }), + + likeForDetail: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: () => putClubLike(token, clubId), + onSuccess: async () => { + await invalidateClubDetailAndListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + unlikeForDetail: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: () => deleteClubLike(token, clubId), + onSuccess: async () => { + await invalidateClubDetailAndListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + create: (queryClient: QueryClient, token: string, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: NewClubData) => postClub(token, data), + onSuccess: async () => { + await invalidateClubListQueries(queryClient); + await callbacks.onSuccess?.(); + }, + }), + + update: (queryClient: QueryClient, token: string, clubId: number | string, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: NewClubData) => putClubDetail(token, data, clubId), + onSuccess: async () => { + await invalidateClubDetailAndListQueries(queryClient, Number(clubId), true); + await callbacks.onSuccess?.(); + }, + }), + + createRecruitment: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: ClubRecruitmentRequest) => postClubRecruitment(token, clubId, data), + onSuccess: async () => { + await invalidateRecruitmentQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + updateRecruitment: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: ClubRecruitmentRequest) => putClubRecruitment(token, clubId, data), + onSuccess: async () => { + await invalidateRecruitmentQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + deleteRecruitment: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: () => deleteClubRecruitment(token, clubId), + onSuccess: async () => { + await invalidateRecruitmentQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + createEvent: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: ClubEventRequest) => postClubEvent(token, clubId, data), + onSuccess: async () => { + await invalidateEventListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + updateEvent: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: ({ eventId, data }: { eventId: number; data: ClubEventRequest }) => + putClubEvent(token, clubId, eventId, data), + onSuccess: async (_, variables) => { + await Promise.all([ + invalidateEventListQueries(queryClient, clubId), + queryClient.invalidateQueries({ queryKey: clubQueryKeys.eventDetail(clubId, variables.eventId) }), + ]); + await callbacks.onSuccess?.(); + }, + }), + + deleteEvent: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (eventId: number) => deleteClubEvent(token, clubId, eventId), + onSuccess: async (_, eventId) => { + await Promise.all([ + invalidateEventListQueries(queryClient, clubId), + queryClient.invalidateQueries({ queryKey: clubQueryKeys.eventDetail(clubId, eventId) }), + ]); + await callbacks.onSuccess?.(); + }, + }), + + mandateManager: (queryClient: QueryClient, token: string, clubId: number, callbacks: ClubMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (data: NewClubManager) => putNewClubManager(token, data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: clubQueryKeys.detailRoot(clubId) }); + await callbacks.onSuccess?.(); + }, + }), + + subscribeRecruitmentNotification: ( + queryClient: QueryClient, + token: string, + clubId: number, + callbacks: ClubMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: () => postClubRecruitmentNotification(token, clubId), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: clubQueryKeys.detailRoot(clubId) }); + await callbacks.onSuccess?.(); + }, + }), + + unsubscribeRecruitmentNotification: ( + queryClient: QueryClient, + token: string, + clubId: number, + callbacks: ClubMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: () => deleteClubRecruitmentNotification(token, clubId), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: clubQueryKeys.detailRoot(clubId) }); + await callbacks.onSuccess?.(); + }, + }), + + subscribeEventNotification: ( + queryClient: QueryClient, + token: string, + clubId: number, + callbacks: ClubMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: (eventId: number) => postClubEventNotification(token, clubId, eventId), + onSuccess: async () => { + await invalidateEventListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), + + unsubscribeEventNotification: ( + queryClient: QueryClient, + token: string, + clubId: number, + callbacks: ClubMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: (eventId: number) => deleteClubEventNotification(token, clubId, eventId), + onSuccess: async () => { + await invalidateEventListQueries(queryClient, clubId); + await callbacks.onSuccess?.(); + }, + }), +}; diff --git a/src/api/club/queries.ts b/src/api/club/queries.ts new file mode 100644 index 000000000..842de1f09 --- /dev/null +++ b/src/api/club/queries.ts @@ -0,0 +1,141 @@ +import { isKoinError } from '@bcsdlab/koin'; +import { queryOptions } from '@tanstack/react-query'; +import type { ClubRecruitmentResponse, HotClubResponse } from './entity'; +import { + getClubCategories, + getClubDetail, + getClubEventDetail, + getClubEventList, + getClubList, + getClubQnA, + getHotClub, + getRecruitmentClub, +} from './index'; + +const EMPTY_HOT_CLUB: HotClubResponse = { + club_id: -1, + name: '인기 동아리가 없어요', + image_url: '', +}; + +const EMPTY_RECRUITMENT: ClubRecruitmentResponse = { + id: 0, + status: 'NONE', + dday: 0, + start_date: '', + end_date: '', + image_url: '', + content: '', + is_manager: false, +}; + +interface ClubListQueryParams { + token?: string | null; + categoryId?: number; + sortType?: string; + isRecruiting?: boolean; + clubName?: string; +} + +type ClubViewerScope = 'auth' | 'guest'; + +const getViewerScope = (token?: string | null): ClubViewerScope => (token ? 'auth' : 'guest'); + +export const clubQueryKeys = { + all: ['club'] as const, + categories: (token?: string | null) => [...clubQueryKeys.all, 'categories', getViewerScope(token)] as const, + listRoot: () => [...clubQueryKeys.all, 'list'] as const, + list: ({ token, categoryId, sortType, isRecruiting, clubName }: ClubListQueryParams) => + [ + ...clubQueryKeys.listRoot(), + getViewerScope(token), + categoryId ?? null, + sortType ?? '', + Boolean(isRecruiting), + clubName ?? '', + ] as const, + hot: () => [...clubQueryKeys.all, 'hot'] as const, + detailRoot: (clubId?: number | string) => + clubId === undefined ? [...clubQueryKeys.all, 'detail'] as const : [...clubQueryKeys.all, 'detail', Number(clubId)] as const, + detail: (clubId: number, token?: string | null) => + [...clubQueryKeys.detailRoot(clubId), getViewerScope(token)] as const, + recruitment: (clubId: number) => [...clubQueryKeys.all, 'recruitment', clubId] as const, + eventListRoot: (clubId?: string | number) => + clubId === undefined + ? [...clubQueryKeys.all, 'event-list'] as const + : [...clubQueryKeys.all, 'event-list', clubId] as const, + eventList: (clubId: string | number, eventType: string, token?: string | null) => + [...clubQueryKeys.eventListRoot(clubId), eventType, getViewerScope(token)] as const, + eventDetail: (clubId: string | number, eventId: string | number) => + [...clubQueryKeys.all, 'event-detail', clubId, eventId] as const, + qna: (clubId: number | string, token?: string | null) => + [...clubQueryKeys.all, 'qna', clubId, getViewerScope(token)] as const, +}; + +export const clubQueries = { + categories: (token?: string | null) => + queryOptions({ + queryKey: clubQueryKeys.categories(token), + queryFn: () => getClubCategories(token ?? undefined), + }), + + list: ({ token, categoryId, sortType, isRecruiting, clubName }: ClubListQueryParams) => + queryOptions({ + queryKey: clubQueryKeys.list({ token, categoryId, sortType, isRecruiting, clubName }), + queryFn: () => getClubList(token ?? undefined, categoryId, sortType, isRecruiting, clubName), + }), + + hot: () => + queryOptions({ + queryKey: clubQueryKeys.hot(), + queryFn: async () => { + try { + return await getHotClub(); + } catch (error) { + if (isKoinError(error) && error.status === 404) { + return EMPTY_HOT_CLUB; + } + throw error; + } + }, + }), + + detail: (clubId: number, token?: string | null) => + queryOptions({ + queryKey: clubQueryKeys.detail(clubId, token), + queryFn: () => getClubDetail(token ?? '', clubId), + }), + + recruitment: (clubId: number) => + queryOptions({ + queryKey: clubQueryKeys.recruitment(clubId), + queryFn: async () => { + try { + return await getRecruitmentClub(clubId); + } catch (error) { + if (isKoinError(error) && error.status === 404) { + return EMPTY_RECRUITMENT; + } + throw error; + } + }, + }), + + eventList: (clubId: string | number, eventType: 'RECENT' | 'ONGOING' | 'UPCOMING' | 'ENDED', token?: string | null) => + queryOptions({ + queryKey: clubQueryKeys.eventList(clubId, eventType, token), + queryFn: () => getClubEventList(clubId, eventType, token ?? undefined), + }), + + eventDetail: (clubId: string | number, eventId: string | number) => + queryOptions({ + queryKey: clubQueryKeys.eventDetail(clubId, eventId), + queryFn: () => getClubEventDetail(clubId, eventId), + }), + + qna: (clubId: number | string, token?: string | null) => + queryOptions({ + queryKey: clubQueryKeys.qna(clubId, token), + queryFn: () => getClubQnA(token ?? '', Number(clubId)), + }), +}; diff --git a/src/api/coopshop/queries.ts b/src/api/coopshop/queries.ts new file mode 100644 index 000000000..5aab7eaa8 --- /dev/null +++ b/src/api/coopshop/queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getAllShopInfo, getCafeteriaInfo } from './index'; + +export const coopshopQueryKeys = { + all: ['coopshop'] as const, + allShopInfo: () => [...coopshopQueryKeys.all, 'all-shop-info'] as const, + cafeteriaInfo: () => [...coopshopQueryKeys.all, 'cafeteria-info'] as const, +}; + +export const coopshopQueries = { + allShopInfo: () => + queryOptions({ + queryKey: coopshopQueryKeys.allShopInfo(), + queryFn: getAllShopInfo, + }), + + cafeteriaInfo: () => + queryOptions({ + queryKey: coopshopQueryKeys.cafeteriaInfo(), + queryFn: getCafeteriaInfo, + }), +}; diff --git a/src/api/course/queries.ts b/src/api/course/queries.ts new file mode 100644 index 000000000..e2eb8e7c9 --- /dev/null +++ b/src/api/course/queries.ts @@ -0,0 +1,24 @@ +import { queryOptions } from '@tanstack/react-query'; +import { CourseRequestParams } from './entity'; +import { getCourseSearch, getPreCourseList } from './index'; + +export const courseQueryKeys = { + all: ['course'] as const, + search: (params: CourseRequestParams) => [...courseQueryKeys.all, 'search', params] as const, + preCourseList: (timetableFrameId: number) => [...courseQueryKeys.all, 'pre-course-list', timetableFrameId] as const, +}; + +export const courseQueries = { + search: (params: CourseRequestParams) => + queryOptions({ + queryKey: courseQueryKeys.search(params), + queryFn: () => getCourseSearch(params.name || undefined, params.department || undefined, params.year, params.semester), + }), + + preCourseList: (token: string, timetableFrameId: number) => + queryOptions({ + queryKey: courseQueryKeys.preCourseList(timetableFrameId), + queryFn: () => getPreCourseList(token, timetableFrameId), + gcTime: 0, + }), +}; diff --git a/src/api/dept/queries.ts b/src/api/dept/queries.ts new file mode 100644 index 000000000..3e4bf2a45 --- /dev/null +++ b/src/api/dept/queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getDeptList, getDeptMajorList } from './index'; + +export const deptQueryKeys = { + all: ['dept'] as const, + list: () => [...deptQueryKeys.all, 'list'] as const, + majorList: () => [...deptQueryKeys.all, 'major-list'] as const, +}; + +export const deptQueries = { + list: () => + queryOptions({ + queryKey: deptQueryKeys.list(), + queryFn: getDeptList, + }), + + majorList: () => + queryOptions({ + queryKey: deptQueryKeys.majorList(), + queryFn: getDeptMajorList, + }), +}; diff --git a/src/api/graduationCalculator/queries.ts b/src/api/graduationCalculator/queries.ts new file mode 100644 index 000000000..c66d33492 --- /dev/null +++ b/src/api/graduationCalculator/queries.ts @@ -0,0 +1,40 @@ +import { queryOptions } from '@tanstack/react-query'; +import { Semester } from './entity'; +import { calculateGraduationCredits, getCourseType, getGeneralEducation } from './index'; + +export const graduationCalculatorQueryKeys = { + all: ['graduation-calculator'] as const, + creditsByCourseType: ['graduation-calculator', 'credits-by-course-type'] as const, + generalEducation: ['graduation-calculator', 'general-education'] as const, + courseType: (semester: Semester, name: string, generalEducationArea?: string) => + [ + 'graduation-calculator', + 'course-type', + { + year: semester.year, + term: semester.term, + name, + generalEducationArea: generalEducationArea ?? '', + }, + ] as const, +}; + +export const graduationCalculatorQueries = { + creditsByCourseType: (token: string) => + queryOptions({ + queryKey: graduationCalculatorQueryKeys.creditsByCourseType, + queryFn: () => calculateGraduationCredits(token), + }), + + generalEducation: (token: string) => + queryOptions({ + queryKey: graduationCalculatorQueryKeys.generalEducation, + queryFn: () => getGeneralEducation(token), + }), + + courseType: (token: string, semester: Semester, name: string, generalEducationArea?: string) => + queryOptions({ + queryKey: graduationCalculatorQueryKeys.courseType(semester, name, generalEducationArea), + queryFn: () => getCourseType(token, semester, name, generalEducationArea), + }), +}; diff --git a/src/api/index.ts b/src/api/index.ts index 42ce63eb2..45e02ecf6 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -13,3 +13,4 @@ export * as store from './store'; export * as timetable from './timetable'; export * as uploadFile from './uploadFile'; export * as graduationCalculator from './graduationCalculator'; +export * as callvan from './callvan'; diff --git a/src/api/review/mutations.ts b/src/api/review/mutations.ts new file mode 100644 index 000000000..31eb43df8 --- /dev/null +++ b/src/api/review/mutations.ts @@ -0,0 +1,43 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { storeQueryKeys } from 'api/store/queries'; +import { ReviewRequest } from './entity'; +import { postStoreReview, putStoreReview } from './index'; + +interface ReviewMutationCallbacks { + onSuccess?: () => void | Promise; +} + +const invalidateStoreReviewQueries = async (queryClient: QueryClient, shopId: string) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: storeQueryKeys.reviews(Number(shopId)) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.myReviews(shopId) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.detail(shopId) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.detailPage(shopId) }), + ]); +}; + +export const reviewMutations = { + add: (queryClient: QueryClient, token: string, shopId: string, callbacks: ReviewMutationCallbacks = {}) => + mutationOptions({ + mutationFn: (reviewData: ReviewRequest) => postStoreReview(token, shopId, reviewData), + onSuccess: async () => { + await invalidateStoreReviewQueries(queryClient, shopId); + await callbacks.onSuccess?.(); + }, + }), + + edit: ( + queryClient: QueryClient, + token: string, + shopId: string, + reviewId: string, + callbacks: ReviewMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: (reviewData: ReviewRequest) => putStoreReview(token, shopId, reviewId, reviewData), + onSuccess: async () => { + await invalidateStoreReviewQueries(queryClient, shopId); + await callbacks.onSuccess?.(); + }, + }), +}; diff --git a/src/api/review/queries.ts b/src/api/review/queries.ts new file mode 100644 index 000000000..79cb58b75 --- /dev/null +++ b/src/api/review/queries.ts @@ -0,0 +1,15 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getStoreReview } from './index'; + +export const reviewQueryKeys = { + all: ['review'] as const, + detail: (shopId: string, reviewId: string) => [...reviewQueryKeys.all, Number(shopId), reviewId] as const, +}; + +export const reviewQueries = { + detail: (token: string, shopId: string, reviewId: string) => + queryOptions({ + queryKey: reviewQueryKeys.detail(shopId, reviewId), + queryFn: () => getStoreReview(token, shopId, reviewId), + }), +}; diff --git a/src/api/room/queries.ts b/src/api/room/queries.ts new file mode 100644 index 000000000..de1ee9892 --- /dev/null +++ b/src/api/room/queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query'; +import { getRoomDetailInfo, getRoomList } from './index'; + +export const roomQueryKeys = { + all: ['room'] as const, + list: () => [...roomQueryKeys.all, 'list'] as const, + detail: (id: string) => [...roomQueryKeys.all, 'detail', id] as const, +}; + +export const roomQueries = { + list: () => + queryOptions({ + queryKey: roomQueryKeys.list(), + queryFn: getRoomList, + }), + + detail: (id: string) => + queryOptions({ + queryKey: roomQueryKeys.detail(id), + queryFn: () => getRoomDetailInfo(id), + }), +}; diff --git a/src/api/store/mutations.ts b/src/api/store/mutations.ts new file mode 100644 index 000000000..921e75494 --- /dev/null +++ b/src/api/store/mutations.ts @@ -0,0 +1,49 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { ReviewReportRequest } from './entity'; +import { storeQueryKeys } from './queries'; +import { deleteReview, postReviewReport } from './index'; + +interface StoreMutationCallbacks { + onSuccess?: () => void | Promise; +} + +const invalidateStoreReviewQueries = async (queryClient: QueryClient, shopId: string) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: storeQueryKeys.reviews(Number(shopId)) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.myReviews(shopId) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.detail(shopId) }), + queryClient.invalidateQueries({ queryKey: storeQueryKeys.detailPage(shopId) }), + ]); +}; + +export const storeMutations = { + deleteReview: ( + queryClient: QueryClient, + reviewId: number, + shopId: string, + token: string, + callbacks: StoreMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: () => deleteReview(reviewId, shopId, token), + onSuccess: async () => { + await invalidateStoreReviewQueries(queryClient, shopId); + await callbacks.onSuccess?.(); + }, + }), + + reportReview: ( + queryClient: QueryClient, + shopId: string, + reviewId: string, + token: string, + callbacks: StoreMutationCallbacks = {}, + ) => + mutationOptions({ + mutationFn: (data: ReviewReportRequest) => postReviewReport(Number(shopId), Number(reviewId), data, token), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: storeQueryKeys.reviews(Number(shopId)) }); + await callbacks.onSuccess?.(); + }, + }), +}; diff --git a/src/api/store/queries.ts b/src/api/store/queries.ts new file mode 100644 index 000000000..b8db76244 --- /dev/null +++ b/src/api/store/queries.ts @@ -0,0 +1,130 @@ +import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; +import { StoreFilterType, StoreSorterType } from './entity'; +import { + getAllEvent, + getMyReview, + getRelateSearch, + getReviewList, + getStoreBenefitCategory, + getStoreBenefitList, + getStoreCategories, + getStoreDetailInfo, + getStoreDetailMenu, + getStoreEventList, + getStoreListV2, +} from './index'; + +interface StoreListQueryParams { + sorter: StoreSorterType; + filter: StoreFilterType[]; + query?: string; +} + +interface StoreReviewListQueryParams { + shopId: number; + page: number; + sorter: string; + token?: string; +} + +export const storeQueryKeys = { + all: ['store'] as const, + categories: () => [...storeQueryKeys.all, 'categories'] as const, + listV2: ({ sorter, filter, query }: StoreListQueryParams) => + [...storeQueryKeys.all, 'list-v2', { sorter, filter, query: query ?? '' }] as const, + allEvents: () => [...storeQueryKeys.all, 'all-events'] as const, + detail: (id: string) => [...storeQueryKeys.all, 'detail', id] as const, + detailMenu: (id: string) => [...storeQueryKeys.all, 'detail-menu', id] as const, + detailPage: (id: string) => [...storeQueryKeys.all, 'detail-page', id] as const, + eventList: (id: string) => [...storeQueryKeys.all, 'event-list', id] as const, + benefitCategory: () => [...storeQueryKeys.all, 'benefit-category'] as const, + benefitList: (id: string) => [...storeQueryKeys.all, 'benefit-list', id] as const, + relatedSearch: (query: string) => [...storeQueryKeys.all, 'related-search', query] as const, + reviews: (shopId: number) => ['review', shopId] as const, + reviewFeed: (shopId: number, sorter: string) => [...storeQueryKeys.reviews(shopId), sorter] as const, + reviewList: ({ shopId, page, sorter }: Omit) => + [...storeQueryKeys.reviewFeed(shopId, sorter), page] as const, + myReviews: (shopId: string) => ['review', 'my-review', shopId] as const, + myReview: (shopId: string, sorter: string) => [...storeQueryKeys.myReviews(shopId), sorter] as const, +}; + +export const storeQueries = { + categories: () => + queryOptions({ + queryKey: storeQueryKeys.categories(), + queryFn: getStoreCategories, + }), + + listV2: ({ sorter, filter, query }: StoreListQueryParams) => + queryOptions({ + queryKey: storeQueryKeys.listV2({ sorter, filter, query }), + queryFn: () => getStoreListV2(sorter, filter, query), + }), + + allEvents: () => + queryOptions({ + queryKey: storeQueryKeys.allEvents(), + queryFn: getAllEvent, + }), + + detail: (id: string) => + queryOptions({ + queryKey: storeQueryKeys.detail(id), + queryFn: () => getStoreDetailInfo(id), + }), + + detailMenu: (id: string) => + queryOptions({ + queryKey: storeQueryKeys.detailMenu(id), + queryFn: () => getStoreDetailMenu(id), + }), + + eventList: (id: string) => + queryOptions({ + queryKey: storeQueryKeys.eventList(id), + queryFn: () => getStoreEventList(id), + }), + + benefitCategory: () => + queryOptions({ + queryKey: storeQueryKeys.benefitCategory(), + queryFn: getStoreBenefitCategory, + }), + + benefitList: (id: string) => + queryOptions({ + queryKey: storeQueryKeys.benefitList(id), + queryFn: () => getStoreBenefitList(id), + }), + + relatedSearch: (query: string) => + queryOptions({ + queryKey: storeQueryKeys.relatedSearch(query), + queryFn: () => getRelateSearch(query), + }), + + reviewList: ({ shopId, page, sorter, token }: StoreReviewListQueryParams) => + queryOptions({ + queryKey: storeQueryKeys.reviewList({ shopId, page, sorter }), + queryFn: () => getReviewList(shopId, page, sorter, token), + }), + + reviewFeed: ({ shopId, sorter, token }: Omit) => + infiniteQueryOptions({ + queryKey: storeQueryKeys.reviewFeed(shopId, sorter), + initialPageParam: 1, + queryFn: ({ pageParam }) => getReviewList(shopId, pageParam, sorter, token), + getNextPageParam: (lastPage) => { + if (lastPage.total_page > lastPage.current_page) { + return lastPage.current_page + 1; + } + return undefined; + }, + }), + + myReview: (shopId: string, sorter: string, token: string) => + queryOptions({ + queryKey: storeQueryKeys.myReview(shopId, sorter), + queryFn: () => getMyReview(shopId, sorter, token), + }), +}; diff --git a/src/api/timetable/mutations.ts b/src/api/timetable/mutations.ts new file mode 100644 index 000000000..9bec0ecf6 --- /dev/null +++ b/src/api/timetable/mutations.ts @@ -0,0 +1,145 @@ +import { mutationOptions, QueryClient } from '@tanstack/react-query'; +import { graduationCalculatorQueryKeys } from 'api/graduationCalculator/queries'; +import { + AddTimetableFrameRequest, + AddTimetableLectureCustomRequest, + AddTimetableLectureRegularRequest, + RollbackTimetableLectureRequest, + Semester, + TimetableCustomLecture, + TimetableFrameInfo, + TimetableRegularLecture, +} from './entity'; +import { timetableQueryKeys } from './queries'; +import { + addTimetableFrame, + addTimetableLectureCustom, + addTimetableLectureRegular, + deleteSemester, + deleteTimetableFrame, + deleteTimetableLecture, + editTimetableFrame, + editTimetableLectureCustom, + editTimetableLectureRegular, + rollbackTimetableFrame, + rollbackTimetableLecture, +} from './index'; + +type DeleteTimetableFrameVariables = { + id: number; +}; + +type EditTimetableLectureRegularVariables = { + timetableFrameId: number; + editedLecture: TimetableRegularLecture; + token: string; +}; + +type EditTimetableLectureCustomVariables = { + timetableFrameId: number; + editedLecture: TimetableCustomLecture; + token: string; +}; + +const invalidateFrameList = (queryClient: QueryClient, semester: Semester) => + queryClient.invalidateQueries({ queryKey: timetableQueryKeys.frameList(semester) }); + +export const timetableMutations = { + addSemester: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: (data: AddTimetableFrameRequest) => addTimetableFrame(data, token), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: timetableQueryKeys.mySemester() }); + await invalidateFrameList(queryClient, semester); + }, + }), + + addFrame: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: (data: AddTimetableFrameRequest) => addTimetableFrame(data, token), + onSuccess: () => invalidateFrameList(queryClient, semester), + }), + + updateFrame: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: (frameInfo: TimetableFrameInfo) => + editTimetableFrame(token, frameInfo.id!, { name: frameInfo.name, is_main: frameInfo.is_main }), + onSuccess: () => invalidateFrameList(queryClient, semester), + }), + + deleteFrame: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: ({ id }: DeleteTimetableFrameVariables) => deleteTimetableFrame(token, id), + onSuccess: () => invalidateFrameList(queryClient, semester), + }), + + rollbackFrame: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: (timetableFrameId: number) => rollbackTimetableFrame(token, timetableFrameId), + onSuccess: () => invalidateFrameList(queryClient, semester), + }), + + deleteSemester: (queryClient: QueryClient, token: string, semester: Semester) => + mutationOptions({ + mutationFn: () => deleteSemester(token, semester), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: timetableQueryKeys.mySemester() }); + await invalidateFrameList(queryClient, semester); + await queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); + }, + }), + + addLectureRegular: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (data: AddTimetableLectureRegularRequest) => addTimetableLectureRegular(data, token), + onSuccess: (data, variables) => { + queryClient.setQueryData(timetableQueryKeys.lectureInfo(variables.timetable_frame_id), data); + }, + }), + + addLectureCustom: (queryClient: QueryClient, token: string) => + mutationOptions({ + mutationFn: (data: AddTimetableLectureCustomRequest) => addTimetableLectureCustom(data, token), + onSuccess: (data, variables) => { + queryClient.setQueryData(timetableQueryKeys.lectureInfo(variables.timetable_frame_id), data); + }, + }), + + editLectureRegular: (queryClient: QueryClient) => + mutationOptions({ + mutationFn: ({ timetableFrameId, editedLecture, token }: EditTimetableLectureRegularVariables) => + editTimetableLectureRegular( + { timetable_frame_id: timetableFrameId, timetable_lecture: editedLecture }, + token, + ), + onSuccess: async (data, variables) => { + queryClient.setQueryData(timetableQueryKeys.lectureInfo(variables.timetableFrameId), data); + await queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); + await queryClient.invalidateQueries({ queryKey: timetableQueryKeys.allLectures }); + }, + }), + + editLectureCustom: (queryClient: QueryClient) => + mutationOptions({ + mutationFn: ({ timetableFrameId, editedLecture, token }: EditTimetableLectureCustomVariables) => + editTimetableLectureCustom({ timetable_frame_id: timetableFrameId, timetable_lecture: editedLecture }, token), + onSuccess: (data, variables) => { + queryClient.setQueryData(timetableQueryKeys.lectureInfo(variables.timetableFrameId), data); + }, + }), + + deleteLecture: (queryClient: QueryClient, authorization: string) => + mutationOptions({ + mutationFn: (id: number) => deleteTimetableLecture(authorization, id), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: timetableQueryKeys.lectureInfoAll }); + await queryClient.invalidateQueries({ queryKey: graduationCalculatorQueryKeys.all }); + }, + }), + + rollbackLecture: (queryClient: QueryClient, token: string, timetableFrameId: number) => + mutationOptions({ + mutationFn: (data: RollbackTimetableLectureRequest) => rollbackTimetableLecture(data, token), + onSuccess: () => queryClient.invalidateQueries({ queryKey: timetableQueryKeys.lectureInfo(timetableFrameId) }), + }), +}; diff --git a/src/api/timetable/queries.ts b/src/api/timetable/queries.ts new file mode 100644 index 000000000..9269f0101 --- /dev/null +++ b/src/api/timetable/queries.ts @@ -0,0 +1,109 @@ +import { queryOptions } from '@tanstack/react-query'; +import { Semester, TimetableFrameListResponse, VersionType } from './entity'; +import { + getLectureList, + getMySemester, + getSemesterInfoList, + getTimetableAllLectureInfo, + getTimetableFrame, + getTimetableLectureInfo, + getVersion, +} from './index'; + +const MY_SEMESTER_INFO_KEY = 'my_semester'; +const SEMESTER_INFO_KEY = 'semester'; +const LECTURE_LIST_KEY = 'lecture'; +const TIMETABLE_FRAME_KEY = 'timetable_frame'; +const TIMETABLE_INFO_LIST = 'TIMETABLE_INFO_LIST'; +const ALL_LECTURES_KEY = 'allLectures'; + +type TimetableUserType = 'STUDENT' | 'GENERAL' | '' | null; + +type MySemesterQueryParams = { + userType?: TimetableUserType; +}; + +type FrameListQueryParams = { + fallbackOnError?: boolean; + userType?: TimetableUserType; +}; + +const canUseStudentTimetableQuery = (token: string, userType?: TimetableUserType) => + Boolean(token) && (!userType || userType === 'STUDENT'); + +export const createDefaultTimetableFrameList = (): TimetableFrameListResponse => [ + { + id: null, + name: '기본 시간표', + is_main: true, + }, +]; + +export const timetableQueryKeys = { + mySemester: () => [MY_SEMESTER_INFO_KEY] as const, + semesterInfo: () => [SEMESTER_INFO_KEY] as const, + lectureList: (semester: Semester) => [LECTURE_LIST_KEY, semester] as const, + frameList: (semester: Semester) => [`${TIMETABLE_FRAME_KEY}${semester.year}${semester.term}`] as const, + lectureInfoAll: [TIMETABLE_INFO_LIST] as const, + lectureInfo: (timetableFrameId: number) => [TIMETABLE_INFO_LIST, timetableFrameId] as const, + allLectures: [ALL_LECTURES_KEY] as const, + version: (type: VersionType) => [type] as const, +}; + +export const timetableQueries = { + mySemester: (token: string, { userType }: MySemesterQueryParams = {}) => + queryOptions({ + queryKey: timetableQueryKeys.mySemester(), + queryFn: () => (canUseStudentTimetableQuery(token, userType) ? getMySemester(token) : null), + }), + + semesterInfo: () => + queryOptions({ + queryKey: timetableQueryKeys.semesterInfo(), + queryFn: getSemesterInfoList, + }), + + lectureList: (semester: Semester) => + queryOptions({ + queryKey: timetableQueryKeys.lectureList(semester), + queryFn: () => getLectureList(semester), + }), + + frameList: (token: string, semester: Semester, { fallbackOnError = false, userType }: FrameListQueryParams = {}) => + queryOptions({ + queryKey: timetableQueryKeys.frameList(semester), + queryFn: async () => { + if (!canUseStudentTimetableQuery(token, userType)) { + return createDefaultTimetableFrameList(); + } + + if (!fallbackOnError) { + return getTimetableFrame(token, semester); + } + + try { + return await getTimetableFrame(token, semester); + } catch { + return createDefaultTimetableFrameList(); + } + }, + }), + + lectureInfo: (authorization: string, timetableFrameId: number) => + queryOptions({ + queryKey: timetableQueryKeys.lectureInfo(timetableFrameId), + queryFn: () => (authorization ? getTimetableLectureInfo(authorization, timetableFrameId) : null), + }), + + allLectures: (token: string) => + queryOptions({ + queryKey: timetableQueryKeys.allLectures, + queryFn: () => (token ? getTimetableAllLectureInfo(token) : null), + }), + + version: (type: VersionType) => + queryOptions({ + queryKey: timetableQueryKeys.version(type), + queryFn: () => getVersion(type), + }), +}; diff --git a/src/api/uploadFile/entity.ts b/src/api/uploadFile/entity.ts index b85b8f909..a7529370d 100644 --- a/src/api/uploadFile/entity.ts +++ b/src/api/uploadFile/entity.ts @@ -1,6 +1,6 @@ import { APIResponse } from 'interfaces/APIResponse'; -export type UploadDomain = 'SHOPS' | 'LOST_ITEMS' | 'CLUB'; +export type UploadDomain = 'SHOPS' | 'LOST_ITEMS' | 'CLUB' | 'CALLVAN_REPORT' | 'CALLVAN_CHAT'; export interface UploadImage extends APIResponse { file_url: string; diff --git a/src/assets/svg/Articles/filter.svg b/src/assets/svg/Articles/filter.svg new file mode 100644 index 000000000..a44abe060 --- /dev/null +++ b/src/assets/svg/Articles/filter.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/Articles/refresh.svg b/src/assets/svg/Articles/refresh.svg new file mode 100644 index 000000000..a57766ae5 --- /dev/null +++ b/src/assets/svg/Articles/refresh.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/svg/Articles/search.svg b/src/assets/svg/Articles/search.svg new file mode 100644 index 000000000..5ee507ae3 --- /dev/null +++ b/src/assets/svg/Articles/search.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/Callvan/arrow-back.svg b/src/assets/svg/Callvan/arrow-back.svg new file mode 100644 index 000000000..d78dc2c19 --- /dev/null +++ b/src/assets/svg/Callvan/arrow-back.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/car.svg b/src/assets/svg/Callvan/car.svg new file mode 100644 index 000000000..042681854 --- /dev/null +++ b/src/assets/svg/Callvan/car.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/Callvan/chat.svg b/src/assets/svg/Callvan/chat.svg new file mode 100644 index 000000000..05dc66030 --- /dev/null +++ b/src/assets/svg/Callvan/chat.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/Callvan/chevron-down.svg b/src/assets/svg/Callvan/chevron-down.svg new file mode 100644 index 000000000..7371c6ecd --- /dev/null +++ b/src/assets/svg/Callvan/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/chevron-right.svg b/src/assets/svg/Callvan/chevron-right.svg new file mode 100644 index 000000000..823c1e430 --- /dev/null +++ b/src/assets/svg/Callvan/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/filter.svg b/src/assets/svg/Callvan/filter.svg new file mode 100644 index 000000000..158cbbc11 --- /dev/null +++ b/src/assets/svg/Callvan/filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/image-upload.svg b/src/assets/svg/Callvan/image-upload.svg new file mode 100644 index 000000000..fabe4c68a --- /dev/null +++ b/src/assets/svg/Callvan/image-upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/notification-bell-read.svg b/src/assets/svg/Callvan/notification-bell-read.svg new file mode 100644 index 000000000..cfa5640a3 --- /dev/null +++ b/src/assets/svg/Callvan/notification-bell-read.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/notification-bell-unread.svg b/src/assets/svg/Callvan/notification-bell-unread.svg new file mode 100644 index 000000000..c8337a156 --- /dev/null +++ b/src/assets/svg/Callvan/notification-bell-unread.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/notification.svg b/src/assets/svg/Callvan/notification.svg new file mode 100644 index 000000000..8445e70e6 --- /dev/null +++ b/src/assets/svg/Callvan/notification.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svg/Callvan/people-gray.svg b/src/assets/svg/Callvan/people-gray.svg new file mode 100644 index 000000000..69af13c80 --- /dev/null +++ b/src/assets/svg/Callvan/people-gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/people-purple.svg b/src/assets/svg/Callvan/people-purple.svg new file mode 100644 index 000000000..3f0fdf6bd --- /dev/null +++ b/src/assets/svg/Callvan/people-purple.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/people.svg b/src/assets/svg/Callvan/people.svg new file mode 100644 index 000000000..825d02a9b --- /dev/null +++ b/src/assets/svg/Callvan/people.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/person-avatar.svg b/src/assets/svg/Callvan/person-avatar.svg new file mode 100644 index 000000000..d6cc45ecc --- /dev/null +++ b/src/assets/svg/Callvan/person-avatar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/Callvan/phone-calling.svg b/src/assets/svg/Callvan/phone-calling.svg new file mode 100644 index 000000000..73721605f --- /dev/null +++ b/src/assets/svg/Callvan/phone-calling.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/Callvan/phone.svg b/src/assets/svg/Callvan/phone.svg new file mode 100644 index 000000000..73721605f --- /dev/null +++ b/src/assets/svg/Callvan/phone.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/Callvan/route-indicator.svg b/src/assets/svg/Callvan/route-indicator.svg new file mode 100644 index 000000000..5970a80f1 --- /dev/null +++ b/src/assets/svg/Callvan/route-indicator.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/Callvan/search.svg b/src/assets/svg/Callvan/search.svg new file mode 100644 index 000000000..e282e0b23 --- /dev/null +++ b/src/assets/svg/Callvan/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/Callvan/send.svg b/src/assets/svg/Callvan/send.svg new file mode 100644 index 000000000..3b8b1109f --- /dev/null +++ b/src/assets/svg/Callvan/send.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/Callvan/siren.svg b/src/assets/svg/Callvan/siren.svg new file mode 100644 index 000000000..1657d3679 --- /dev/null +++ b/src/assets/svg/Callvan/siren.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svg/Callvan/sleeping-planet.svg b/src/assets/svg/Callvan/sleeping-planet.svg new file mode 100644 index 000000000..ab720f80e --- /dev/null +++ b/src/assets/svg/Callvan/sleeping-planet.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svg/Callvan/spin.svg b/src/assets/svg/Callvan/spin.svg new file mode 100644 index 000000000..fbb659e21 --- /dev/null +++ b/src/assets/svg/Callvan/spin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/Callvan/swap.svg b/src/assets/svg/Callvan/swap.svg new file mode 100644 index 000000000..69114a772 --- /dev/null +++ b/src/assets/svg/Callvan/swap.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/svg/Callvan/three-dots-small.svg b/src/assets/svg/Callvan/three-dots-small.svg new file mode 100644 index 000000000..9ea4cadc0 --- /dev/null +++ b/src/assets/svg/Callvan/three-dots-small.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/Callvan/three-dots.svg b/src/assets/svg/Callvan/three-dots.svg new file mode 100644 index 000000000..e205dc9ee --- /dev/null +++ b/src/assets/svg/Callvan/three-dots.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Articles/ArticlesPage/ArticlesPage.module.scss b/src/components/Articles/ArticlesPage/ArticlesPage.module.scss index 4879ca77e..81e835dd7 100644 --- a/src/components/Articles/ArticlesPage/ArticlesPage.module.scss +++ b/src/components/Articles/ArticlesPage/ArticlesPage.module.scss @@ -28,12 +28,16 @@ .header { width: 100%; - margin-bottom: 8px; + margin-bottom: 24px; position: relative; display: flex; justify-content: space-between; align-items: center; + @include media.media-breakpoint(mobile) { + margin: 4px 0; + } + &__title { font-family: Pretendard, sans-serif; font-size: 32px; @@ -45,50 +49,14 @@ display: none; } } +} - &__container { - display: flex; - gap: 20px; - } - - &__button-container { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - color: #4b4b4b; - border: 1px solid #e1e1e1; - background-color: #fafafa; - padding: 8px 12px; - border-radius: 999px; - } - - &__button-context { - color: #4b4b4b; - align-items: center; - line-height: normal; - font-family: Pretendard, sans-serif; - } - - &__writing-options { - display: flex; - gap: 20px; - align-items: center; - } - - &__option-button { - display: flex; - padding: 8px 12px; - border: 1px solid #e1e1e1; - background-color: #fafafa; - border-radius: 999px; - align-items: center; - gap: 8px; - } - - &__option-text { - font-family: Pretendard, sans-serif; - color: #4b4b4b; +.listScroll { + @include media.media-breakpoint(mobile) { + flex: 1 1 auto; + min-height: 0; + width: 100%; + overflow-y: auto; } } @@ -96,4 +64,8 @@ display: flex; flex-direction: column; align-items: center; + + @include media.media-breakpoint(mobile) { + display: none; + } } diff --git a/src/components/Articles/ArticlesPage/index.tsx b/src/components/Articles/ArticlesPage/index.tsx index 0f584866d..d943da22b 100644 --- a/src/components/Articles/ArticlesPage/index.tsx +++ b/src/components/Articles/ArticlesPage/index.tsx @@ -1,33 +1,26 @@ import Link from 'next/link'; import HotArticles from 'components/Articles/components/HotArticle'; -import LostItemRouteButton from 'components/Articles/components/LostItemRouteButton'; import ROUTES from 'static/routes'; +import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useScrollToTop from 'utils/hooks/ui/useScrollToTop'; -// import { useUser } from 'utils/hooks/state/useUser'; -// import { useArticlesLogger } from 'pages/Articles/hooks/useArticlesLogger'; import styles from './ArticlesPage.module.scss'; export default function ArticlesPageLayout({ children }: { children: React.ReactNode }) { useScrollToTop(); - // const { pathname } = useLocation(); - // const isBoard = pathname.endsWith(ROUTES.Articles()); - // const { data: userInfo } = useUser(); - // const isCouncil = userInfo && userInfo.student_number === '2022136000'; - // const { logItemWriteClick } = useArticlesLogger(); + const isMobile = useMediaQuery(); return (
-
- -

공지사항

- -
- - {/* isBoard && isCouncil && */} -
- {children} + {!isMobile && ( +
+ +

공지사항

+ +
+ )} +
{children}
diff --git a/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts b/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts index 0a252b183..5135715c9 100644 --- a/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts +++ b/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postBlockLostItemChatroom } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -10,12 +10,12 @@ const useDeleteLostItemChatroom = () => { const token = useTokenState(); const queryClient = useQueryClient(); const router = useRouter(); + const mutation = articleMutations.blockLostItemChatroom(queryClient, token); const { mutate } = useMutation({ - mutationFn: ({ articleId, chatroomId }: { articleId: number; chatroomId: number }) => - postBlockLostItemChatroom(token, articleId, chatroomId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['chatroom'] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '채팅방이 차단되었습니다.'); router.push(ROUTES.LostItemChat()); }, diff --git a/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts b/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts new file mode 100644 index 000000000..9dab73860 --- /dev/null +++ b/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts @@ -0,0 +1,197 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { isKoinError, sendClientError } from '@bcsdlab/koin'; +import { + keepPreviousData, + skipToken, + useMutation, + useQuery, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; +import { postLeaveLostItemChatroomV2, postLostItemChatroomMessageV2 } from 'api/articles'; +import { articleQueries, articleQueryKeys } from 'api/articles/queries'; +import { getCachedMessages, cacheMessages, clearChatroomCache } from 'utils/db/chatDB'; +import showToast from 'utils/ts/showToast'; + +const POLLING_INTERVAL_MS = 10_000; + +interface UseChatPollingOptions { + token: string; + articleId: number | string | null; + chatroomId: number | string | null; + isOnline?: boolean; +} + +const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseChatPollingOptions) => { + const queryClient = useQueryClient(); + + const { data: chatroomList } = useSuspenseQuery({ + ...articleQueries.lostItemChatroomList(token), + staleTime: isOnline ? 0 : Infinity, + refetchInterval: isOnline ? POLLING_INTERVAL_MS : false, + refetchIntervalInBackground: false, + }); + + const defaultChatroomId = chatroomId ?? chatroomList?.[0]?.chat_room_id ?? null; + const defaultArticleId = articleId ?? chatroomList?.[0]?.article_id ?? null; + + const numericArticleId = defaultArticleId != null ? Number(defaultArticleId) : null; + const numericChatroomId = defaultChatroomId != null ? Number(defaultChatroomId) : null; + + // indexedDB 진입 + useEffect(() => { + if (numericArticleId == null || numericChatroomId == null) return; + + const queryKey = articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId); + const existing = queryClient.getQueryData(queryKey); + if (existing) return; + + getCachedMessages(numericArticleId, numericChatroomId).then((cached) => { + if (cached && cached.length > 0) { + queryClient.setQueryData(queryKey, cached); + } + }); + }, [queryClient, numericArticleId, numericChatroomId, defaultArticleId, defaultChatroomId]); + + const { data: chatroomDetail } = useQuery({ + ...(defaultArticleId && defaultChatroomId && isOnline + ? articleQueries.lostItemChatroomDetail(token, Number(defaultArticleId), Number(defaultChatroomId)) + : { + queryKey: articleQueryKeys.lostItemChatroomDetail(defaultArticleId, defaultChatroomId), + queryFn: skipToken, + }), + placeholderData: keepPreviousData, + }); + + const { data: messages } = useQuery({ + ...(defaultArticleId && defaultChatroomId && isOnline + ? articleQueries.lostItemChatroomMessages(token, Number(defaultArticleId), Number(defaultChatroomId)) + : { + queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId), + queryFn: skipToken, + }), + placeholderData: keepPreviousData, + refetchInterval: isOnline && defaultArticleId && defaultChatroomId ? POLLING_INTERVAL_MS : false, + refetchIntervalInBackground: false, + }); + + // 폴링 응답 도착 시 indexedDB 캐시 + useEffect(() => { + if (messages && messages.length > 0 && numericArticleId != null && numericChatroomId != null) { + cacheMessages(numericArticleId, numericChatroomId, messages); + } + }, [messages, numericArticleId, numericChatroomId]); + + const { mutate: sendMessage } = useMutation({ + mutationFn: ({ content, isImage = false }: { content: string; isImage?: boolean }) => { + if (defaultArticleId == null || defaultChatroomId == null) { + return Promise.reject(new Error('채팅방 정보가 없습니다.')); + } + + return postLostItemChatroomMessageV2(token, Number(defaultArticleId), Number(defaultChatroomId), { + content, + is_image: isImage, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId), + }); + }, + onError: (error) => { + if (isKoinError(error)) { + showToast('error', error.message || '메시지 전송을 실패하였습니다'); + } else { + showToast('error', '메시지 전송을 실패하였습니다'); + sendClientError(error); + } + }, + }); + + const { mutateAsync: leaveChatroom } = useMutation({ + mutationFn: () => { + if (defaultArticleId == null || defaultChatroomId == null) { + return Promise.reject(new Error('채팅방 정보가 없습니다.')); + } + + return postLeaveLostItemChatroomV2(token, Number(defaultArticleId), Number(defaultChatroomId)); + }, + onSuccess: () => { + if (numericArticleId != null && numericChatroomId != null) { + clearChatroomCache(numericArticleId, numericChatroomId); + } + queryClient.invalidateQueries({ + queryKey: articleQueryKeys.lostItemChatroomList, + }); + }, + onError: (error) => { + if (isKoinError(error)) { + showToast('error', error.message || '채팅방 퇴장을 실패하였습니다'); + } else { + showToast('error', '채팅방 퇴장을 실패하였습니다'); + sendClientError(error); + } + }, + }); + + const leaveRoom = useCallback( + (aId: number, cId: number) => { + postLeaveLostItemChatroomV2(token, aId, cId).catch((error) => { + if (isKoinError(error)) { + showToast('error', error.message || '채팅방 퇴장을 실패하였습니다'); + } else { + showToast('error', '채팅방 퇴장을 실패하였습니다'); + sendClientError(error); + } + }); + }, + [token], + ); + + const prevRoomRef = useRef<{ articleId: number; chatroomId: number } | null>(null); + + useEffect(() => { + if (numericArticleId != null && numericChatroomId != null) { + if ( + prevRoomRef.current && + (prevRoomRef.current.articleId !== numericArticleId || prevRoomRef.current.chatroomId !== numericChatroomId) + ) { + leaveRoom(prevRoomRef.current.articleId, prevRoomRef.current.chatroomId); + } + prevRoomRef.current = { articleId: numericArticleId, chatroomId: numericChatroomId }; + } + + return () => { + if (prevRoomRef.current) { + leaveRoom(prevRoomRef.current.articleId, prevRoomRef.current.chatroomId); + prevRoomRef.current = null; + } + }; + }, [numericArticleId, numericChatroomId, leaveRoom]); + + const invalidateChatroomList = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: articleQueryKeys.lostItemChatroomList, + }); + }, [queryClient]); + + const invalidateMessages = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId), + }); + }, [queryClient, defaultArticleId, defaultChatroomId]); + + return { + chatroomList, + chatroomDetail, + messages, + defaultChatroomId, + defaultArticleId, + sendMessage, + leaveChatroom, + invalidateChatroomList, + invalidateMessages, + }; +}; + +export default useChatPolling; diff --git a/src/components/Articles/LostItemChatPage/hooks/useChatroomQuery.ts b/src/components/Articles/LostItemChatPage/hooks/useChatroomQuery.ts deleted file mode 100644 index 732ebce75..000000000 --- a/src/components/Articles/LostItemChatPage/hooks/useChatroomQuery.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useCallback } from 'react'; -import { keepPreviousData, skipToken, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; -import { getLostItemChatroomDetail, getLostItemChatroomDetailMessages, getLostItemChatroomList } from 'api/articles'; - -const useChatroomQuery = (token: string, articleId: number | string | null, chatroomId: number | string | null) => { - const queryClient = useQueryClient(); - - const { data: chatroomList } = useSuspenseQuery({ - queryKey: ['chatroom', 'lost-item', 'list'], - queryFn: () => getLostItemChatroomList(token), - }); - - const defaultChatroomId = chatroomId ?? chatroomList?.[0]?.chat_room_id ?? null; - const defaultArticleId = articleId ?? chatroomList?.[0]?.article_id ?? null; - - const { data: chatroomDetail } = useQuery({ - queryKey: ['chatroom', 'lost-item', 'detail', defaultArticleId, defaultChatroomId], - queryFn: - defaultArticleId && defaultChatroomId - ? () => getLostItemChatroomDetail(token, Number(defaultArticleId), Number(defaultChatroomId)) - : skipToken, - placeholderData: keepPreviousData, - }); - - const { data: messages } = useQuery({ - queryKey: ['chatroom', 'lost-item', 'messages', defaultArticleId, defaultChatroomId], - queryFn: - defaultArticleId && defaultChatroomId - ? () => getLostItemChatroomDetailMessages(token, Number(defaultArticleId), Number(defaultChatroomId)) - : skipToken, - placeholderData: keepPreviousData, - }); - - const invalidateChatroomList = useCallback(() => { - queryClient.invalidateQueries({ - queryKey: ['chatroom', 'lost-item', 'list'], - }); - }, [queryClient]); - - return { - chatroomList, - chatroomDetail, - messages, - defaultChatroomId, - defaultArticleId, - invalidateChatroomList, - }; -}; - -export default useChatroomQuery; diff --git a/src/components/Articles/LostItemDetailPage/components/DeleteModal/index.tsx b/src/components/Articles/LostItemDetailPage/components/DeleteModal/index.tsx index 54eee14bc..de8dc7767 100644 --- a/src/components/Articles/LostItemDetailPage/components/DeleteModal/index.tsx +++ b/src/components/Articles/LostItemDetailPage/components/DeleteModal/index.tsx @@ -20,7 +20,7 @@ export default function DeleteModal({ articleId, closeDeleteModal }: DeleteModal const router = useRouter(); const { logFindUserDeleteConfirmClick } = useArticlesLogger(); const { mutate: deleteArticle } = useDeleteLostItemArticle({ - onSuccess: () => router.replace(ROUTES.Articles()), + onSuccess: () => router.replace(ROUTES.LostItems()), }); useEscapeKeyDown({ onEscape: closeDeleteModal }); diff --git a/src/components/Articles/LostItemDetailPage/components/DisplayImage/index.tsx b/src/components/Articles/LostItemDetailPage/components/DisplayImage/index.tsx index 1ce0129d3..817997313 100644 --- a/src/components/Articles/LostItemDetailPage/components/DisplayImage/index.tsx +++ b/src/components/Articles/LostItemDetailPage/components/DisplayImage/index.tsx @@ -1,15 +1,15 @@ import { useState } from 'react'; import Image from 'next/image'; import { cn } from '@bcsdlab/utils'; +import { LostItemImageDTO } from 'api/articles/entity'; import ChevronLeft from 'assets/svg/Articles/chevron-left-circle.svg'; import ChevronRight from 'assets/svg/Articles/chevron-right-circle.svg'; import SelectedDotIcon from 'assets/svg/Articles/ellipse-blue.svg'; import NotSelectedDotIcon from 'assets/svg/Articles/ellipse-grey.svg'; -import { ArticleImage } from 'static/articles'; import styles from './DisplayImage.module.scss'; interface DisplayImageProps { - images: ArticleImage[]; + images: LostItemImageDTO[]; } export default function DisplayImage({ images }: DisplayImageProps) { @@ -24,7 +24,7 @@ export default function DisplayImage({ images }: DisplayImageProps) {
{images.length > 0 && (
- 분실물 이미지 + 분실물 이미지 ))} diff --git a/src/components/Articles/LostItemDetailPage/components/FoundChip/FoundChip.module.scss b/src/components/Articles/LostItemDetailPage/components/FoundChip/FoundChip.module.scss new file mode 100644 index 000000000..6f37bcc68 --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/FoundChip/FoundChip.module.scss @@ -0,0 +1,43 @@ +@use "src/utils/scss/media" as media; + +.chip { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 4px; + font-weight: 500; + background-color: #ffa928; + color: #fff; + padding: 4px 8px; + + &--found { + background-color: #f5f5f5; + color: #cacaca; + } + + &--xs { + width: 47px; + height: 21px; + font-size: 14px; + } + + &--small { + font-size: 18px; + height: 29px; + width: 64px; + } + + &--large { + font-weight: 700; + font-size: 26px; + height: 39px; + width: 84px; + } + + @include media.media-breakpoint(mobile) { + padding: 2px 4px; + font-size: 12px; + height: 23px; + width: 40px; + } +} diff --git a/src/components/Articles/LostItemDetailPage/components/FoundChip/index.tsx b/src/components/Articles/LostItemDetailPage/components/FoundChip/index.tsx new file mode 100644 index 000000000..0c9e9ee64 --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/FoundChip/index.tsx @@ -0,0 +1,23 @@ +import { cn } from '@bcsdlab/utils'; +import styles from './FoundChip.module.scss'; + +type ChipSize = 'xs' | 'small' | 'large'; + +interface FoundChipProps { + isFound: boolean; + size?: ChipSize; +} + +export default function FoundChip({ isFound, size = 'large' }: FoundChipProps) { + return ( +
+ {isFound ? '찾음' : '찾는중'} +
+ ); +} diff --git a/src/components/Articles/LostItemDetailPage/components/FoundModal/FoundModal.module.scss b/src/components/Articles/LostItemDetailPage/components/FoundModal/FoundModal.module.scss new file mode 100644 index 000000000..e8e561963 --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/FoundModal/FoundModal.module.scss @@ -0,0 +1,126 @@ +@use "src/utils/scss/media" as media; + +.background { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background: rgba(23 23 23 / 30%); +} + +.modal { + width: 431px; + margin-bottom: 100px; + padding: 32px 24px 24px; + border-radius: 16px; + background-color: #fff; + + @include media.media-breakpoint(mobile) { + width: 301px; + padding: 24px 32px; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + } + + &__title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 26px; + font-weight: 700; + text-align: center; + + @include media.media-breakpoint(mobile) { + height: auto; + font-size: 16px; + font-weight: 500; + line-height: 1.6; + } + } + + &__warning { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + margin-top: 8px; + font-size: 16px; + color: #f7941e; + line-height: 1.6; + + @include media.media-breakpoint(mobile) { + margin-top: 0; + font-size: 12px; + } + } + + &__buttons { + margin-top: 32px; + height: 105px; + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + + @include media.media-breakpoint(mobile) { + height: auto; + margin: 0; + flex-direction: row-reverse; + gap: 12px; + } + + & button { + width: 383px; + height: 50px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + gap: 8px; + line-height: 1.6; + + @include media.media-breakpoint(mobile) { + width: 105px; + height: 46px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + } + } + } +} + +.buttons { + &__confirm { + background-color: #175c8e; + font-size: 16px; + font-weight: 500; + color: #fff; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + + &__cancel { + background-color: #fff; + font-size: 14px; + font-weight: 400; + color: #000; + + @include media.media-breakpoint(mobile) { + border: 1px solid #cacaca; + color: #4b4b4b; + } + } +} diff --git a/src/components/Articles/LostItemDetailPage/components/FoundModal/index.tsx b/src/components/Articles/LostItemDetailPage/components/FoundModal/index.tsx new file mode 100644 index 000000000..9d2ff295f --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/FoundModal/index.tsx @@ -0,0 +1,56 @@ +import WarningIcon from 'assets/svg/Login/warning.svg'; +import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; +import { useBodyScrollLock } from 'utils/hooks/ui/useBodyScrollLock'; +import { useEscapeKeyDown } from 'utils/hooks/ui/useEscapeKeyDown'; +import { useOutsideClick } from 'utils/hooks/ui/useOutsideClick'; +import styles from './FoundModal.module.scss'; + +interface FoundModalProps { + onConfirm: () => void; + onClose: () => void; + isPending: boolean; +} + +export default function FoundModal({ onConfirm, onClose, isPending }: FoundModalProps) { + const isMobile = useMediaQuery(); + useEscapeKeyDown({ onEscape: onClose }); + useBodyScrollLock(); + const { backgroundRef } = useOutsideClick({ onOutsideClick: onClose }); + + const handleConfirmClick = () => { + onConfirm(); + onClose(); + }; + + return ( +
+
+ {isMobile ? ( +
+
상태 변경 시 되돌릴 수 없습니다.
+
찾음으로 변경하시겠습니까?
+
+ ) : ( + <> +
+
'찾음' 상태로
+
변경하시겠습니까?
+
+
+ +
상태 변경 시 되돌릴 수 없습니다.
+
+ + )} +
+ + +
+
+
+ ); +} diff --git a/src/components/Articles/LostItemDetailPage/components/FoundToggle/FoundToggle.module.scss b/src/components/Articles/LostItemDetailPage/components/FoundToggle/FoundToggle.module.scss new file mode 100644 index 000000000..ba419bfef --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/FoundToggle/FoundToggle.module.scss @@ -0,0 +1,63 @@ +@use "src/utils/scss/media" as media; + +.found-toggle { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; + width: 100%; + + @include media.media-breakpoint(mobile) { + gap: 8px; + } + + &__label { + font-size: 18px; + font-weight: 500; + color: #4b4b4b; + + @include media.media-breakpoint(mobile) { + font-size: 14px; + } + } + + &__button { + padding: 0; + background: none; + border: none; + cursor: pointer; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + &__track { + width: 44px; + height: 24px; + border-radius: 12px; + background-color: #e1e1e1; + position: relative; + transition: background-color 0.2s ease; + + &--active { + background-color: #ffa928; + } + } + + &__thumb { + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #fff; + transition: transform 0.2s ease; + + .found-toggle__track--active & { + transform: translateX(20px); + } + } +} diff --git a/src/components/Articles/LostItemDetailPage/components/FoundToggle/index.tsx b/src/components/Articles/LostItemDetailPage/components/FoundToggle/index.tsx new file mode 100644 index 000000000..0859b130f --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/FoundToggle/index.tsx @@ -0,0 +1,28 @@ +import styles from './FoundToggle.module.scss'; + +interface FoundToggleProps { + onToggle: () => void; + disabled?: boolean; + type: string; +} + +export default function FoundToggle({ onToggle, disabled = false, type }: FoundToggleProps) { + return ( +
+ + {type === 'FOUND' ? '주인을 찾았나요?' : '물건을 찾았나요?'} + + +
+ ); +} diff --git a/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/LatestLostItemList.module.scss b/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/LatestLostItemList.module.scss new file mode 100644 index 000000000..b195d4c80 --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/LatestLostItemList.module.scss @@ -0,0 +1,89 @@ +@use "src/utils/scss/media" as media; + +.container { + display: none; + + @include media.media-breakpoint(mobile) { + display: block; + width: 100%; + padding: 14px 24px; + box-sizing: border-box; + border-top: 6px solid #f5f5f5; + } +} + +.title { + font-weight: 600; + padding-bottom: 14px; +} + +.list { + display: flex; + flex-direction: column; +} + +.item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; + font-size: 12px; + font-weight: 500; + line-height: 160%; + + &__content { + display: flex; + gap: 8px; + flex: 1; + min-width: 0; + } + + &__type { + flex-shrink: 0; + color: #10477a; + font-weight: 600; + } + + &__info { + display: flex; + gap: 4px; + min-width: 0; + } + + &__category { + flex-shrink: 0; + padding: 0 8px; + border-radius: 999px; + background-color: #175c8e; + color: #fff; + } + + &__place { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__date { + flex-shrink: 0; + } +} + +.loading { + display: flex; + justify-content: center; + padding: 16px 0; + color: #727272; + font-size: 14px; +} + +.empty { + text-align: center; + padding: 24px 0; + color: #727272; + font-size: 14px; +} diff --git a/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/index.tsx b/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/index.tsx new file mode 100644 index 000000000..c92932104 --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/LatestLostItemList/index.tsx @@ -0,0 +1,57 @@ +import Link from 'next/link'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { articleQueries } from 'api/articles/queries'; +import FoundChip from 'components/Articles/LostItemDetailPage/components/FoundChip'; +import ROUTES from 'static/routes'; +import useTokenState from 'utils/hooks/state/useTokenState'; +import useInfiniteScroll from 'utils/hooks/ui/useInfiniteScroll'; +import styles from './LatestLostItemList.module.scss'; + +function LatestLostItemList() { + const token = useTokenState(); + const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useSuspenseInfiniteQuery( + articleQueries.lostItemInfiniteList(token, { + limit: 10, + sort: 'LATEST', + }), + ); + + const observerRef = useInfiniteScroll(fetchNextPage, hasNextPage, isFetchingNextPage); + const articles = data.pages.flatMap((page) => page.articles); + + return ( +
+
최근 게시물
+
+ {articles.length === 0 ? ( +
게시물이 없습니다.
+ ) : ( + articles.map((article) => ( + +
+ {article.type === 'LOST' ? '분실물' : '습득물'} +
+ {article.category} + {article.found_place} + | {article.found_date} +
+
+ + + )) + )} + {hasNextPage && ( +
+ {isFetchingNextPage && '불러오는 중...'} +
+ )} +
+
+ ); +} + +export default LatestLostItemList; diff --git a/src/components/Articles/LostItemDetailPage/components/LostItemSEO/index.tsx b/src/components/Articles/LostItemDetailPage/components/LostItemSEO/index.tsx new file mode 100644 index 000000000..108337046 --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/components/LostItemSEO/index.tsx @@ -0,0 +1,32 @@ +import Head from 'next/head'; +import { SingleLostItemArticleResponseDTO } from 'api/articles/entity'; + +interface LostItemSEOProps { + article: SingleLostItemArticleResponseDTO; +} + +const DEFAULT_IMAGE = 'https://static.koreatech.in/assets/img/facebook_showcase_image.png'; + +export default function LostItemSEO({ article }: LostItemSEOProps) { + const { type, category, found_place, content, images } = article; + + const typeLabel = type === 'FOUND' ? '[습득물]' : '[분실물]'; + const title = `${typeLabel} ${category} - ${found_place} | KOIN 분실물`; + const description = content || `${found_place}에서 ${type === 'FOUND' ? '습득' : '분실'}한 ${category}입니다.`; + const image = images?.[0]?.image_url || DEFAULT_IMAGE; + + return ( + + {title} + + + + + + + + + + + ); +} diff --git a/src/components/Articles/LostItemDetailPage/components/ReportForm/ReportForm.module.scss b/src/components/Articles/LostItemDetailPage/components/ReportForm/ReportForm.module.scss index 7b7bfc1ac..85bc78422 100644 --- a/src/components/Articles/LostItemDetailPage/components/ReportForm/ReportForm.module.scss +++ b/src/components/Articles/LostItemDetailPage/components/ReportForm/ReportForm.module.scss @@ -1,5 +1,11 @@ @use "src/utils/scss/media" as media; +.container { + display: flex; + flex-direction: column; + flex: 1; +} + .header { display: flex; padding: 32px 60px; @@ -54,6 +60,7 @@ .buttons { display: flex; padding: 0 24px 24px; + margin-top: auto; flex-direction: column; justify-content: center; align-items: center; diff --git a/src/components/Articles/LostItemDetailPage/components/ReportForm/index.tsx b/src/components/Articles/LostItemDetailPage/components/ReportForm/index.tsx index 9effe3981..fd45722f1 100644 --- a/src/components/Articles/LostItemDetailPage/components/ReportForm/index.tsx +++ b/src/components/Articles/LostItemDetailPage/components/ReportForm/index.tsx @@ -47,7 +47,7 @@ export default function ReportForm({ articleId, onClose, isModal }: ReportFormPr if (isModal) { onClose(); } else { - navigate(ROUTES.Articles()); + navigate(ROUTES.LostItems()); } } catch { toast.error('신고 중 오류가 발생했습니다. 다시 시도해주세요.'); @@ -57,7 +57,7 @@ export default function ReportForm({ articleId, onClose, isModal }: ReportFormPr }; return ( -
+
신고 이유를 선택해주세요.
diff --git a/src/components/Articles/LostItemDetailPage/hooks/usePostFoundLostItem.ts b/src/components/Articles/LostItemDetailPage/hooks/usePostFoundLostItem.ts new file mode 100644 index 000000000..0982196a5 --- /dev/null +++ b/src/components/Articles/LostItemDetailPage/hooks/usePostFoundLostItem.ts @@ -0,0 +1,28 @@ +import { isKoinError, sendClientError } from '@bcsdlab/koin'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { articleMutations } from 'api/articles/mutations'; +import useTokenState from 'utils/hooks/state/useTokenState'; +import showToast from 'utils/ts/showToast'; + +const usePostFoundLostItem = (articleId: number) => { + const token = useTokenState(); + const queryClient = useQueryClient(); + const mutation = articleMutations.toggleLostItemFound(queryClient, token, articleId); + + const { mutate, isPending } = useMutation({ + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); + showToast('success', '상태가 변경되었습니다.'); + }, + onError: (e) => { + if (isKoinError(e)) { + showToast('error', e.message); + } else sendClientError(e); + }, + }); + + return { mutate, isPending }; +}; + +export default usePostFoundLostItem; diff --git a/src/components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom.ts b/src/components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom.ts index 7d25623e7..20b8cfbee 100644 --- a/src/components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom.ts +++ b/src/components/Articles/LostItemDetailPage/hooks/usePostLostItemChatroom.ts @@ -1,18 +1,15 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postLostItemChatroom } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const usePostLostItemChatroom = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.createLostItemChatroom(queryClient, token); const { mutateAsync } = useMutation({ - mutationFn: async (articleId: number) => { - const response = await postLostItemChatroom(token, articleId); - return response; - }, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['chatroom', 'lost-item'] }), + ...mutation, onError: (e) => { if (isKoinError(e)) { showToast('error', e.message); diff --git a/src/components/Articles/LostItemEditPage/hooks/usePutLostItemArticle.ts b/src/components/Articles/LostItemEditPage/hooks/usePutLostItemArticle.ts new file mode 100644 index 000000000..ea759893d --- /dev/null +++ b/src/components/Articles/LostItemEditPage/hooks/usePutLostItemArticle.ts @@ -0,0 +1,27 @@ +import { isKoinError, sendClientError } from '@bcsdlab/koin'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { articleMutations } from 'api/articles/mutations'; +import useTokenState from 'utils/hooks/state/useTokenState'; +import showToast from 'utils/ts/showToast'; + +const usePutLostItemArticle = (articleId: number) => { + const token = useTokenState(); + const queryClient = useQueryClient(); + const mutation = articleMutations.updateLostItem(queryClient, token, articleId); + const { status, mutateAsync } = useMutation({ + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); + showToast('success', '게시글 수정이 완료되었습니다.'); + }, + onError: (e) => { + if (isKoinError(e)) { + showToast('error', e.message); + } else sendClientError(e); + }, + }); + + return { status, mutateAsync }; +}; + +export default usePutLostItemArticle; diff --git a/src/components/Articles/LostItemEditPage/index.tsx b/src/components/Articles/LostItemEditPage/index.tsx new file mode 100644 index 000000000..c83dfd99f --- /dev/null +++ b/src/components/Articles/LostItemEditPage/index.tsx @@ -0,0 +1,118 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { LostItemImageDTO } from 'api/articles/entity'; +import { articleQueries } from 'api/articles/queries'; +import LostItemPageTemplate from 'components/Articles/components/LostItemPageTemplate'; +import { FindUserCategory, useArticlesLogger } from 'components/Articles/hooks/useArticlesLogger'; +import { useLostItemForm } from 'components/Articles/hooks/useLostItemForm'; +import usePutLostItemArticle from 'components/Articles/LostItemEditPage/hooks/usePutLostItemArticle'; +import LostItemForm from 'components/Articles/LostItemWritePage/components/LostItemForm'; +import ROUTES from 'static/routes'; +import useTokenState from 'utils/hooks/state/useTokenState'; +import { getYyyyMmDd } from 'utils/ts/calendar'; + +interface LostItemEditPageProps { + articleId: number; +} + +const EDIT_TITLES = { + FOUND: { + title: '습득물 수정', + subtitle: '주인을 찾아요', + description: '습득한 물건을 자세히 설명해주세요!', + }, + LOST: { + title: '분실물 수정', + subtitle: '잃어버렸어요', + description: '분실한 물건을 자세히 설명해주세요!', + }, +}; + +export default function LostItemEditPage({ articleId }: LostItemEditPageProps) { + const router = useRouter(); + const token = useTokenState(); + const { logLostItemModifyComplete } = useArticlesLogger(); + const { data: article } = useSuspenseQuery(articleQueries.lostItemDetail(token, articleId)); + const { status, mutateAsync: putLostItem } = usePutLostItemArticle(articleId); + + const type = article.type as 'FOUND' | 'LOST'; + const isFound = type === 'FOUND'; + const { title, subtitle, description } = EDIT_TITLES[type]; + + const [originalImages] = useState(article.images); + const [deleteImageIds, setDeleteImageIds] = useState([]); + + const { lostItems, lostItemHandler, validateAndUpdateItems, checkArticleFormFull } = useLostItemForm({ + defaultType: type, + initialItems: [ + { + id: article.id, + type, + category: article.category as FindUserCategory, + foundDate: new Date(article.found_date), + foundPlace: article.found_place, + content: article.content ?? '', + author: article.author, + images: article.images.map((img) => img.image_url), + registered_at: article.registered_at, + updated_at: article.updated_at, + hasDateBeenSelected: true, + isCategorySelected: true, + isDateSelected: true, + isFoundPlaceSelected: true, + }, + ], + }); + + const lostItem = lostItems[0]; + + const baseHandler = lostItemHandler(0); + const customLostItemHandler = { + ...baseHandler, + setImages: (images: Array) => { + const removedUrls = lostItem.images.filter((url) => !images.includes(url)); + const newDeleteIds = originalImages.filter((img) => removedUrls.includes(img.image_url)).map((img) => img.id); + setDeleteImageIds((prev: number[]) => Array.from(new Set([...prev, ...newDeleteIds]))); + baseHandler.setImages(images); + }, + }; + + const handleCompleteClick = async () => { + validateAndUpdateItems(); + if (!checkArticleFormFull()) return; + + const originalImageUrls = originalImages.map((img) => img.image_url); + const newImages = lostItem.images.filter((url) => !originalImageUrls.includes(url)); + + const data = { + category: lostItem.category, + found_place: + type === 'LOST' && (!lostItem.foundPlace || lostItem.foundPlace.trim() === '') + ? '장소 미상' + : lostItem.foundPlace, + found_date: getYyyyMmDd(lostItem.foundDate), + content: lostItem.content, + new_images: newImages, + delete_image_ids: deleteImageIds, + }; + + await putLostItem(data); + logLostItemModifyComplete(type === 'LOST' ? '분실물' : '습득물'); + router.replace(ROUTES.LostItemDetail({ id: String(articleId) })); + }; + + return ( + + + + ); +} diff --git a/src/components/Articles/LostItemWritePage/components/FormCategory/FormCategory.module.scss b/src/components/Articles/LostItemWritePage/components/FormCategory/FormCategory.module.scss index 1ddcdbc17..928751c94 100644 --- a/src/components/Articles/LostItemWritePage/components/FormCategory/FormCategory.module.scss +++ b/src/components/Articles/LostItemWritePage/components/FormCategory/FormCategory.module.scss @@ -7,6 +7,7 @@ @include media.media-breakpoint(mobile) { flex-direction: column; + gap: 8px; } &__wrapper { @@ -20,6 +21,17 @@ &__text { display: flex; flex-direction: column; + + @include media.media-breakpoint(mobile) { + justify-content: space-between; + flex-direction: row; + } + } + + &__label { + display: flex; + align-items: center; + gap: 4px; } &__buttons { @@ -39,7 +51,6 @@ background-color: #fff; border: 1px solid #175c8e; color: #175c8e; - font-family: Pretendard, sans-serif; font-size: 14px; font-weight: 500; @@ -48,50 +59,35 @@ color: #fff; } } -} -.title { - font-family: Pretendard, sans-serif; - font-size: 20px; - font-weight: 500; - line-height: 1.2; + &__required { + color: #f00; - @include media.media-breakpoint(mobile) { - font-size: 14px; - line-height: 1.6; + @include media.media-breakpoint(mobile) { + font-size: 14px; + } } - &__description { - color: #727272; - font-family: Pretendard, sans-serif; - font-size: 14px; - line-height: 1.6; + &__title { + font-size: 20px; + font-weight: 500; @include media.media-breakpoint(mobile) { - margin-bottom: 12px; - font-size: 12px; + font-size: 14px; line-height: 1.6; } } -} -.warning { - position: absolute; - left: 0; - bottom: -32px; - display: flex; - align-items: center; - gap: 6px; - color: #f7941e; - font-family: Pretendard, sans-serif; - font-size: 14px; - font-weight: 400; - line-height: 1.6; + &__warning { + display: flex; + align-items: center; + gap: 6px; + color: #f7941e; + font-size: 14px; + line-height: 1.6; - @include media.media-breakpoint(mobile) { - left: initial; - bottom: initial; - right: 0; - top: -53px; + @include media.media-breakpoint(mobile) { + font-size: 12px; + } } } diff --git a/src/components/Articles/LostItemWritePage/components/FormCategory/index.tsx b/src/components/Articles/LostItemWritePage/components/FormCategory/index.tsx index 329de6a4e..05cc8f46c 100644 --- a/src/components/Articles/LostItemWritePage/components/FormCategory/index.tsx +++ b/src/components/Articles/LostItemWritePage/components/FormCategory/index.tsx @@ -27,8 +27,16 @@ export default function FormCategory({ category, setCategory, isCategorySelected return (
- 품목 - 품목을 선택해주세요. +
+ 품목 + * +
+ {!isCategorySelected && ( + + + 품목이 선택되지 않았습니다. + + )}
@@ -46,12 +54,6 @@ export default function FormCategory({ category, setCategory, isCategorySelected ))}
- {!isCategorySelected && ( - - - 품목이 선택되지 않았습니다. - - )}
); diff --git a/src/components/Articles/LostItemWritePage/components/FormDate/FormDate.module.scss b/src/components/Articles/LostItemWritePage/components/FormDate/FormDate.module.scss index 94c19d28f..9b602609e 100644 --- a/src/components/Articles/LostItemWritePage/components/FormDate/FormDate.module.scss +++ b/src/components/Articles/LostItemWritePage/components/FormDate/FormDate.module.scss @@ -14,6 +14,23 @@ gap: 8px; } + &__text { + display: flex; + flex-direction: column; + + @include media.media-breakpoint(mobile) { + width: 100%; + flex-direction: row; + justify-content: space-between; + } + } + + &__label { + display: flex; + align-items: center; + gap: 4px; + } + &__wrapper { position: relative; display: flex; @@ -45,7 +62,6 @@ &__description { padding-top: 2px; color: #727272; - font-family: Pretendard, sans-serif; font-size: 14px; font-weight: 400; line-height: 1.6; @@ -70,51 +86,51 @@ top: 48px; right: 0; z-index: 1; + + @include media.media-breakpoint(mobile) { + position: relative; + top: 0; + width: 100%; + } } -} -.title { - font-family: Pretendard, sans-serif; - font-size: 20px; - font-weight: 500; - line-height: 1.2; + &__required { + color: #f00; - @include media.media-breakpoint(mobile) { - font-size: 14px; - line-height: 1.6; + @include media.media-breakpoint(mobile) { + font-size: 14px; + } } - &__description { - color: #727272; - font-family: Pretendard, sans-serif; - font-size: 14px; - line-height: 1.6; + &__title { + font-size: 20px; + font-weight: 500; @include media.media-breakpoint(mobile) { - margin-bottom: 12px; - font-size: 12px; + font-size: 14px; line-height: 1.6; } } -} -.warning { - position: absolute; - left: 0; - bottom: -32px; - display: flex; - align-items: center; - gap: 6px; - color: #f7941e; - font-family: Pretendard, sans-serif; - font-size: 14px; - font-weight: 400; - line-height: 1.6; + .icon { + display: flex; + transition: transform 0.2s ease; - @include media.media-breakpoint(mobile) { - left: initial; - bottom: initial; - right: 0; - top: -32px; + &--open { + transform: rotate(180deg); + } + } + + &__warning { + display: flex; + align-items: center; + gap: 6px; + color: #f7941e; + font-size: 14px; + line-height: 1.6; + + @include media.media-breakpoint(mobile) { + font-size: 12px; + } } } diff --git a/src/components/Articles/LostItemWritePage/components/FormDate/index.tsx b/src/components/Articles/LostItemWritePage/components/FormDate/index.tsx index 44a026beb..32707daaf 100644 --- a/src/components/Articles/LostItemWritePage/components/FormDate/index.tsx +++ b/src/components/Articles/LostItemWritePage/components/FormDate/index.tsx @@ -2,6 +2,8 @@ import { cn } from '@bcsdlab/utils'; import ChevronDown from 'assets/svg/Articles/chevron-down.svg'; import WarnIcon from 'assets/svg/Articles/warn.svg'; import Calendar from 'components/Articles/LostItemWritePage/components/Calendar'; +import MobileDatePicker from 'components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker'; +import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import { useEscapeKeyDown } from 'utils/hooks/ui/useEscapeKeyDown'; import { useOutsideClick } from 'utils/hooks/ui/useOutsideClick'; @@ -32,6 +34,7 @@ export default function FormDate({ type, }: FormDateProps) { const [calendarOpen, , closeCalendar, toggleCalendar] = useBooleanState(false); + const isMobile = useMediaQuery(); const handleDateSelect = (date: Date) => { setFoundDate(date); @@ -39,16 +42,33 @@ export default function FormDate({ closeCalendar(); }; + const handleMobileDateSelect = (date: Date) => { + setFoundDate(date); + setHasDateBeenSelected(); + closeCalendar(); + }; + const { containerRef } = useOutsideClick({ onOutsideClick: closeCalendar }); useEscapeKeyDown({ onEscape: closeCalendar }); - const getDate = type === 'FOUND' ? '습득 일자' : '분실 일자'; + const dateLabel = type === 'FOUND' ? '습득 일자' : '분실 일자'; const placeholderText = type === 'FOUND' ? '습득 일자를 선택해주세요.' : '분실 일자를 선택해주세요.'; const warningText = type === 'FOUND' ? '습득 일자가 입력되지 않았습니다.' : '분실 일자가 입력되지 않았습니다.'; return (
- {getDate} +
+
+ {dateLabel} + * +
+ {!isDateSelected && ( + + + {warningText} + + )} +
{calendarOpen && (
- + {isMobile ? ( + + ) : ( + + )}
)} - {!isDateSelected && ( - - - {warningText} - - )}
diff --git a/src/components/Articles/LostItemWritePage/components/FormFoundPlace/FormFoundPlace.module.scss b/src/components/Articles/LostItemWritePage/components/FormFoundPlace/FormFoundPlace.module.scss index b40fc9c06..027fc4567 100644 --- a/src/components/Articles/LostItemWritePage/components/FormFoundPlace/FormFoundPlace.module.scss +++ b/src/components/Articles/LostItemWritePage/components/FormFoundPlace/FormFoundPlace.module.scss @@ -12,6 +12,23 @@ gap: 8px; } + &__text { + display: flex; + flex-direction: column; + + @include media.media-breakpoint(mobile) { + width: 100%; + flex-direction: row; + justify-content: space-between; + } + } + + &__label { + display: flex; + align-items: center; + gap: 4px; + } + &__wrapper { position: relative; display: flex; @@ -34,9 +51,7 @@ border-radius: 8px; background-color: #f5f5f5; border: none; - font-family: Pretendard, sans-serif; font-size: 14px; - font-weight: 400; line-height: 1.6; @include media.media-breakpoint(mobile) { @@ -44,49 +59,35 @@ font-size: 12px; } } -} -.title { - font-family: Pretendard, sans-serif; - font-size: 20px; - font-weight: 500; - line-height: 1.2; + &__required { + color: #f00; - @include media.media-breakpoint(mobile) { - font-size: 14px; - line-height: 1.6; + @include media.media-breakpoint(mobile) { + font-size: 14px; + } } - &__description { - color: #727272; - font-family: Pretendard, sans-serif; - font-size: 14px; - line-height: 1.6; + &__title { + font-size: 20px; + font-weight: 500; @include media.media-breakpoint(mobile) { - margin-bottom: 12px; - font-size: 12px; + font-size: 14px; + line-height: 1.6; } } -} -.warning { - position: absolute; - left: 0; - bottom: -32px; - display: flex; - align-items: center; - gap: 6px; - color: #f7941e; - font-family: Pretendard, sans-serif; - font-size: 14px; - font-weight: 400; - line-height: 1.6; + &__warning { + display: flex; + align-items: center; + gap: 6px; + color: #f7941e; + font-size: 14px; + line-height: 1.6; - @include media.media-breakpoint(mobile) { - left: initial; - bottom: initial; - right: 0; - top: -32px; + @include media.media-breakpoint(mobile) { + font-size: 12px; + } } } diff --git a/src/components/Articles/LostItemWritePage/components/FormFoundPlace/index.tsx b/src/components/Articles/LostItemWritePage/components/FormFoundPlace/index.tsx index 28e5907dc..de432db96 100644 --- a/src/components/Articles/LostItemWritePage/components/FormFoundPlace/index.tsx +++ b/src/components/Articles/LostItemWritePage/components/FormFoundPlace/index.tsx @@ -19,11 +19,21 @@ export default function FormFoundPlace({ foundPlace, setFoundPlace, isFoundPlace const placeLabel = type === 'FOUND' ? '습득 장소' : '분실 장소'; const placeholderText = type === 'FOUND' ? '습득 장소를 선택해주세요.' : '예상되는 분실 장소가 있다면 입력해주세요.'; - const warningText = type === 'FOUND' ? '습득 장소가 입력되지 않았습니다.' : '분실 장소가 입력되지 않았습니다.'; return (
- {placeLabel} +
+
+ {placeLabel} + {type === 'FOUND' && *} +
+ {type === 'FOUND' && !isFoundPlaceSelected && ( + + + 습득 장소가 입력되지 않았습니다. + + )} +
- {type === 'FOUND' && !isFoundPlaceSelected && ( - - - {warningText} - - )}
); diff --git a/src/components/Articles/LostItemWritePage/components/FormImage/index.tsx b/src/components/Articles/LostItemWritePage/components/FormImage/index.tsx index 8a8f0cd87..d1da30fb7 100644 --- a/src/components/Articles/LostItemWritePage/components/FormImage/index.tsx +++ b/src/components/Articles/LostItemWritePage/components/FormImage/index.tsx @@ -17,7 +17,7 @@ interface FormImageProps { export default function FormImage({ images, setImages, type, formIndex }: FormImageProps) { const isMobile = useMediaQuery(); - const { imgRef, saveImgFile } = useImageUpload({ domain: 'LOST_ITEMS' }); + const { imgRef, saveImgFile, setImageFile } = useImageUpload({ domain: 'LOST_ITEMS', maxLength: MAX_IMAGES_LENGTH }); const imageCounter = `${images.length}/${MAX_IMAGES_LENGTH}`; const inputId = `image-file-${formIndex}`; @@ -41,7 +41,9 @@ export default function FormImage({ images, setImages, type, formIndex }: FormIm }; const deleteImage = (url: string) => { - setImages(images.filter((image: string) => image !== url)); + const filtered = images.filter((image: string) => image !== url); + setImages(filtered); + setImageFile(filtered); }; const uploadImage = type === 'FOUND' ? '습득물 사진을 업로드해주세요.' : '분실물 사진을 업로드해주세요.'; diff --git a/src/components/Articles/LostItemWritePage/components/LostItemForm/LostItemForm.module.scss b/src/components/Articles/LostItemWritePage/components/LostItemForm/LostItemForm.module.scss index 7cf4b2ad9..f78234044 100644 --- a/src/components/Articles/LostItemWritePage/components/LostItemForm/LostItemForm.module.scss +++ b/src/components/Articles/LostItemWritePage/components/LostItemForm/LostItemForm.module.scss @@ -92,19 +92,22 @@ } } - & svg { - @include media.media-breakpoint(mobile) { - width: 20px; - height: 20px; - padding-top: 4px; - } - } - &__delete { display: none; &--visible { display: flex; + align-items: center; + + @include media.media-breakpoint(mobile) { + padding-top: 4px; + + svg { + display: block; + width: 20px; + height: 20px; + } + } } } } diff --git a/src/components/Articles/LostItemWritePage/components/LostItemForm/index.tsx b/src/components/Articles/LostItemWritePage/components/LostItemForm/index.tsx index 87e71f1ca..1cd2f27af 100644 --- a/src/components/Articles/LostItemWritePage/components/LostItemForm/index.tsx +++ b/src/components/Articles/LostItemWritePage/components/LostItemForm/index.tsx @@ -16,12 +16,13 @@ const MAX_LOST_ITEM_TYPE = { interface LostItemFormProps { type: 'FOUND' | 'LOST'; count: number; + totalCount: number; lostItem: LostItem; lostItemHandler: LostItemHandler; - removeLostItem: (index: number) => void; + removeLostItem?: (index: number) => void; } -export default function LostItemForm({ type, count, lostItem, lostItemHandler, removeLostItem }: LostItemFormProps) { +export default function LostItemForm({ type, count, totalCount, lostItem, lostItemHandler, removeLostItem }: LostItemFormProps) { const { category, foundDate, @@ -44,10 +45,10 @@ export default function LostItemForm({ type, count, lostItem, lostItemHandler, r + +
+
+ ); +} diff --git a/src/components/Articles/LostItemWritePage/index.tsx b/src/components/Articles/LostItemWritePage/index.tsx index 4ab28500c..a82765d50 100644 --- a/src/components/Articles/LostItemWritePage/index.tsx +++ b/src/components/Articles/LostItemWritePage/index.tsx @@ -1,26 +1,16 @@ import { useEffect } from 'react'; import { useRouter } from 'next/router'; -import AddIcon from 'assets/svg/Articles/add.svg'; -import FoundIcon from 'assets/svg/Articles/found.svg'; -import LostIcon from 'assets/svg/Articles/lost.svg'; +import LostItemPageTemplate from 'components/Articles/components/LostItemPageTemplate'; import { useArticlesLogger } from 'components/Articles/hooks/useArticlesLogger'; import { useLostItemForm } from 'components/Articles/hooks/useLostItemForm'; import usePostLostItemArticles from 'components/Articles/hooks/usePostLostItemArticles'; import LostItemForm from 'components/Articles/LostItemWritePage/components/LostItemForm'; import ROUTES from 'static/routes'; -import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import { useUser } from 'utils/hooks/state/useUser'; +import { getYyyyMmDd } from 'utils/ts/calendar'; import showToast from 'utils/ts/showToast'; -import uuidv4 from 'utils/ts/uuidGenerater'; -import styles from './LostItemWritePage.module.scss'; -const getyyyyMMdd = (date: Date) => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - - return `${year}-${month}-${day}`; -}; +const MAX_ITEMS = 10; const TITLES = { FOUND: { @@ -34,18 +24,16 @@ const TITLES = { description: '분실한 물건을 자세히 설명해주세요!', }, }; - type LostItemType = 'FOUND' | 'LOST'; export default function LostItemWritePage() { const { data: user } = useUser(); const router = useRouter(); - const isMobile = useMediaQuery(); const type: LostItemType = router.asPath.includes('/found') ? 'FOUND' : 'LOST'; const isFound = type === 'FOUND'; const { title, subtitle, description } = TITLES[type]; const { lostItems, lostItemHandler, addLostItem, removeLostItem, validateAndUpdateItems, checkArticleFormFull } = - useLostItemForm(type); + useLostItemForm({ defaultType: type }); useEffect(() => { if (user?.name) { @@ -92,9 +80,9 @@ export default function LostItemWritePage() { const articles = lostItems.map((article) => ({ type, category: article.category, - foundPlace: + found_place: type === 'LOST' && (!article.foundPlace || article.foundPlace.trim() === '') ? '장소 미상' : article.foundPlace, - foundDate: getyyyyMMdd(article.foundDate), + found_date: getYyyyMmDd(article.foundDate), content: article.content, images: article.images, registered_at: article.registered_at, @@ -102,48 +90,31 @@ export default function LostItemWritePage() { })); const id = await postLostItem({ articles }); - router.replace(ROUTES.LostItemDetail({ id: String(id), isLink: true })); + router.replace(ROUTES.LostItemDetail({ id: String(id) })); }; return ( -
-
-
- - {isMobile ? subtitle : title} - {isMobile && {isFound ? : }} - - {description} -
-
- {lostItems.map((lostItem, index) => ( - - ))} -
-
- -
-
- -
-
-
+ + {lostItems.map((lostItem, index) => ( + + ))} + ); } diff --git a/src/components/Articles/components/ArticleHeader/index.tsx b/src/components/Articles/components/ArticleHeader/index.tsx index ba3f76aca..ff9c12016 100644 --- a/src/components/Articles/components/ArticleHeader/index.tsx +++ b/src/components/Articles/components/ArticleHeader/index.tsx @@ -1,6 +1,5 @@ import Image from 'next/image'; import { convertArticlesTag } from 'components/Articles/utils/convertArticlesTag'; -import setArticleRegisteredDate from 'components/Articles/utils/setArticleRegisteredDate'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import styles from './ArticleHeader.module.scss'; @@ -10,9 +9,10 @@ interface ArticleHeaderProps { registeredAt: string; author: string; hit: number; + isNew: boolean; } -export default function ArticleHeader({ boardId, title, registeredAt, author, hit }: ArticleHeaderProps) { +export default function ArticleHeader({ boardId, title, registeredAt, author, hit, isNew }: ArticleHeaderProps) { const isMobile = useMediaQuery(); return ( @@ -21,7 +21,7 @@ export default function ArticleHeader({ boardId, title, registeredAt, author, hi
{convertArticlesTag(boardId)} {title} - {setArticleRegisteredDate(registeredAt)[1] && ( + {isNew && ( { @@ -21,15 +20,22 @@ const parseLostItemTitle = (title: string) => { }; }; +const formatDate = (time: string) => { + if (typeof time !== 'string') { + return ''; + } + return time.split(' ')[0].replaceAll('-', '.'); +}; + export default function ArticleList({ articles }: ArticleListProps) { const isMobile = useMediaQuery(); - const getLink = (article: Article) => { + const getLink = (article: ArticleWithNew) => { switch (article.board_id) { case 14: - return ROUTES.LostItemDetail({ id: String(article.id), isLink: true }); + return ROUTES.LostItemDetail({ id: String(article.id) }); default: - return ROUTES.ArticlesDetail({ id: String(article.id), isLink: true }); + return ROUTES.ArticlesDetail({ id: String(article.id) }); } }; @@ -37,8 +43,7 @@ export default function ArticleList({ articles }: ArticleListProps) {
{articles.map((article) => { const { type, content, date } = parseLostItemTitle(article.title); - const registeredDate = setArticleRegisteredDate(article.registered_at)[0]; - const isNewArticle = setArticleRegisteredDate(article.registered_at)[1]; + const registeredDate = formatDate(article.registered_at); // 1. 신고된 게시글 (클릭 X, 토스트메시지, 내용 표시 다르게) if (article.board_id === 14 && article.is_reported) { @@ -82,7 +87,7 @@ export default function ArticleList({ articles }: ArticleListProps) {
{content}
|
{date}
- {isNewArticle && ( + {article.isNew && ( {/* 일반 공지사항 */}
{article.title}
- {isNewArticle && ( + {article.isNew && (
{isMobile ? `${article.author}` : article.author}
-
{setArticleRegisteredDate(article.registered_at)[0]}
+
{registeredDate}
); })} diff --git a/src/components/Articles/components/HotArticle/index.tsx b/src/components/Articles/components/HotArticle/index.tsx index 95288121b..e302e0729 100644 --- a/src/components/Articles/components/HotArticle/index.tsx +++ b/src/components/Articles/components/HotArticle/index.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useQuery } from '@tanstack/react-query'; -import { articles } from 'api'; +import { articleQueries } from 'api/articles/queries'; import LoadingSpinner from 'components/feedback/LoadingSpinner'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; @@ -32,10 +32,7 @@ const LINK_LIST = [ export default function HotArticles() { const logger = useLogger(); - const { data: hotArticles, isLoading } = useQuery({ - queryKey: ['hotArticles'], - queryFn: articles.getHotArticles, - }); + const { data: hotArticles, isLoading } = useQuery(articleQueries.hot()); if (isLoading || !hotArticles) { return ; @@ -48,7 +45,7 @@ export default function HotArticles() { {hotArticles.map((article, index) => ( logger.actionEventClick({ team: 'CAMPUS', event_label: 'notice_hot', value: article.title })} > diff --git a/src/components/Articles/components/LostItemFilterBottomSheet/LostItemFilterBottomSheet.module.scss b/src/components/Articles/components/LostItemFilterBottomSheet/LostItemFilterBottomSheet.module.scss new file mode 100644 index 000000000..b1d536a88 --- /dev/null +++ b/src/components/Articles/components/LostItemFilterBottomSheet/LostItemFilterBottomSheet.module.scss @@ -0,0 +1,12 @@ +.sheet { + width: 100%; + background: #fff; + border-radius: 24px 24px 0 0; + box-shadow: 0 12px 30px rgba(0 0 0 / 18%); +} + +.sheetInner { + height: 100%; + padding: 20px 20px 24px; + margin-bottom: 20px; +} diff --git a/src/components/Articles/components/LostItemFilterBottomSheet/index.tsx b/src/components/Articles/components/LostItemFilterBottomSheet/index.tsx new file mode 100644 index 000000000..6333dcf0b --- /dev/null +++ b/src/components/Articles/components/LostItemFilterBottomSheet/index.tsx @@ -0,0 +1,22 @@ +import LostItemFilterContent from 'components/Articles/components/LostItemFilterContent'; +import BottomModal from 'components/ui/BottomModal'; +import type { FilterState } from 'components/Articles/components/LostItemFilterContent'; +import styles from './LostItemFilterBottomSheet.module.scss'; + +interface Props { + isOpen: boolean; + onClose: () => void; + onReset?: () => void; + onApply: (filter: FilterState) => void; + initialFilter?: FilterState; +} + +export default function LostItemFilterBottomSheet({ isOpen, onClose, onReset, onApply, initialFilter }: Props) { + return ( + +
+ +
+
+ ); +} diff --git a/src/components/Articles/components/LostItemFilterContent/LostItemFilterContent.module.scss b/src/components/Articles/components/LostItemFilterContent/LostItemFilterContent.module.scss new file mode 100644 index 000000000..516619996 --- /dev/null +++ b/src/components/Articles/components/LostItemFilterContent/LostItemFilterContent.module.scss @@ -0,0 +1,106 @@ +@use "src/utils/scss/media" as media; + +.container { + display: flex; + flex-direction: column; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.title { + font-size: 18px; + font-weight: 600; + color: #175c8e; +} + +.close { + background: transparent; + border: 0; + padding: 6px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.section { + padding: 24px 0; +} + +.label { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; +} + +.divider { + height: 1px; + background: #eee; +} + +.categorys { + width: 70%; +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.chip { + border: 1px solid #e1e1e1; + background: #fff; + border-radius: 999px; + padding: 8px 12px; + font-size: 14px; + font-weight: 600; + color: #727272; +} + +.active { + border-color: #175c8e; + color: #175c8e; +} + +.footer { + display: flex; + justify-content: center; + gap: 24px; +} + +.reset { + display: flex; + padding: 8px 16px; + border: 1px solid #cacaca; + background: #fff; + border-radius: 12px; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + color: #4b4b4b; + + @include media.media-breakpoint(mobile) { + flex: 1; + height: 48px; + justify-content: center; + } +} + +.apply { + padding: 8px 36px; + background: #175c8e; + color: #fff; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + + @include media.media-breakpoint(mobile) { + flex: 2; + height: 48px; + } +} diff --git a/src/components/Articles/components/LostItemFilterContent/index.tsx b/src/components/Articles/components/LostItemFilterContent/index.tsx new file mode 100644 index 000000000..1c260bfd7 --- /dev/null +++ b/src/components/Articles/components/LostItemFilterContent/index.tsx @@ -0,0 +1,202 @@ +import { useState } from 'react'; +import CloseIcon from 'assets/svg/Articles/close.svg'; +import RefreshIcon from 'assets/svg/Articles/refresh.svg'; +import { useArticlesLogger } from 'components/Articles/hooks/useArticlesLogger'; +import { LIST_OPTIONS, CATEGORY_OPTIONS, ITEM_TYPE_OPTIONS, STATUS_OPTIONS } from 'static/filterOptions'; +import styles from './LostItemFilterContent.module.scss'; + +export type Author = 'ALL' | 'MY'; +export type Type = 'ALL' | 'LOST' | 'FOUND'; +export type Category = 'CARD' | 'ID' | 'WALLET' | 'ELECTRONICS' | 'ETC'; +export type FoundStatus = 'ALL' | 'FOUND' | 'NOT_FOUND'; + +export type FilterState = { + author: Author; + type: Type; + category: Category[]; + foundStatus: FoundStatus; +}; + +const DEFAULT_FILTER: FilterState = { + author: 'ALL', + type: 'ALL', + category: [], + foundStatus: 'ALL', +}; + +// 단일 선택 Chip -------------------- +interface ChipSingleOption { + key: T; + label: string; +} + +function ChipSingle({ + value, + options, + onChange, + allKey, + allLabel = '전체', +}: { + value: T | null; + options: readonly ChipSingleOption[]; + onChange: (next: T) => void; + allKey: T; + allLabel?: string; +}) { + return ( +
+ + {options.map((opt) => ( + + ))} +
+ ); +} + +// 복수 선택 Chip -------------------- +function toggleInArray(arr: T[], value: T) { + return arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value]; +} + +function ChipMulti({ + value, + options, + onChange, + allLabel = '전체', +}: { + value: T[]; + options: readonly ChipSingleOption[]; + onChange: (next: T[]) => void; + allLabel?: string; +}) { + return ( +
+ + + {options.map((opt) => ( + + ))} +
+ ); +} + +interface LostItemFilterContentProps { + onClose: () => void; + onReset?: () => void; + onApply: (filter: FilterState) => void; + initialFilter?: FilterState; +} + +export default function LostItemFilterContent({ + onClose, + onReset, + onApply, + initialFilter, +}: LostItemFilterContentProps) { + const { logLostItemFilterApply } = useArticlesLogger(); + const [filter, setFilter] = useState(initialFilter ?? DEFAULT_FILTER); + + const handleApply = () => { + logLostItemFilterApply(); + onApply(filter); + }; + + const handleReset = () => { + setFilter(DEFAULT_FILTER); + onReset?.(); + }; + + return ( +
+
+
필터
+ +
+ +
+
목록
+ setFilter((p) => ({ ...p, author }))} + /> +
+ +
+ +
+
물품 카테고리
+ setFilter((p) => ({ ...p, type }))} + /> +
+ +
+ +
+
물품 종류
+
+ setFilter((p) => ({ ...p, category }))} + /> +
+
+ +
+ +
+
물품 상태
+ setFilter((p) => ({ ...p, foundStatus }))} + /> +
+ +
+ + +
+
+ ); +} diff --git a/src/components/Articles/components/LostItemFilterModal/LostItemFilterModal.module.scss b/src/components/Articles/components/LostItemFilterModal/LostItemFilterModal.module.scss new file mode 100644 index 000000000..bc817a07c --- /dev/null +++ b/src/components/Articles/components/LostItemFilterModal/LostItemFilterModal.module.scss @@ -0,0 +1,13 @@ +@use "src/utils/scss/media" as media; + +.modal { + width: 360px; + background: #fff; + border-radius: 16px; + box-shadow: 0 10px 24px rgba(0 0 0 / 12%); + overflow: hidden; +} + +.modalInner { + padding: 16px 32px; +} diff --git a/src/components/Articles/components/LostItemFilterModal/index.tsx b/src/components/Articles/components/LostItemFilterModal/index.tsx new file mode 100644 index 000000000..b8cb8b464 --- /dev/null +++ b/src/components/Articles/components/LostItemFilterModal/index.tsx @@ -0,0 +1,26 @@ +import LostItemFilterContent, { FilterState } from 'components/Articles/components/LostItemFilterContent'; +import { useOutsideClick } from 'utils/hooks/ui/useOutsideClick'; +import styles from './LostItemFilterModal.module.scss'; + +interface LostItemFilterModalProps { + onClose: () => void; + onReset?: () => void; + onApply: (filter: FilterState) => void; + initialFilter?: FilterState; +} + +export default function LostItemFilterModal(props: LostItemFilterModalProps) { + const { containerRef } = useOutsideClick({ + onOutsideClick: () => { + props.onClose(); + }, + }); + + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/Articles/components/LostItemList/LostItemList.module.scss b/src/components/Articles/components/LostItemList/LostItemList.module.scss new file mode 100644 index 000000000..ec5ce971a --- /dev/null +++ b/src/components/Articles/components/LostItemList/LostItemList.module.scss @@ -0,0 +1,292 @@ +@use "src/utils/scss/media" as media; + +$lost-items-columns: 1fr 5.5fr 1.5fr 1.5fr 1fr; + +.header { + width: 100%; + border-top: 2px solid #175c8e; + + @include media.media-breakpoint(mobile) { + width: 100%; + border: none; + } + + &__container { + width: 100%; + height: 44px; + font-weight: bold; + + @include media.media-breakpoint(mobile) { + display: none; + } + } + + &__row { + display: grid; + grid-template-columns: $lost-items-columns; + height: 100%; + align-items: center; + border-bottom: 1px solid #175c8e; + } +} + +.info { + min-width: 0; + height: 100%; + font-size: 15px; + color: #175c8e; + display: flex; + align-items: center; + justify-content: center; +} + +.lostItemList { + width: 100%; + + &__row, + &__rowDisabled { + display: grid; + grid-template-columns: $lost-items-columns; + height: 68px; + align-items: center; + border-bottom: 1px solid #d2dae2; + text-decoration: none; + background: transparent; + } + + &__row { + cursor: pointer; + + &:hover { + background: #f8fafb; + } + } + + &__rowDisabled { + cursor: pointer; + width: 100%; + text-align: left; + + &:hover { + background: #f8fafb; + } + + @include media.media-breakpoint(mobile) { + border: none; + padding: 12px 24px; + } + } + + &__type, + &__title, + &__author, + &__date, + &__status { + min-width: 0; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + } + + &__type { + color: #252525; + + @include media.media-breakpoint(mobile) { + display: none; + } + } + + &__title { + justify-content: flex-start; + overflow: hidden; + padding: 0 12px; + color: #252525; + + @include media.media-breakpoint(mobile) { + width: 100%; + height: 100%; + box-sizing: border-box; + display: block; + padding: 0; + } + } + + &__titleMeta { + min-width: 0; + display: flex; + gap: 12px; + align-items: center; + white-space: nowrap; + overflow: hidden; + } + + &__badge { + padding: 4px 12px; + background-color: #175c8e; + color: #fff; + border-radius: 999px; + font-weight: 500; + flex: 0 0 auto; + font-size: 14px; + + @include media.media-breakpoint(mobile) { + padding: 4px 8px; + } + } + + &__place { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + } + + &__foundDate { + flex: 0 0 auto; + overflow: visible; + } + + &__newIcon { + flex: 0 0 auto; + + @include media.media-breakpoint(mobile) { + display: none; + } + } + + &__author { + color: #175c8e; + overflow: hidden; + + @include media.media-breakpoint(mobile) { + font-size: 13px; + color: #a1a1a1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__date { + color: #727272; + + @include media.media-breakpoint(mobile) { + font-size: 13px; + color: #a1a1a1; + float: right; + height: 50%; + } + } + + &__status { + color: #252525; + + @include media.media-breakpoint(mobile) { + display: none; + } + } + + &__reportedText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__chip { + display: flex; + justify-content: center; + + @include media.media-breakpoint(mobile) { + justify-content: none; + } + } + + .lostItemListMobile { + font-size: 12px; + + &__row, + &__rowDisabled { + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; + padding: 12px 8px; + box-sizing: border-box; + } + + &__rowDisabled { + background: #fff; + border: 0; + text-align: left; + } + + &__type { + text-align: left; + flex-shrink: 0; + white-space: nowrap; + color: #175c8e; + font-weight: 600; + font-size: 12px; + } + + &__title { + display: flex; + align-items: center; + gap: 12px; + } + + &__titleMeta { + display: flex; + gap: 8px; + align-items: center; + font-size: 14px; + flex: 1; + min-width: 0; + } + + &__badge { + padding: 4px 8px; + background-color: #175c8e; + color: #fff; + border-radius: 999px; + font-weight: 500; + align-content: center; + flex: 0 0 auto; + } + + &__content { + max-width: 282px; + font-size: 12px; + line-height: 160%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__place { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__foundDate { + flex: 0 0 auto; + white-space: nowrap; + } + + &__writeMeta { + display: flex; + gap: 2px; + } + + &__author, + &__dot, + &__date { + color: #727272; + font-size: 12px; + } + } +} diff --git a/src/components/Articles/components/LostItemList/index.tsx b/src/components/Articles/components/LostItemList/index.tsx new file mode 100644 index 000000000..167d23dbd --- /dev/null +++ b/src/components/Articles/components/LostItemList/index.tsx @@ -0,0 +1,185 @@ +import React from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { LostItemArticleForGetDTO } from 'api/articles/entity'; +import { useArticlesLogger } from 'components/Articles/hooks/useArticlesLogger'; +import FoundChip from 'components/Articles/LostItemDetailPage/components/FoundChip'; +import setArticleRegisteredDate from 'components/Articles/utils/setArticleRegisteredDate'; +import ROUTES from 'static/routes'; +import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; +import showToast from 'utils/ts/showToast'; +import styles from './LostItemList.module.scss'; + +interface LostItemListProps { + articles: LostItemArticleForGetDTO[]; +} + +type HeaderRowInfo = { + [key: string]: string; +}; + +const HEADER_ROW: HeaderRowInfo = { + classification: '분류', + title: '제목', + author: '작성자', + date: '날짜', + stat: '물품 상태', +}; + +export default function LostItemList({ articles }: LostItemListProps) { + const isMobile = useMediaQuery(); + const { logLostItemPostEntry } = useArticlesLogger(); + + const handleReportedClick = () => showToast('error', '신고된 게시글은 볼 수 없습니다.'); + + const getCommon = (article: LostItemArticleForGetDTO) => { + const [registeredDate, isNewArticle] = setArticleRegisteredDate(article.registered_at); + const detailLink = ROUTES.LostItemDetail({ id: String(article.id) }); + const typeText = article.type === 'LOST' ? '분실물' : '습득물'; + + return { registeredDate, isNewArticle, detailLink, typeText }; + }; + + const mobileRow = (article: LostItemArticleForGetDTO) => { + const { registeredDate, detailLink, typeText } = getCommon(article); + + if (article.is_reported) { + return ( + + ); + } + + return ( + logLostItemPostEntry(article.type === 'LOST' ? '분실물' : '습득물')} + > +
{typeText}
+ +
+
+ {article.category} +
{article.found_place}
+
|
+
{article.found_date}
+
+ +
+ +
{article.content}
+ +
+
{article.author}
+
·
+
{registeredDate}
+
+ + ); + }; + + const desktopRow = (article: LostItemArticleForGetDTO) => { + const { registeredDate, isNewArticle, detailLink, typeText } = getCommon(article); + + if (article.is_reported) { + return ( + + ); + } + + return ( + logLostItemPostEntry(article.type === 'LOST' ? '분실물' : '습득물')} + > +
{typeText}
+ +
+
+ {article.category} +
{article.found_place}
+
|
+
{article.found_date}
+ + {isNewArticle && ( + new + )} +
+
+ +
{article.author}
+
{registeredDate}
+ +
+ +
+ + ); + }; + + return ( + +
+
+
+ {Object.keys(HEADER_ROW).map((key) => ( +
+ {HEADER_ROW[key]} +
+ ))} +
+
+
+
+ {articles.map((article) => (isMobile ? mobileRow(article) : desktopRow(article)))} +
+
+ ); +} diff --git a/src/components/Articles/components/LostItemPageLayout/LostItemPageLayout.module.scss b/src/components/Articles/components/LostItemPageLayout/LostItemPageLayout.module.scss new file mode 100644 index 000000000..b8e3ac227 --- /dev/null +++ b/src/components/Articles/components/LostItemPageLayout/LostItemPageLayout.module.scss @@ -0,0 +1,138 @@ +@use "src/utils/scss/media" as media; + +.lostItem-template { + display: flex; + margin-top: 60px; + width: 100%; + box-sizing: border-box; + padding: 0 100px; + gap: 40px; + + @include media.media-breakpoint(mobile) { + width: 100%; + margin-top: 0; + padding: 0; + } +} + +.lostItem-content { + width: 100%; + max-width: 1500px; // MacBook Pro 14" 기준 + margin-bottom: 60px; + display: flex; + flex-direction: column; + + @include media.media-breakpoint(mobile) { + width: 100%; + height: 100dvh; + margin: 0; + overflow: hidden; + } +} + +.header { + width: 100%; + margin-bottom: 24px; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + + @include media.media-breakpoint(mobile) { + margin: 4px 0; + } + + &__title { + font-family: Pretendard, sans-serif; + font-size: 32px; + font-weight: 500; + color: #175c8e; + margin: 0; + + @include media.media-breakpoint(mobile) { + display: none; + } + } +} + +.index { + display: flex; + align-items: center; + width: 100%; + margin-bottom: 24px; + gap: 8px; + + @include media.media-breakpoint(mobile) { + margin-bottom: 4px; + padding: 4px 8px; + box-sizing: border-box; + } + + &__rightButton { + margin-left: auto; + flex-shrink: 0; + } +} + +.search-container { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + color: #4b4b4b; + font-family: Pretendard, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + border-radius: 8px; + border: 1px solid #eee; + background: #fff; + box-shadow: 0 1px 9px 1px rgba(0 0 0 / 6%); + max-width: 550px; + flex: 1; + min-width: 0; + width: auto; + + @include media.media-breakpoint(mobile) { + width: 230px; + padding: 8px 12px; + border: 4px; + background: #f5f5f5; + box-shadow: none; + gap: 4px; + } + + &__input { + width: 100%; + border: none; + outline: none; + color: #4b4b4b; + font-family: Pretendard, sans-serif; + font-size: 16px; + font-weight: 400; + + @include media.media-breakpoint(mobile) { + font-size: 14px; + background: #f5f5f5; + } + } +} + +.listScroll { + @include media.media-breakpoint(mobile) { + flex: 1 1 auto; + min-height: 0; + width: 100%; + overflow-y: auto; + } +} + +.aside { + display: flex; + flex-direction: column; + align-items: center; + + @include media.media-breakpoint(mobile) { + display: none; + } +} diff --git a/src/components/Articles/components/LostItemPageLayout/index.tsx b/src/components/Articles/components/LostItemPageLayout/index.tsx new file mode 100644 index 000000000..96ea6762d --- /dev/null +++ b/src/components/Articles/components/LostItemPageLayout/index.tsx @@ -0,0 +1,69 @@ +import { useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import SearchIcon from 'assets/svg/Articles/search.svg'; +import HotArticles from 'components/Articles/components/HotArticle'; +import LostItemRouteButton from 'components/Articles/components/LostItemRouteButton'; +import ROUTES from 'static/routes'; +import useScrollToTop from 'utils/hooks/ui/useScrollToTop'; +import styles from './LostItemPageLayout.module.scss'; + +export default function LostItemPageLayout({ children }: { children: React.ReactNode }) { + useScrollToTop(); + + const router = useRouter(); + + const keywordFromQuery = (Array.isArray(router.query.keyword) ? router.query.keyword[0] : router.query.keyword) ?? ''; + const [keyword, setKeyword] = useState(String(keywordFromQuery)); + + const applySearch = () => { + const next = keyword.trim(); + + router.push({ + pathname: router.pathname, + query: { + ...router.query, + keyword: next || undefined, + page: 1, + }, + }); + }; + + return ( +
+
+
+ +

분실물

+ +
+ +
+
+ + setKeyword(e.target.value)} + placeholder="검색어를 입력해주세요." + onKeyDown={(e) => { + if (e.key === 'Enter') applySearch(); + }} + /> +
+ +
+ +
+
+ +
{children}
+
+ +
+ +
+
+ ); +} diff --git a/src/components/Articles/LostItemWritePage/LostItemWritePage.module.scss b/src/components/Articles/components/LostItemPageTemplate/LostItemPageTemplate.module.scss similarity index 67% rename from src/components/Articles/LostItemWritePage/LostItemWritePage.module.scss rename to src/components/Articles/components/LostItemPageTemplate/LostItemPageTemplate.module.scss index e2e4bb78f..2b0027113 100644 --- a/src/components/Articles/LostItemWritePage/LostItemWritePage.module.scss +++ b/src/components/Articles/components/LostItemPageTemplate/LostItemPageTemplate.module.scss @@ -38,7 +38,6 @@ } &__title { - font-family: Pretendard, sans-serif; font-size: 32px; font-weight: 500; color: #175c8e; @@ -53,8 +52,12 @@ } } + &__info-wrapper { + display: flex; + justify-content: space-between; + } + &__description { - font-family: Pretendard, sans-serif; font-size: 18px; line-height: 1.6; color: #666; @@ -65,6 +68,19 @@ color: #727272; } } + + &__info { + font-size: 14px; + + > span { + color: #f00; + } + + @include media.media-breakpoint(mobile) { + font-size: 11px; + line-height: 1.6; + } + } } .forms { @@ -77,71 +93,74 @@ } } -.add { - width: 100%; - margin: 32px 0 20px; +.buttons { display: flex; - justify-content: flex-end; - box-sizing: border-box; + justify-content: space-between; + align-items: center; + margin: 100px 0; @include media.media-breakpoint(mobile) { - margin: 0; - padding: 16px 24px; + flex-direction: column; + align-items: end; + gap: 40px; + margin: 16px 0; + padding: 0 24px; } - &__button { - width: 127px; - height: 48px; + &__add { display: flex; justify-content: center; align-items: center; + padding: 12px 16px; gap: 4px; border-radius: 8px; background-color: #e4f2ff; color: #10477a; - font-family: Pretendard, sans-serif; font-size: 18px; font-weight: 500; - line-height: 1.2; box-shadow: - 0 2px 4px rgba(0 0 0 / 4%), - 0 1px 1px rgba(0 0 0 / 2%); + 0 1px 4px rgba(0 0 0 / 4%), + 0 4px 10px rgba(0 0 0 / 8%); @include media.media-breakpoint(mobile) { height: 40px; font-size: 14px; } } -} -.complete { - margin-top: auto; - display: flex; - justify-content: center; - margin-bottom: 100px; + &__group { + display: flex; + gap: 24px; + margin-left: auto; - @include media.media-breakpoint(mobile) { - margin-top: 24px; + @include media.media-breakpoint(mobile) { + width: 100%; + } } &__button { width: 240px; height: 53px; - border: none; border-radius: 8px; background-color: #175c8e; color: white; - font-family: Pretendard, sans-serif; font-size: 18px; - font-weight: 700; + font-weight: 600; line-height: 1.6; box-shadow: - 0 2px 4px rgba(0 0 0 / 10%), - 0 1px 1px rgba(0 0 0 / 5%); + 0 1px 4px rgba(0 0 0 / 4%), + 0 4px 10px rgba(0 0 0 / 8%); @include media.media-breakpoint(mobile) { + width: 100%; height: 44px; font-size: 14px; } + + &--cancel { + background-color: #fff; + color: #175c8e; + border: 1px solid #e1e1e1; + } } } diff --git a/src/components/Articles/components/LostItemPageTemplate/index.tsx b/src/components/Articles/components/LostItemPageTemplate/index.tsx new file mode 100644 index 000000000..8ba85b42c --- /dev/null +++ b/src/components/Articles/components/LostItemPageTemplate/index.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import AddIcon from 'assets/svg/Articles/add.svg'; +import FoundIcon from 'assets/svg/Articles/found.svg'; +import LostIcon from 'assets/svg/Articles/lost.svg'; +import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; +import styles from './LostItemPageTemplate.module.scss'; + +interface LostItemPageTemplateProps { + title: string; + subtitle: string; + description: string; + isFound: boolean; + children: React.ReactNode; + bottomButtonText: string; + onBottomButtonClick: () => void; + isBottomButtonDisabled?: boolean; + onAddButtonClick?: () => void; +} + +export default function LostItemPageTemplate({ + title, + subtitle, + description, + isFound, + children, + bottomButtonText, + onBottomButtonClick, + isBottomButtonDisabled = false, + onAddButtonClick, +}: LostItemPageTemplateProps) { + const isMobile = useMediaQuery(); + const router = useRouter(); + + const handleCancelClick = () => { + router.back(); + }; + + return ( +
+
+
+ + {isMobile ? subtitle : title} + {isMobile && {isFound ? : }} + +
+ {description} + + * 표시는 필수 입력란입니다. + +
+
+
{children}
+ +
+ {onAddButtonClick && ( + + )} +
+ {!isMobile && ( + + )} + +
+
+
+
+ ); +} diff --git a/src/components/Articles/components/LostItemRouteButton/LostItemRouteButton.module.scss b/src/components/Articles/components/LostItemRouteButton/LostItemRouteButton.module.scss index 43221170c..52474d765 100644 --- a/src/components/Articles/components/LostItemRouteButton/LostItemRouteButton.module.scss +++ b/src/components/Articles/components/LostItemRouteButton/LostItemRouteButton.module.scss @@ -3,39 +3,32 @@ .links { display: flex; justify-content: flex-end; - gap: 20px; + gap: 16px; - &__writing-options { + &__filter { display: flex; - gap: 20px; + justify-content: center; align-items: center; - - @include media.media-breakpoint(mobile) { - flex-direction: column; - position: fixed; - right: 24px; - bottom: 100px; - gap: 12px; - z-index: 1001; - } - } - - &__option-button { - display: flex; - padding: 8px 12px; - border: 1px solid #e1e1e1; - background-color: #fafafa; + gap: 6px; + padding: 12px 20px; border-radius: 999px; - align-items: center; - gap: 8px; - } - - &__option-text { - font-family: Pretendard, sans-serif; + border: none; + background-color: #e4f2ff; color: #4b4b4b; + font-family: Pretendard, sans-serif; font-size: 14px; font-weight: 500; - line-height: 1.2; + cursor: pointer; + + svg { + width: 18px; + height: 18px; + } + + @include media.media-breakpoint(mobile) { + display: flex; + padding: 10px 12px; + } } &__write { @@ -51,7 +44,6 @@ color: #4b4b4b; font-family: Pretendard, sans-serif; font-weight: 500; - line-height: 1.2; @include media.media-breakpoint(mobile) { position: fixed; @@ -60,26 +52,85 @@ padding: 8px 12px; border-radius: 24px; font-size: 16px; - line-height: 1.6; - z-index: 1002; + z-index: 15; } } +} - &--active { - @include media.media-breakpoint(mobile) { - z-index: 1003; - } - } +.filterAnchor { + position: relative; } -.overlay { - @include media.media-breakpoint(mobile) { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: rgba(0 0 0 / 40%); - z-index: 1000; - } +.filterPopover { + position: absolute; + right: 0; + top: 0; + z-index: 11; +} + +.writeAnchor { + position: relative; + display: inline-flex; + align-items: center; +} + +.writePopover { + position: absolute; + right: 0; + top: 0; + width: 220px; + background: #fff; + border-radius: 24px; + box-shadow: 0 10px 24px rgba(0 0 0 / 12%); + overflow: hidden; + z-index: 50; +} + +.writeHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + border-bottom: 1px solid #e1e1e1; +} + +.writeTitle { + font-family: Pretendard, sans-serif; + font-size: 18px; + font-weight: 700; + color: #175c8e; +} + +.writeClose { + border: 0; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.writeBody { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px 32px; +} + +.writeOptionButton { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + padding: 12px 16px; + border: 1px solid #e1e1e1; + background: #fafafa; + border-radius: 999px; + text-decoration: none; +} + +.writeOptionText { + font-family: Pretendard, sans-serif; + font-size: 14px; + font-weight: 600; + color: #4b4b4b; } diff --git a/src/components/Articles/components/LostItemRouteButton/index.tsx b/src/components/Articles/components/LostItemRouteButton/index.tsx index 0d9c777c5..40a96f252 100644 --- a/src/components/Articles/components/LostItemRouteButton/index.tsx +++ b/src/components/Articles/components/LostItemRouteButton/index.tsx @@ -2,24 +2,48 @@ import { useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import CloseIcon from 'assets/svg/Articles/close.svg'; +import FilterIcon from 'assets/svg/Articles/filter.svg'; import FoundIcon from 'assets/svg/Articles/found.svg'; import LostIcon from 'assets/svg/Articles/lost.svg'; import PencilIcon from 'assets/svg/Articles/pencil.svg'; +import LostItemFilterBottomSheet from 'components/Articles/components/LostItemFilterBottomSheet'; +import { FilterState } from 'components/Articles/components/LostItemFilterContent'; +import LostItemFilterModal from 'components/Articles/components/LostItemFilterModal'; +import LostItemWriteBottomSheet from 'components/Articles/components/LostItemWriteBottomSheet'; import { useArticlesLogger } from 'components/Articles/hooks/useArticlesLogger'; +import { buildQueryFromFilter, LostItemParams, parseLostItemQuery } from 'components/Articles/utils/lostItemQuery'; import LoginRequiredModal from 'components/modal/LoginRequiredModal'; import ROUTES from 'static/routes'; +import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useModalPortal from 'utils/hooks/layout/useModalPortal'; import { useUser } from 'utils/hooks/state/useUser'; +import { useOutsideClick } from 'utils/hooks/ui/useOutsideClick'; import styles from './LostItemRouteButton.module.scss'; export default function LostItemRouteButton() { - const { logItemWriteClick, logFindUserWriteClick, logLostItemWriteClick, logLoginRequire } = useArticlesLogger(); - const [isWriting, setIsWriting] = useState(false); + const { + logItemWriteClick, + logFindUserWriteClick, + logLostItemWriteClick, + logLostItemWriteLoginRequest, + logLostItemFilter, + } = useArticlesLogger(); const router = useRouter(); - const { pathname } = router; + + const [isWriting, setIsWriting] = useState(false); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const portalManager = useModalPortal(); const { data: userInfo } = useUser(); + const isMobile = useMediaQuery(); + + const { containerRef: writeContainerRef } = useOutsideClick({ + onOutsideClick: () => { + setIsWriting(false); + }, + }); + const handleWritingButtonClick = () => { if (userInfo) { setIsWriting((prev) => !prev); @@ -32,58 +56,168 @@ export default function LostItemRouteButton() { title="게시글을 작성하기" description="로그인 후 분실물 주인을 찾아주세요!" onClose={portalOption.close} - onLoginClick={() => logLoginRequire('게시글 작성 팝업')} + onLoginClick={() => logLostItemWriteLoginRequest('로그인하기')} + onCancelClick={() => logLostItemWriteLoginRequest('닫기')} /> )); } }; - return ( - <> - {isWriting && ( -
setIsWriting(false)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - setIsWriting(false); - } - }} + const fallback: LostItemParams = { + page: 1, + type: null, + category: [], + foundStatus: 'ALL', + sort: 'LATEST', + author: 'ALL', + }; + + const params = parseLostItemQuery(router.query, fallback); + + const initialFilter: FilterState = { + author: params.author, + type: params.type ?? 'ALL', + category: params.category, + foundStatus: params.foundStatus, + }; + + const handleApply = (filter: FilterState) => { + if (filter.author === 'MY' && !userInfo) { + portalManager.open((portalOption) => ( + - )} -
- {isWriting && ( -
- logFindUserWriteClick()} - > - -
주인을 찾아요
- - - logLostItemWriteClick()} - > - -
잃어버렸어요
- -
- )} + )); + return; + } + + const nextQuery = buildQueryFromFilter(filter, { + page: 1, + sort: + (Array.isArray(router.query.sort) ? router.query.sort[0] : router.query.sort) === 'OLDEST' + ? 'OLDEST' + : 'LATEST', + }); + + router.push({ pathname: router.pathname, query: nextQuery }); + setIsFilterOpen(false); + }; + + const handleFilterButtonClick = () => { + setIsFilterOpen((prev) => !prev); + if (!isFilterOpen) { + logLostItemFilter(); + } + }; - {pathname.endsWith('articles') && ( + // 필터 버튼 렌더 -------------------------- + const renderFilter = () => { + if (isMobile) { + return ( + setIsFilterOpen(false)} + onApply={handleApply} + /> + ); + } + + if (!isFilterOpen) return null; + + return ( +
+ setIsFilterOpen(false)} + onApply={handleApply} + /> +
+ ); + }; + + // 글쓰기 버튼 렌더 -------------------------- + const renderWriteMenu = () => { + if (isMobile) { + return ( + <> + + setIsWriting(false)} + onFoundClick={logFindUserWriteClick} + onLostClick={logLostItemWriteClick} + /> + + ); + } + + return ( +
+ {!isWriting && ( + + )} + + {isWriting && ( +
+
+
글쓰기
+ +
+ +
+ { + logFindUserWriteClick(); + setIsWriting(false); + }} + > + + 주인을 찾아요 + + + { + logLostItemWriteClick(); + setIsWriting(false); + }} + > + + 잃어버렸어요 + +
+
)}
- + ); + }; + + return ( +
+
+ + + {renderFilter()} +
+ {renderWriteMenu()} +
); } diff --git a/src/components/Articles/components/LostItemWriteBottomSheet/LostItemWriteBottomSheet.module.scss b/src/components/Articles/components/LostItemWriteBottomSheet/LostItemWriteBottomSheet.module.scss new file mode 100644 index 000000000..f29e642ff --- /dev/null +++ b/src/components/Articles/components/LostItemWriteBottomSheet/LostItemWriteBottomSheet.module.scss @@ -0,0 +1,54 @@ +@use "src/utils/scss/media" as media; + +.container { + display: flex; + flex-direction: column; + padding: 18px 20px 20px; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 14px; +} + +.title { + font-family: Pretendard, sans-serif; + font-size: 18px; + font-weight: 600; + color: #000; +} + +.close { + border: 0; + background: transparent; + padding: 6px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.body { + display: flex; + flex-direction: column; + gap: 12px; + padding-top: 8px; + padding-bottom: 24px; +} + +.option { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 4px; + border-radius: 8px; + border: 1px solid #e1e1e1; + background: #f8f8fa; + text-decoration: none; + font-family: Pretendard, sans-serif; + font-size: 18px; + font-weight: 600; + color: #4b4b4b; +} diff --git a/src/components/Articles/components/LostItemWriteBottomSheet/index.tsx b/src/components/Articles/components/LostItemWriteBottomSheet/index.tsx new file mode 100644 index 000000000..baf27ecec --- /dev/null +++ b/src/components/Articles/components/LostItemWriteBottomSheet/index.tsx @@ -0,0 +1,55 @@ +import Link from 'next/link'; +import CloseIcon from 'assets/svg/Articles/close.svg'; +import FoundIcon from 'assets/svg/Articles/found.svg'; +import LostIcon from 'assets/svg/Articles/lost.svg'; +import BottomModal from 'components/ui/BottomModal'; +import ROUTES from 'static/routes'; +import styles from './LostItemWriteBottomSheet.module.scss'; + +interface Props { + isOpen: boolean; + onClose: () => void; + onFoundClick?: () => void; + onLostClick?: () => void; +} + +export default function LostItemWriteBottomSheet({ isOpen, onClose, onFoundClick, onLostClick }: Props) { + return ( + +
+
+
유형을 선택해 주세요
+ +
+ +
+ { + onFoundClick?.(); + onClose(); + }} + > + + 주인을 찾아요 + + + { + onLostClick?.(); + onClose(); + }} + > + + 잃어버렸어요 + +
+
+
+ ); +} diff --git a/src/components/Articles/components/Pagination/Pagination.module.scss b/src/components/Articles/components/Pagination/Pagination.module.scss index 2d1caa4d8..d03d85f16 100644 --- a/src/components/Articles/components/Pagination/Pagination.module.scss +++ b/src/components/Articles/components/Pagination/Pagination.module.scss @@ -26,8 +26,13 @@ letter-spacing: -0.7px; cursor: pointer; + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + @media (hover: hover) { - &:hover { + &:not(:disabled):hover { background-color: #175c8e; color: white; } diff --git a/src/components/Articles/components/Pagination/index.tsx b/src/components/Articles/components/Pagination/index.tsx index e0d1d45a5..4647105f9 100644 --- a/src/components/Articles/components/Pagination/index.tsx +++ b/src/components/Articles/components/Pagination/index.tsx @@ -1,21 +1,15 @@ import { cn } from '@bcsdlab/utils'; import usePagination from 'components/Articles/hooks/usePagination'; -import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; import styles from './Pagination.module.scss'; -const LIMIT_COUNT = [0, 1, 2, 3, 4]; - interface PaginationProps { totalPageNum: number; } export default function Pagination({ totalPageNum }: PaginationProps) { - const { calcIndexPage, onClickMove } = usePagination(); - const { params } = useParamsHandler(); + const { pages, currentPage, isFirst, isLast, movePage } = usePagination(totalPageNum); - const raw = Number(params.page) || 1; - const currentPage = Math.min(Math.max(raw, 1), Math.max(totalPageNum, 1)); - const totalPage = Array.from({ length: totalPageNum || 0 }, (_, i) => i + 1); + if (totalPageNum <= 0) return null; return (
@@ -23,47 +17,34 @@ export default function Pagination({ totalPageNum }: PaginationProps) { type="button" aria-label="이전 페이지로" className={styles.pagination__move} - onClick={onClickMove('prev', totalPageNum)} + onClick={() => movePage('prev')} + disabled={isFirst} > 이전으로 - {LIMIT_COUNT.length - 1 < totalPageNum - ? LIMIT_COUNT.map((limit) => ( - - - - )) - : totalPage.map((limit) => ( - - - - ))} + + {pages.map((pageNum) => ( + + ))} + diff --git a/src/components/Articles/hooks/useArticle.ts b/src/components/Articles/hooks/useArticle.ts deleted file mode 100644 index 98783fe4a..000000000 --- a/src/components/Articles/hooks/useArticle.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { articles } from 'api'; - -const useArticle = (id: string | undefined) => { - const { data: article } = useSuspenseQuery({ - queryKey: ['article', id], - queryFn: async ({ queryKey }) => { - const queryFnParams = queryKey[1]; - - return articles.getArticle(queryFnParams); - }, - }); - - return { article }; -}; - -export default useArticle; diff --git a/src/components/Articles/hooks/useArticles.ts b/src/components/Articles/hooks/useArticles.ts deleted file mode 100644 index 7ed998cc4..000000000 --- a/src/components/Articles/hooks/useArticles.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import { PaginationInfo } from 'api/articles/entity'; -import { articles as articlesApi } from 'api/index'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -const useArticles = (page = '1') => { - const token = useTokenState(); - - const { data: articleData } = useQuery({ - queryKey: ['articles', page], - queryFn: async () => { - // if (!token) throw new Error('🚨 로그인 토큰이 필요합니다.'); - - const queryFnParams = page; - - return articlesApi.getArticles(token, queryFnParams); - }, - placeholderData: keepPreviousData, - select: (data) => { - const { - // 일관성을 유지하기 위해 변수명을 변경하지 않았습니다. - articles, - total_count, - current_count, - total_page, - current_page, - } = data; - - const paginationInfo: PaginationInfo = { - total_count, - current_count, - total_page, - current_page, - }; - - return { articles, paginationInfo }; - }, - }); - - return articleData; -}; - -export default useArticles; diff --git a/src/components/Articles/hooks/useArticlesLogger.ts b/src/components/Articles/hooks/useArticlesLogger.ts index 7ca45de43..7b7487313 100644 --- a/src/components/Articles/hooks/useArticlesLogger.ts +++ b/src/components/Articles/hooks/useArticlesLogger.ts @@ -35,7 +35,7 @@ const CLICK_EVENTS = [ }, { label: 'find_user_delete', - value: '삭제', + value: '', // {”분실물”, ”습득물”} }, { label: 'find_user_delete_confirm', @@ -57,6 +57,46 @@ const CLICK_EVENTS = [ label: 'login_prompt', value: '', // {"게시글 작성 팝업", "쪽지 보내기 팝업"} }, + { + label: 'lost_item_filter', + value: '분실물', + }, + { + label: 'lost_item_filter_apply', + value: '분실물', + }, + { + label: 'lost_item_post_entry', + value: '', // {"분실물", "습득물"} + }, + { + label: 'lost_item_message_login_request', + value: '', // {"닫기", "로그인하기"} + }, + { + label: 'lost_item_write_login_request', + value: '', // {"닫기", "로그인하기"} + }, + { + label: 'lost_item_state_change', + value: '', + }, + { + label: 'lost_item_found', + value: '', + }, + { + label: 'lost_item_modify', + value: '', + }, + { + label: 'lost_item_modify_complete', + value: '', + }, + { + label: 'item_message_send', + value: '', // {"분실물 쪽지 보내기", "습득물 쪽지 보내기"} + }, ] as const; export type ClickEventLabel = (typeof CLICK_EVENTS)[number]['label']; @@ -89,13 +129,25 @@ export const useArticlesLogger = () => { const logLostItemAddItemClick = () => logEvent('lost_item_add_item'); const logFindUserWriteConfirmClick = () => logEvent('find_user_write_confirm'); const logLostItemWriteConfirmClick = () => logEvent('lost_item_write_confirm'); - const logFindUserDeleteClick = () => logEvent('find_user_delete'); + const logFindUserDeleteClick = (value: '분실물' | '습득물') => logEvent('find_user_delete', value); const logFindUserDeleteConfirmClick = () => logEvent('find_user_delete_confirm'); const logFindUserCategory = (category: FindUserCategory) => logEvent('find_user_category', category); const logLostItemCategory = (category: FindUserCategory) => logEvent('lost_item_category', category); const logItemPostReportClick = () => logEvent('item_post_report'); const logItemPostReportConfirm = () => logEvent('item_post_report_confirm'); const logLoginRequire = (category: LoginPopUpCategory) => logEvent('login_prompt', category); + const logLostItemFilter = () => logEvent('lost_item_filter'); + const logLostItemFilterApply = () => logEvent('lost_item_filter_apply'); + const logLostItemPostEntry = (value: '분실물' | '습득물') => logEvent('lost_item_post_entry', value); + const logLostItemMessageLoginRequest = (value: '닫기' | '로그인하기') => + logEvent('lost_item_message_login_request', value); + const logLostItemWriteLoginRequest = (value: '닫기' | '로그인하기') => + logEvent('lost_item_write_login_request', value); + const logLostItemStateChange = (value: '분실물' | '습득물') => logEvent('lost_item_state_change', value); + const logLostItemFound = (value: '분실물' | '습득물') => logEvent('lost_item_found', value); + const logLostItemModify = (value: '분실물' | '습득물') => logEvent('lost_item_modify', value); + const logLostItemModifyComplete = (value: '분실물' | '습득물') => logEvent('lost_item_modify_complete', value); + const logItemMessageSend = (value: '분실물 쪽지 보내기' | '습득물 쪽지 보내기') => logEvent('item_message_send', value); return { logItemWriteClick, @@ -112,5 +164,15 @@ export const useArticlesLogger = () => { logItemPostReportClick, logItemPostReportConfirm, logLoginRequire, + logLostItemFilter, + logLostItemFilterApply, + logLostItemPostEntry, + logLostItemMessageLoginRequest, + logLostItemWriteLoginRequest, + logLostItemStateChange, + logLostItemFound, + logLostItemModify, + logLostItemModifyComplete, + logItemMessageSend, }; }; diff --git a/src/components/Articles/hooks/useDeleteLostItemArticles.ts b/src/components/Articles/hooks/useDeleteLostItemArticles.ts index b1a4cffe4..c6f74235f 100644 --- a/src/components/Articles/hooks/useDeleteLostItemArticles.ts +++ b/src/components/Articles/hooks/useDeleteLostItemArticles.ts @@ -1,6 +1,6 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteLostItemArticle } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; @@ -11,11 +11,12 @@ interface UseDeleteLostItemArticleProps { const useDeleteLostItemArticle = ({ onSuccess }: UseDeleteLostItemArticleProps = {}) => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.deleteLostItem(queryClient, token); const { mutate } = useMutation({ - mutationFn: (id: number) => deleteLostItemArticle(token, id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['articles', 'lostitem'] }); + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '게시글이 삭제되었습니다.'); onSuccess?.(); }, diff --git a/src/components/Articles/hooks/useLostItemArticles.ts b/src/components/Articles/hooks/useLostItemArticles.ts deleted file mode 100644 index f3ba78e80..000000000 --- a/src/components/Articles/hooks/useLostItemArticles.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { articles } from 'api'; -import { ItemArticleRequestDTO } from 'api/articles/entity'; -import { transformLostItemArticles } from 'components/Articles/utils/transform'; -import useTokenState from 'utils/hooks/state/useTokenState'; - -const useLostItemArticles = (data: ItemArticleRequestDTO) => { - const token = useTokenState(); - - const { data: lostItemArticles } = useSuspenseQuery({ - queryKey: ['lostItem', data], - queryFn: async () => { - const response = await articles.getLostItemArticles(token, data); - return transformLostItemArticles(response); - }, - }); - - return { lostItemArticles }; -}; - -export default useLostItemArticles; diff --git a/src/components/Articles/hooks/useLostItemForm.ts b/src/components/Articles/hooks/useLostItemForm.ts index ccb3b25be..06b9b0ffe 100644 --- a/src/components/Articles/hooks/useLostItemForm.ts +++ b/src/components/Articles/hooks/useLostItemForm.ts @@ -1,7 +1,8 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { FindUserCategory } from './useArticlesLogger'; export interface LostItem { + id: number; type: 'FOUND' | 'LOST'; category: FindUserCategory | ''; foundDate: Date; @@ -31,8 +32,9 @@ export interface LostItemHandler { checkIsFoundPlaceSelected: () => void; } -const initialForm: LostItem = { - type: 'FOUND', +const createInitialForm = (id: number, type: 'FOUND' | 'LOST'): LostItem => ({ + id, + type, category: '', foundDate: new Date(), foundPlace: '', @@ -41,99 +43,68 @@ const initialForm: LostItem = { images: [], registered_at: '', updated_at: '', - hasDateBeenSelected: false, + hasDateBeenSelected: true, isCategorySelected: true, isDateSelected: true, isFoundPlaceSelected: true, -}; +}); -export const useLostItemForm = (defaultType: 'FOUND' | 'LOST') => { - const [lostItems, setLostItems] = useState>([{ ...initialForm, type: defaultType }]); +interface UseLostItemFormOptions { + defaultType: 'FOUND' | 'LOST'; + initialItems?: LostItem[]; +} - const lostItemHandler = (key: number) => ({ - setType: (type: 'FOUND' | 'LOST') => { - setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].type = type; - return newLostItems; - }); - }, - setCategory: (category: FindUserCategory) => { - setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].category = category; - return newLostItems; - }); - }, - setFoundDate: (date: Date) => { - setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].foundDate = date; - return newLostItems; - }); - }, - setFoundPlace: (foundPlace: string) => { - setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].foundPlace = foundPlace; - return newLostItems; - }); - }, - setContent: (content: string) => { - setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].content = content; - return newLostItems; - }); - }, - setAuthor: (author: string) => { - setLostItems((prev) => { - const newLostItems = [...prev]; - if (newLostItems[key].author !== author) { - newLostItems[key].author = author; - } - return newLostItems; - }); - }, - setImages: (images: Array) => { - setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].images = images; - return newLostItems; - }); - }, - setHasDateBeenSelected: () => { - setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].hasDateBeenSelected = true; - return newLostItems; - }); - }, +export const useLostItemForm = ({ defaultType, initialItems }: UseLostItemFormOptions) => { + const idCounter = useRef(initialItems?.length ?? 1); + const [lostItems, setLostItems] = useState>(initialItems ?? [createInitialForm(0, defaultType)]); + + const updateItem = (index: number, updates: Partial) => { + setLostItems((prev) => { + const newItems = [...prev]; + newItems[index] = { ...newItems[index], ...updates }; + return newItems; + }); + }; + + const lostItemHandler = (key: number): LostItemHandler => ({ + setType: (type) => updateItem(key, { type }), + setCategory: (category) => updateItem(key, { category }), + setFoundDate: (foundDate) => updateItem(key, { foundDate }), + setFoundPlace: (foundPlace) => updateItem(key, { foundPlace }), + setContent: (content) => updateItem(key, { content }), + setAuthor: (author) => updateItem(key, { author }), + setImages: (images) => updateItem(key, { images }), + setHasDateBeenSelected: () => updateItem(key, { hasDateBeenSelected: true }), checkIsCategorySelected: () => { setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].isCategorySelected = newLostItems[key].category.trim() !== ''; - return newLostItems; + const newItems = [...prev]; + const item = newItems[key]; + newItems[key] = { ...item, isCategorySelected: item.category.trim() !== '' }; + return newItems; }); }, checkIsDateSelected: () => { setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].isDateSelected = newLostItems[key].hasDateBeenSelected; - return newLostItems; + const newItems = [...prev]; + const item = newItems[key]; + newItems[key] = { ...item, isDateSelected: item.hasDateBeenSelected }; + return newItems; }); }, checkIsFoundPlaceSelected: () => { setLostItems((prev) => { - const newLostItems = [...prev]; - newLostItems[key].isFoundPlaceSelected = newLostItems[key].foundPlace.trim() !== ''; - return newLostItems; + const newItems = [...prev]; + const item = newItems[key]; + newItems[key] = { ...item, isFoundPlaceSelected: item.foundPlace.trim() !== '' }; + return newItems; }); }, }); const addLostItem = () => { - setLostItems((prev) => [...prev, { ...initialForm }]); + const newId = idCounter.current; + idCounter.current += 1; + setLostItems((prev) => [...prev, createInitialForm(newId, defaultType)]); }; const removeLostItem = (key: number) => { diff --git a/src/components/Articles/hooks/usePageParams.ts b/src/components/Articles/hooks/usePageParams.ts deleted file mode 100644 index 9c18cb586..000000000 --- a/src/components/Articles/hooks/usePageParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; - -const usePageParams = () => { - const { params } = useParamsHandler(); - - return params.page ?? '1'; -}; - -export default usePageParams; diff --git a/src/components/Articles/hooks/usePagination.ts b/src/components/Articles/hooks/usePagination.ts index 1b07a468a..f6ecc3f39 100644 --- a/src/components/Articles/hooks/usePagination.ts +++ b/src/components/Articles/hooks/usePagination.ts @@ -1,66 +1,41 @@ +import { useMemo } from 'react'; import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; -import showToast from 'utils/ts/showToast'; const PAGE_LIMIT = 5; -const displayCorrectionNum = (totalPageNum: number, nowPageNum: number) => { - if (totalPageNum <= PAGE_LIMIT) return 0; - - if (nowPageNum <= Math.ceil(PAGE_LIMIT / 2)) { - return 0; - } - if (totalPageNum - nowPageNum <= Math.floor(PAGE_LIMIT / 2)) { - return totalPageNum - PAGE_LIMIT; - } +const usePagination = (totalPageNum: number) => { + const { params, setParams } = useParamsHandler(); - return nowPageNum - Math.ceil(PAGE_LIMIT / 2); -}; + const raw = Number(params.page) || 1; + const currentPage = Math.min(Math.max(raw, 1), Math.max(totalPageNum, 1)); -type MoveType = string | 'prev' | 'next'; + const pages = useMemo(() => { + const half = Math.floor(PAGE_LIMIT / 2); + const maxStart = Math.max(1, totalPageNum - PAGE_LIMIT + 1); + const startPage = Math.min(Math.max(1, currentPage - half), maxStart); + const length = Math.min(PAGE_LIMIT, totalPageNum); -const usePagination = () => { - const { params, setParams } = useParamsHandler(); + return Array.from({ length }, (_, i) => startPage + i); + }, [currentPage, totalPageNum]); - const calcIndexPage = (limit: number, totalPageNum: number, page: string) => - String(limit + 1 + displayCorrectionNum(totalPageNum, Number(page))); + const movePage = (target: number | 'prev' | 'next') => { + let nextPage = currentPage; - const onClickMove = (move: MoveType, totalPageNum?: number) => () => { - const currentPage = Number(params.page) || 1; - let targetPage = currentPage; + if (target === 'prev') nextPage = Math.max(1, currentPage - 1); + else if (target === 'next') nextPage = Math.min(totalPageNum, currentPage + 1); + else nextPage = target; - if (move === 'prev') { - if (currentPage <= 1) { - showToast('error', '첫 페이지입니다.'); - targetPage = 1; - } else { - targetPage = currentPage - 1; - } - } else if (move === 'next') { - const maxPage = totalPageNum || 1; - if (currentPage >= maxPage) { - showToast('error', '마지막 페이지입니다.'); - targetPage = maxPage; - } else { - targetPage = currentPage + 1; - } - } else { - targetPage = Number(move); - } + if (nextPage === currentPage) return; - setParams( - { - page: String(targetPage), - }, - { - deleteBeforeParam: false, - replacePage: true, - }, - ); + setParams({ page: String(nextPage) }, { replacePage: true }); }; return { - calcIndexPage, - onClickMove, + pages, + currentPage, + isFirst: currentPage <= 1, + isLast: currentPage >= totalPageNum, + movePage, }; }; diff --git a/src/components/Articles/hooks/usePostLostItemArticles.ts b/src/components/Articles/hooks/usePostLostItemArticles.ts index 5e1000611..1ac09c775 100644 --- a/src/components/Articles/hooks/usePostLostItemArticles.ts +++ b/src/components/Articles/hooks/usePostLostItemArticles.ts @@ -1,22 +1,18 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postLostItemArticle } from 'api/articles'; -import { transformLostItemArticlesForPost } from 'components/Articles/utils/transform'; -import { LostItemArticlesForPost } from 'static/articles'; +import { articleMutations } from 'api/articles/mutations'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; const usePostLostItemArticles = () => { const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.createLostItem(queryClient, token); const { status, mutateAsync } = useMutation({ - mutationFn: async (data: LostItemArticlesForPost) => { - const response = await postLostItemArticle(token, transformLostItemArticlesForPost(data)); - return response.id; - }, - onSuccess: () => { + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); showToast('success', '게시글 작성이 완료되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['lostItem'] }); }, onError: (e) => { if (isKoinError(e)) { diff --git a/src/components/Articles/hooks/useReportLostItemArticle.ts b/src/components/Articles/hooks/useReportLostItemArticle.ts index 41df9e013..186e81ba2 100644 --- a/src/components/Articles/hooks/useReportLostItemArticle.ts +++ b/src/components/Articles/hooks/useReportLostItemArticle.ts @@ -1,20 +1,23 @@ +import { useRouter } from 'next/router'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postReportLostItemArticle } from 'api/articles'; +import { articleMutations } from 'api/articles/mutations'; +import ROUTES from 'static/routes'; import useTokenState from 'utils/hooks/state/useTokenState'; import showToast from 'utils/ts/showToast'; export default function useReportLostItemArticle() { + const router = useRouter(); const token = useTokenState(); const queryClient = useQueryClient(); + const mutation = articleMutations.reportLostItem(queryClient, token); return useMutation({ - mutationFn: ({ articleId, reports }: { articleId: number; reports: { title: string; content: string }[] }) => - postReportLostItemArticle(token, articleId, { reports }), - onSuccess: () => { + ...mutation, + onSuccess: async (...args) => { + await mutation.onSuccess?.(...args); + router.push(ROUTES.LostItems()); showToast('success', '게시글이 신고되었습니다.'); - queryClient.invalidateQueries({ queryKey: ['articles'] }); - queryClient.invalidateQueries({ queryKey: ['lostitem'] }); // 다시 패치할 필요가 있는지? // queryClient.refetchQueries({ queryKey: ['articles', 'lostitem'] }); diff --git a/src/components/Articles/utils/convertArticlesTag.ts b/src/components/Articles/utils/convertArticlesTag.ts index 23cb3d20f..5ffb4feec 100644 --- a/src/components/Articles/utils/convertArticlesTag.ts +++ b/src/components/Articles/utils/convertArticlesTag.ts @@ -44,4 +44,5 @@ export const BOARD_IDS = { 홍보게시판: 11, 현장실습: 12, 학생생활: 13, + 분실물: 14, } as const; diff --git a/src/components/Articles/utils/lostItemQuery.ts b/src/components/Articles/utils/lostItemQuery.ts new file mode 100644 index 000000000..63a35519b --- /dev/null +++ b/src/components/Articles/utils/lostItemQuery.ts @@ -0,0 +1,142 @@ +import { LostItemAuthor, LostItemCategory, LostItemFoundStatus, LostItemSort, LostItemType } from 'api/articles/entity'; +import { Category, FilterState } from 'components/Articles/components/LostItemFilterContent'; +import type { ParsedUrlQuery, ParsedUrlQueryInput } from 'querystring'; + +/* +[제작 배경] +1. API의 type(LOST / FOUND)에는 ALL 개념이 없지만, UI에서는 '전체' 상태가 필요하여 별도의 변환 로직이 필요함 +2. 해당 로직은 SSR(getServerSideProps)과 클라이언트 API 요청에서 공통으로 사용되므로 유틸로 분리함 +3. 쿼리 파싱 과정에서 발생하는 타입 불일치를 명시적인 타입 가드로 해결하기 위함 + - category는 복수 선택 가능 + - category가 빈 배열([])이면 '전체'로 간주 + +[타입 역할] + - LostItemParams: UI 및 라우터 쿼리 기준의 필터 타입 + - LostItemArticlesRequest: API 요청에 사용되는 필터 타입 +*/ + +type QueryValue = ParsedUrlQuery[keyof ParsedUrlQuery]; + +export function toArray(v: QueryValue): string[] { + if (!v) return []; + return Array.isArray(v) ? v : [v]; +} + +type LostItemSelectableCategory = Exclude; + +export type LostItemParams = { + page: number; + type: LostItemType | null; + category: LostItemSelectableCategory[]; + foundStatus: LostItemFoundStatus; + sort: LostItemSort; + author: LostItemAuthor; +}; + +const TYPE_VALUES = ['LOST', 'FOUND'] as const; +const SORT_VALUES = ['LATEST', 'OLDEST'] as const; +const FOUND_STATUS_VALUES = ['ALL', 'FOUND', 'NOT_FOUND'] as const; +const AUTHOR_VALUES = ['ALL', 'MY'] as const; +const CATEGORY_VALUES = ['CARD', 'ID', 'WALLET', 'ELECTRONICS', 'ETC'] as const; + +const isLostItemType = (v: string): v is LostItemType => (TYPE_VALUES as readonly string[]).includes(v); +const isLostItemSort = (v: string): v is LostItemSort => (SORT_VALUES as readonly string[]).includes(v); +const isLostItemFoundStatus = (v: string): v is LostItemFoundStatus => + (FOUND_STATUS_VALUES as readonly string[]).includes(v); +const isLostItemAuthor = (v: string): v is LostItemAuthor => (AUTHOR_VALUES as readonly string[]).includes(v); +const isSelectableCategory = (v: string): v is LostItemSelectableCategory => + (CATEGORY_VALUES as readonly string[]).includes(v); + +const MAX_PAGE = 9999; + +function parsePage(v: QueryValue, fallback: number) { + const raw = Array.isArray(v) ? v[0] : v; + const n = Number(raw); + + if (!Number.isFinite(n)) return fallback; + + const int = Math.floor(n); + return Math.min(Math.max(int, 1), MAX_PAGE); +} + +export function parseLostItemQuery(query: ParsedUrlQuery, fallback: LostItemParams): LostItemParams { + const pageRaw = query.page; + const typeRaw = query.type; + const categoryRaw = query.category; + const foundStatusRaw = query.foundStatus; + const sortRaw = query.sort; + const authorRaw = query.author; + + const page = parsePage(pageRaw, fallback.page); + + const typeCandidate = Array.isArray(typeRaw) ? typeRaw[0] : typeRaw; + const type = typeCandidate && isLostItemType(typeCandidate) ? typeCandidate : fallback.type; + + const category = toArray(categoryRaw).filter(isSelectableCategory); + + const foundStatusCandidate = Array.isArray(foundStatusRaw) ? foundStatusRaw[0] : foundStatusRaw; + const foundStatus = + foundStatusCandidate && isLostItemFoundStatus(foundStatusCandidate) ? foundStatusCandidate : fallback.foundStatus; + + const sortCandidate = Array.isArray(sortRaw) ? sortRaw[0] : sortRaw; + const sort = sortCandidate && isLostItemSort(sortCandidate) ? sortCandidate : fallback.sort; + + const authorCandidate = Array.isArray(authorRaw) ? authorRaw[0] : authorRaw; + const author = authorCandidate && isLostItemAuthor(authorCandidate) ? authorCandidate : fallback.author; + + return { page, type, category, foundStatus, sort, author }; +} + +export function buildLostItemQuery(params: LostItemParams): ParsedUrlQueryInput { + const q: ParsedUrlQueryInput = { + page: params.page, + foundStatus: params.foundStatus, + sort: params.sort, + author: params.author, + }; + + if (params.type) q.type = params.type; + if (params.category.length > 0) q.category = params.category; + return q; +} + +export function parseFilterStateFromQuery(query: ParsedUrlQuery): FilterState { + const typeRaw = Array.isArray(query.type) ? query.type[0] : query.type; + + const type: FilterState['type'] = typeRaw === 'LOST' || typeRaw === 'FOUND' ? typeRaw : 'ALL'; + + const category = toArray(query.category).filter((v): v is Category => + (['CARD', 'ID', 'WALLET', 'ELECTRONICS', 'ETC'] as const).includes(v as Category), + ); + + const foundStatusRaw = Array.isArray(query.foundStatus) ? query.foundStatus[0] : query.foundStatus; + const foundStatus: FilterState['foundStatus'] = + foundStatusRaw === 'FOUND' || foundStatusRaw === 'NOT_FOUND' ? foundStatusRaw : 'ALL'; + + const authorRaw = Array.isArray(query.author) ? query.author[0] : query.author; + const author: FilterState['author'] = authorRaw === 'MY' ? 'MY' : 'ALL'; + + return { type, category, foundStatus, author }; +} + +export function buildQueryFromFilter( + filter: FilterState, + base: { page: number; sort: LostItemSort }, +): ParsedUrlQueryInput { + const q: ParsedUrlQueryInput = { + page: base.page, + sort: base.sort, + foundStatus: filter.foundStatus, + author: filter.author, + }; + + if (filter.type !== 'ALL') { + q.type = filter.type; + } + + if (filter.category.length > 0) { + q.category = filter.category; + } + + return q; +} diff --git a/src/components/Articles/utils/selectArticlesData.ts b/src/components/Articles/utils/selectArticlesData.ts new file mode 100644 index 000000000..e4b21b6b8 --- /dev/null +++ b/src/components/Articles/utils/selectArticlesData.ts @@ -0,0 +1,56 @@ +import { isNewArticle } from './setArticleRegisteredDate'; +import type { + ArticleWithNew, + ArticlesResponse, + LostItemArticleForGetDTO, + LostItemArticlesResponseDTO, + PaginationInfo, +} from 'api/articles/entity'; + +export interface ArticlesListViewData { + articles: ArticleWithNew[]; + paginationInfo: PaginationInfo; +} + +export interface LostItemPaginationViewData { + articles: LostItemArticleForGetDTO[]; + paginationInfo: PaginationInfo; +} + +export const selectArticlesWithNew = (data: ArticlesResponse): ArticlesListViewData => { + const { + articles, + total_count, + current_count, + total_page, + current_page, + } = data; + + const currentDate = new Date(); + const articlesWithNew: ArticleWithNew[] = articles.map((article) => ({ + ...article, + isNew: isNewArticle(article.registered_at, currentDate), + })); + + return { + articles: articlesWithNew, + paginationInfo: { + total_count, + current_count, + total_page, + current_page, + }, + }; +}; + +export const selectLostItemPaginationData = ( + data: LostItemArticlesResponseDTO, +): LostItemPaginationViewData => ({ + articles: data.articles, + paginationInfo: { + total_count: data.total_count, + current_count: data.current_count, + total_page: data.total_page, + current_page: data.current_page, + }, +}); diff --git a/src/components/Articles/utils/setArticleRegisteredDate.ts b/src/components/Articles/utils/setArticleRegisteredDate.ts index 45bee46c9..6743e7f0f 100644 --- a/src/components/Articles/utils/setArticleRegisteredDate.ts +++ b/src/components/Articles/utils/setArticleRegisteredDate.ts @@ -1,15 +1,3 @@ -const isCheckNewArticle = (registered: number[]) => { - const today = new Date(); - - if ( - registered[0] - today.getFullYear() === 0 && - registered[1] - today.getMonth() === 1 && - today.getDate() - registered[2] <= 4 - ) - return true; - return false; -}; - const convertDate = (time: string) => { if (typeof time !== 'string') { return ''; @@ -17,15 +5,25 @@ const convertDate = (time: string) => { return time.split(' ')[0].replaceAll('-', '.'); }; -function setArticleRegisteredDate(time: string) { - const registered = convertDate(time) +export const isNewArticle = (registeredAt: string, currentDate: Date) => { + const registered = convertDate(registeredAt) .split('.') .map((item: string) => parseInt(item, 10)); - if (isCheckNewArticle(registered)) { - return [`${registered.join('.')}`, true]; + if ( + registered[0] - currentDate.getFullYear() === 0 && + registered[1] - currentDate.getMonth() === 1 && + currentDate.getDate() - registered[2] <= 4 + ) { + return true; } - return [`${registered.join('.')}`, false]; -} + return false; +}; + +const setArticleRegisteredDate = (registeredAt: string): [string, boolean] => { + const formattedDate = convertDate(registeredAt); + const isNew = isNewArticle(registeredAt, new Date()); + return [formattedDate, isNew]; +}; export default setArticleRegisteredDate; diff --git a/src/components/Articles/utils/transform.ts b/src/components/Articles/utils/transform.ts deleted file mode 100644 index 6bf05b292..000000000 --- a/src/components/Articles/utils/transform.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - LostItemArticlesRequestDTO, - LostItemArticlesResponseDTO, - SingleLostItemArticleResponseDTO, - ReportItemArticleRequestDTO, -} from 'api/articles/entity'; -import { - LostItemArticlesForGet, - LostItemArticlesForPost, - SingleLostItemArticle, - LostItemArticlesReportForPost, -} from 'static/articles'; - -export const transformLostItemArticles = (dto: LostItemArticlesResponseDTO): LostItemArticlesForGet => ({ - articles: dto.articles.map((article) => ({ - id: article.id, - boardId: article.board_id, - type: article.type, - category: article.category, - foundPlace: article.found_place, - foundDate: article.found_date, - content: article.content, - author: article.author, - registeredAt: article.registered_at, - is_reported: article.is_reported, - updatedAt: article.updated_at, - })), - totalCount: dto.total_count, - currentCount: dto.current_count, - totalPage: dto.total_page, - currentPage: dto.current_page, -}); - -export const transformSingleLostItemArticle = (dto: SingleLostItemArticleResponseDTO): SingleLostItemArticle => ({ - id: dto.id, - boardId: dto.board_id, - type: dto.type, - category: dto.category, - foundPlace: dto.found_place, - foundDate: dto.found_date, - content: dto.content, - author: dto.author, - is_council: dto.is_council, - is_mine: dto.is_mine, - images: dto.images.map((image) => ({ - id: image.id, - imageUrl: image.image_url, - })), - prevId: dto.prev_id, - nextId: dto.next_id, - registeredAt: dto.registered_at, - updatedAt: dto.updated_at, -}); - -export const transformLostItemArticlesForPost = (dto: LostItemArticlesForPost): LostItemArticlesRequestDTO => ({ - articles: dto.articles.map((article) => ({ - type: article.type, - category: article.category, - found_place: article.foundPlace, - found_date: article.foundDate, - content: article.content, - images: article.images, - registered_at: article.registered_at, - updated_at: article.updated_at, - })), -}); - -export const transformLostItemArticleReportForPost = ( - dto: LostItemArticlesReportForPost, -): ReportItemArticleRequestDTO => ({ - reports: dto.reports.map((report) => ({ - title: report.title, - content: report.content, - })), -}); diff --git a/src/components/Auth/LoginPage/components/AdditionalLink/index.tsx b/src/components/Auth/LoginPage/components/AdditionalLink/index.tsx index 091cd7a94..3d6d2c829 100644 --- a/src/components/Auth/LoginPage/components/AdditionalLink/index.tsx +++ b/src/components/Auth/LoginPage/components/AdditionalLink/index.tsx @@ -38,7 +38,7 @@ export default function AdditionalLink() { @@ -69,14 +69,14 @@ export default function AdditionalLink() { 비밀번호 찾기 { sessionLogger.actionSessionEvent({ session_name: 'sign_up', diff --git a/src/components/Auth/LoginPage/components/LoginForm/index.tsx b/src/components/Auth/LoginPage/components/LoginForm/index.tsx index 4a5a10923..62ca40684 100644 --- a/src/components/Auth/LoginPage/components/LoginForm/index.tsx +++ b/src/components/Auth/LoginPage/components/LoginForm/index.tsx @@ -80,7 +80,7 @@ export default function LoginForm() { {isMobile && ( { sessionLogger.actionSessionEvent({ session_name: 'sign_up', diff --git a/src/components/Auth/LoginPage/hooks/useLogin.ts b/src/components/Auth/LoginPage/hooks/useLogin.ts index 294f40973..147775988 100644 --- a/src/components/Auth/LoginPage/hooks/useLogin.ts +++ b/src/components/Auth/LoginPage/hooks/useLogin.ts @@ -1,7 +1,8 @@ import { isKoinError, sendClientError } from '@bcsdlab/koin'; import { sha256 } from '@bcsdlab/utils'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { auth } from 'api'; +import { login } from 'api/auth'; +import { COOKIE_KEY } from 'static/url'; import useLogger from 'utils/hooks/analytics/useLogger'; import { useLoginRedirect } from 'utils/hooks/auth/useLoginRedirect'; import { getCookieDomain, setCookie } from 'utils/ts/cookie'; @@ -26,7 +27,7 @@ export const useLogin = (state: IsAutoLogin) => { const logger = useLogger(); const postLogin = useMutation({ - mutationFn: auth.login, + mutationFn: login, onSuccess: (data: LoginResponse) => { const domain = getCookieDomain(); @@ -39,8 +40,8 @@ export const useLogin = (state: IsAutoLogin) => { setRefreshToken(data.refresh_token); } queryClient.invalidateQueries(); - setCookie('AUTH_TOKEN_KEY', data.token, { domain }); - setCookie('AUTH_USER_TYPE', data.user_type, { domain }); + setCookie(COOKIE_KEY.AUTH_TOKEN, data.token, { domain }); + setCookie(COOKIE_KEY.AUTH_USER_TYPE, data.user_type, { domain }); setToken(data.token); setUserType(data.user_type); redirectAfterLogin(); @@ -63,7 +64,7 @@ export const useLogin = (state: IsAutoLogin) => { }, }); - const login = async (userInfo: UserInfo) => { + const submitLogin = async (userInfo: UserInfo) => { const hashedPassword = await sha256(userInfo.login_pw); if (userInfo.login_id === '') { @@ -81,5 +82,5 @@ export const useLogin = (state: IsAutoLogin) => { }); }; - return login; + return submitLogin; }; diff --git a/src/components/Auth/ModifyInfoPage/hooks/useUserDelete.ts b/src/components/Auth/ModifyInfoPage/hooks/useUserDelete.ts index f03757589..f4939b215 100644 --- a/src/components/Auth/ModifyInfoPage/hooks/useUserDelete.ts +++ b/src/components/Auth/ModifyInfoPage/hooks/useUserDelete.ts @@ -1,5 +1,5 @@ import { useMutation } from '@tanstack/react-query'; -import * as api from 'api'; +import { deleteUser } from 'api/auth'; import { AxiosError } from 'axios'; import { useLogout } from 'utils/hooks/auth/useLogout'; import showToast from 'utils/ts/showToast'; @@ -7,7 +7,7 @@ import showToast from 'utils/ts/showToast'; const useUserDelete = () => { const logout = useLogout(); const mutation = useMutation({ - mutationFn: api.auth.deleteUser, + mutationFn: deleteUser, onSuccess: () => { showToast('success', '성공적으로 계정을 삭제하였습니다.'); logout(); diff --git a/src/components/Auth/SignupPage/Steps/MobileStudentDetailStep/index.tsx b/src/components/Auth/SignupPage/Steps/MobileStudentDetailStep/index.tsx index 0ac3c5a2e..17fe2940d 100644 --- a/src/components/Auth/SignupPage/Steps/MobileStudentDetailStep/index.tsx +++ b/src/components/Auth/SignupPage/Steps/MobileStudentDetailStep/index.tsx @@ -2,8 +2,9 @@ import { useState } from 'react'; import { isKoinError } from '@bcsdlab/koin'; import { sha256 } from '@bcsdlab/utils'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { checkId, nicknameDuplicateCheck, signupStudent } from 'api/auth'; +import { deptQueries } from 'api/dept/queries'; import { Controller, ControllerRenderProps, FieldError, useFormContext, useFormState, useWatch } from 'react-hook-form'; import { REGEX, MESSAGES } from 'static/auth'; import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; @@ -11,7 +12,6 @@ import useBooleanState from 'utils/hooks/state/useBooleanState'; import showToast from 'utils/ts/showToast'; import CustomInput, { type InputMessage } from '../../components/CustomInput'; import CustomSelector from '../../components/CustomSelector'; -import useDeptList from '../../hooks/useDeptList'; import styles from './MobileStudentDetailStep.module.scss'; interface MobileVerificationProps { @@ -51,7 +51,7 @@ function MobileStudentDetailStep({ onNext }: MobileVerificationProps) { const isFormFilled = isIdPasswordValid && major && (!nicknameControl || isCorrectNickname); - const { data: deptList } = useDeptList(); + const { data: deptList } = useSuspenseQuery(deptQueries.list()); const deptOptionList = deptList.map((dept) => ({ label: dept.name, value: dept.name, diff --git a/src/components/Auth/SignupPage/Steps/StudentDetailStep/index.tsx b/src/components/Auth/SignupPage/Steps/StudentDetailStep/index.tsx index 10902fafb..591ecccd0 100644 --- a/src/components/Auth/SignupPage/Steps/StudentDetailStep/index.tsx +++ b/src/components/Auth/SignupPage/Steps/StudentDetailStep/index.tsx @@ -1,12 +1,12 @@ import { useState } from 'react'; import { isKoinError } from '@bcsdlab/koin'; import { cn, sha256 } from '@bcsdlab/utils'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { checkId, emailDuplicateCheck, nicknameDuplicateCheck, signupStudent } from 'api/auth'; +import { deptQueries } from 'api/dept/queries'; import BackIcon from 'assets/svg/arrow-back.svg'; import CustomSelector from 'components/Auth/SignupPage/components/CustomSelector'; import PCCustomInput, { type InputMessage } from 'components/Auth/SignupPage/components/PCCustomInput'; -import useDeptList from 'components/Auth/SignupPage/hooks/useDeptList'; import { Controller, FieldError, useFormContext, useFormState, useWatch } from 'react-hook-form'; import { REGEX, MESSAGES } from 'static/auth'; import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; @@ -65,7 +65,7 @@ function StudentDetail({ onNext, onBack }: VerificationProps) { studentNumber && !errors.student_number; - const { data: deptList } = useDeptList(); + const { data: deptList } = useSuspenseQuery(deptQueries.list()); const deptOptionList = deptList.map((dept) => ({ label: dept.name, value: dept.name, diff --git a/src/components/Auth/SignupPage/hooks/useDeptList.ts b/src/components/Auth/SignupPage/hooks/useDeptList.ts deleted file mode 100644 index f9d3a76eb..000000000 --- a/src/components/Auth/SignupPage/hooks/useDeptList.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { dept } from 'api'; - -const useDeptList = () => { - const { data } = useSuspenseQuery({ - queryKey: ['dept'], - queryFn: dept.getDeptList, - }); - - return { data }; -}; - -export default useDeptList; diff --git a/src/components/Auth/SignupPage/hooks/useStep.tsx b/src/components/Auth/SignupPage/hooks/useStep.tsx index 298cbbc24..5b1cf49e7 100644 --- a/src/components/Auth/SignupPage/hooks/useStep.tsx +++ b/src/components/Auth/SignupPage/hooks/useStep.tsx @@ -9,9 +9,9 @@ function useStep(steps: T[]) { const nextStep = useCallback( (next: T, options?: { replace: boolean }) => { if (options?.replace) { - router.replace(ROUTES.AuthSignup({ currentStep: next, isLink: true })); + router.replace(ROUTES.AuthSignup({ currentStep: next })); } else { - router.push(ROUTES.AuthSignup({ currentStep: next, isLink: true })); + router.push(ROUTES.AuthSignup({ currentStep: next })); } }, [router], diff --git a/src/components/Bus/BusCoursePage/hooks/useBusPrefetch.ts b/src/components/Bus/BusCoursePage/hooks/useBusPrefetch.ts index a6498a787..16bfbcb6a 100644 --- a/src/components/Bus/BusCoursePage/hooks/useBusPrefetch.ts +++ b/src/components/Bus/BusCoursePage/hooks/useBusPrefetch.ts @@ -1,10 +1,6 @@ import { useQueryClient } from '@tanstack/react-query'; -import { - getBusTimetableInfo, - getCityBusTimetableInfo, - getShuttleTimetableDetailInfo, -} from 'api/bus'; -import { CourseBusType, DirectionType } from 'api/bus/entity'; +import { DirectionType, ExpressCourse, ShuttleCourse } from 'api/bus/entity'; +import { busQueries } from 'api/bus/queries'; export type PrefetchParams = | { @@ -13,13 +9,13 @@ export type PrefetchParams = } | { type: 'express'; - bus_type: CourseBusType; + bus_type: ExpressCourse['bus_type']; direction: DirectionType; region: string; } | { type: 'shuttle'; - bus_type: CourseBusType; + bus_type: ShuttleCourse['bus_type']; direction: DirectionType; region: string; } @@ -35,45 +31,36 @@ export default function useBusPrefetch() { const prefetchBusTimetable = async (params: PrefetchParams) => { switch (params.type) { case 'shuttle': { - return queryClient.prefetchQuery({ - queryKey: ['timetable', params.type, params.direction, params.region], - queryFn: () => - getBusTimetableInfo({ - bus_type: params.bus_type, - direction: params.direction, - region: params.region, - }), - }); + return queryClient.prefetchQuery( + busQueries.shuttleTimetable({ + bus_type: params.bus_type, + direction: params.direction, + region: params.region, + }), + ); } case 'shuttle_detail': { - return queryClient.prefetchQuery({ - queryKey: ['bus', 'shuttle', 'timetable', params.id], - queryFn: () => getShuttleTimetableDetailInfo({ id: params.id }), - }); + return queryClient.prefetchQuery(busQueries.shuttleTimetableDetail(params.id)); } case 'express': { - return queryClient.prefetchQuery({ - queryKey: ['timetable', params.type, params.direction, params.region], - queryFn: () => - getBusTimetableInfo({ - bus_type: params.bus_type, - direction: params.direction, - region: params.region, - }), - }); + return queryClient.prefetchQuery( + busQueries.expressTimetable({ + bus_type: params.bus_type, + direction: params.direction, + region: params.region, + }), + ); } case 'city': { - return queryClient.prefetchQuery({ - queryKey: ['timetable', params.bus_number, params.direction], - queryFn: () => - getCityBusTimetableInfo({ - bus_number: params.bus_number, - direction: params.direction, - }), - }); + return queryClient.prefetchQuery( + busQueries.cityTimetable({ + bus_number: params.bus_number, + direction: params.direction, + }), + ); } } } diff --git a/src/components/Bus/BusCoursePage/hooks/useBusTimetable.ts b/src/components/Bus/BusCoursePage/hooks/useBusTimetable.ts deleted file mode 100644 index 3e2fc3187..000000000 --- a/src/components/Bus/BusCoursePage/hooks/useBusTimetable.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; -import { getBusTimetableInfo, getCityBusTimetableInfo } from 'api/bus'; -import { - CityBusParams, - CityInfo, - ExpressCourse, - ShuttleCourse, -} from 'api/bus/entity'; -import useMount from 'utils/hooks/state/useMount'; - -const TIMETABLE_KEY = 'timetable'; - -interface CityTimetable { - info: CityInfo; - type: 'city'; -} - -export function useClientShuttleTimetable(course: ShuttleCourse) { - const isMount = useMount(); - - return useQuery({ - queryKey: ['timetable', 'shuttle', course.direction, course.region], - queryFn: () => getBusTimetableInfo(course), - enabled: isMount, - }); -} - -export function useExpressTimetable(course: ExpressCourse) { - return useQuery({ - queryKey: ['timetable', 'express', course.direction, course.region], - queryFn: () => - getBusTimetableInfo({ - bus_type: course.bus_type, - direction: course.direction, - region: course.region, - }), - }); -} - -export function useCityBusTimetable(course: CityBusParams): CityTimetable { - const { bus_number: busNumber, direction: busDirection } = course; - - const { data } = useSuspenseQuery({ - queryKey: [TIMETABLE_KEY, busNumber, busDirection] as const, - queryFn: ({ queryKey: [, bus_number, direction] }) => getCityBusTimetableInfo({ bus_number, direction }), - select: (response) => ({ - info: response as CityInfo, - type: 'city' as const, - }), - }); - - return data; -} diff --git a/src/components/Bus/BusCoursePage/hooks/useShuttleCourse.ts b/src/components/Bus/BusCoursePage/hooks/useShuttleCourse.ts deleted file mode 100644 index af5b3abd6..000000000 --- a/src/components/Bus/BusCoursePage/hooks/useShuttleCourse.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getShuttleCourseInfo } from 'api/bus'; - -function useShuttleCourse() { - const { data: shuttleCourse } = useSuspenseQuery({ - queryKey: ['bus', 'courses', 'shuttle'], - queryFn: async () => getShuttleCourseInfo(), - }); - - return { shuttleCourse }; -} - -export default useShuttleCourse; diff --git a/src/components/Bus/BusCoursePage/hooks/useShuttleTimetableDetail.ts b/src/components/Bus/BusCoursePage/hooks/useShuttleTimetableDetail.ts deleted file mode 100644 index 54dc92a6d..000000000 --- a/src/components/Bus/BusCoursePage/hooks/useShuttleTimetableDetail.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { skipToken, useQuery } from '@tanstack/react-query'; -import { getShuttleTimetableDetailInfo } from 'api/bus'; - -export default function useShuttleTimetableDetail(id: string | null) { - const { data: shuttleTimetableDetail } = useQuery({ - queryKey: ['bus', 'shuttle', 'timetable', id], - queryFn: id ? async () => getShuttleTimetableDetailInfo({ id }) : skipToken, - staleTime: 1000 * 60 * 10, - }); - - return { shuttleTimetableDetail }; -} diff --git a/src/components/Bus/BusNotice/index.tsx b/src/components/Bus/BusNotice/index.tsx index 0ee132397..6ab5a3155 100644 --- a/src/components/Bus/BusNotice/index.tsx +++ b/src/components/Bus/BusNotice/index.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { busQueries } from 'api/bus/queries'; import InformationIcon from 'assets/svg/Bus/info.svg'; import CloseIcon from 'assets/svg/common/close/close-icon-32x32.svg'; -import useBusNotice from 'components/Bus/hooks/useBusNotice'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; @@ -36,8 +37,8 @@ export default function BusNotice({ loggingLocation }: BusNoticeProps) { const isMobile = useMediaQuery(); const router = useRouter(); const navigate = (path: string) => router.push(path); - const res = useBusNotice(); - const { id, title } = res.data; + const { data } = useSuspenseQuery(busQueries.notice()); + const { id, title } = data; const [lastBusNotice, setLastBusNotice] = useLocalStorage('lastBusNotice', ''); const [busNoticeDismissed, setBusNoticeDismissed] = useLocalStorage('busNoticeDismissed', 'false'); const isDismissed = busNoticeDismissed === 'true'; @@ -54,7 +55,7 @@ export default function BusNotice({ loggingLocation }: BusNoticeProps) { event_label: 'bus_announcement', value: logValue, }); - navigate(ROUTES.ArticlesDetail({ id: String(id), isLink: true })); + navigate(ROUTES.ArticlesDetail({ id: String(id) })); }; const handleClickDismissNotice = () => { diff --git a/src/components/Bus/BusRoutePage/components/RouteList/index.tsx b/src/components/Bus/BusRoutePage/components/RouteList/index.tsx index 5c14d56de..46a3c9e2d 100644 --- a/src/components/Bus/BusRoutePage/components/RouteList/index.tsx +++ b/src/components/Bus/BusRoutePage/components/RouteList/index.tsx @@ -1,7 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; import { Arrival, BusTypeRequest, Depart } from 'api/bus/entity'; +import { busQueries } from 'api/bus/queries'; import BusRoute from 'components/Bus/BusRoutePage/components/BusRoute'; -import useBusRoute from 'components/Bus/BusRoutePage/hooks/useBusRoute'; import { UseTimeSelectReturn } from 'components/Bus/BusRoutePage/hooks/useTimeSelect'; +import { transformBusRoute } from 'components/Bus/BusRoutePage/utils/transform'; import styles from './RouteList.module.scss'; interface RouteListProps { @@ -13,12 +15,16 @@ interface RouteListProps { export default function RouteList({ timeSelect, busType, depart, arrival }: RouteListProps) { const { formattedValues } = timeSelect; - const { data } = useBusRoute({ - dayOfMonth: formattedValues.date, - time: formattedValues.time, - busType, - depart, - arrival, + const { data } = useQuery({ + ...busQueries.route({ + dayOfMonth: formattedValues.date, + time: formattedValues.time, + busType, + depart, + arrival, + }), + select: transformBusRoute, + enabled: Boolean(depart) && Boolean(arrival), }); const isReady = Boolean(depart) && Boolean(arrival); diff --git a/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailMobile/index.tsx b/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailMobile/index.tsx index 999ff0ba7..963560471 100644 --- a/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailMobile/index.tsx +++ b/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailMobile/index.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { coopshopQueries } from 'api/coopshop/queries'; import PickerColumn from 'components/Bus/BusRoutePage/components/PickerColumn'; -import useCoopSemester from 'components/Bus/BusRoutePage/hooks/useCoopSemester'; import { useTimeSelect } from 'components/Bus/BusRoutePage/hooks/useTimeSelect'; import { useBodyScrollLock } from 'utils/hooks/ui/useBodyScrollLock'; import { useEscapeKeyDown } from 'utils/hooks/ui/useEscapeKeyDown'; @@ -27,7 +28,7 @@ export default function TimeDetailMobile({ timeSelect, close }: TimeDetailMobile const [selectedHour, setSelectedHour] = useState(hour % 12); const [selectedMinute, setSelectedMinute] = useState(minute); - const { data: semesterData } = useCoopSemester(); + const { data: semesterData } = useSuspenseQuery(coopshopQueries.allShopInfo()); const { backgroundRef } = useOutsideClick({ onOutsideClick: close }); useBodyScrollLock(); diff --git a/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailPC/index.tsx b/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailPC/index.tsx index 5fb3ff5c2..7a893dea3 100644 --- a/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailPC/index.tsx +++ b/src/components/Bus/BusRoutePage/components/TimeDetail/TimeDetailPC/index.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { coopshopQueries } from 'api/coopshop/queries'; import SelectDropdown from 'components/Bus/BusRoutePage/components/SelectDropdown'; -import useCoopSemester from 'components/Bus/BusRoutePage/hooks/useCoopSemester'; import { useTimeSelect } from 'components/Bus/BusRoutePage/hooks/useTimeSelect'; import { useBusLogger } from 'components/Bus/hooks/useBusLogger'; import styles from './TimeDetailPC.module.scss'; @@ -19,7 +20,7 @@ export default function TimeDetailPC({ timeSelect }: TimeDetailPCProps) { const { hour, minute } = timeSelect.timeState; const { setNow, setDayOfMonth, setHour, setMinute } = timeSelect.timeHandler; const { logDepartureNowClick } = useBusLogger(); - const { data: semesterData } = useCoopSemester(); + const { data: semesterData } = useSuspenseQuery(coopshopQueries.allShopInfo()); const displaySemester = formatSemesterLabel(semesterData.semester); diff --git a/src/components/Bus/BusRoutePage/hooks/useBusRoute.ts b/src/components/Bus/BusRoutePage/hooks/useBusRoute.ts deleted file mode 100644 index 44296d42b..000000000 --- a/src/components/Bus/BusRoutePage/hooks/useBusRoute.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getBusRouteInfo } from 'api/bus'; -import { Arrival, BusRouteParams, Depart } from 'api/bus/entity'; -import { transformBusRoute } from 'components/Bus/BusRoutePage/utils/transform'; - -const BUS_ROUTE_KEY = 'bus-route'; - -interface BusRouteQueryParams extends Omit { - depart: Depart | ''; - arrival: Arrival | ''; -} - -const useBusRoute = (params: BusRouteQueryParams) => { - const { depart, arrival, ...rest } = params; - const isReady = Boolean(depart) && Boolean(arrival); - - return useQuery({ - queryKey: [BUS_ROUTE_KEY, JSON.stringify(params)], - queryFn: async () => { - const response = await getBusRouteInfo({ - ...rest, - depart: depart as Depart, - arrival: arrival as Arrival, - }); - return transformBusRoute(response); - }, - enabled: isReady, - }); -}; - -export default useBusRoute; diff --git a/src/components/Bus/BusRoutePage/hooks/useCoopSemester.ts b/src/components/Bus/BusRoutePage/hooks/useCoopSemester.ts deleted file mode 100644 index 08a16994f..000000000 --- a/src/components/Bus/BusRoutePage/hooks/useCoopSemester.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { coopshop } from 'api'; - -const useCoopSemester = () => - useSuspenseQuery({ - queryKey: ['coopSemester'], - queryFn: async () => coopshop.getAllShopInfo(), - }); - -export default useCoopSemester; diff --git a/src/components/Bus/hooks/useBusNotice.ts b/src/components/Bus/hooks/useBusNotice.ts deleted file mode 100644 index 884883d98..000000000 --- a/src/components/Bus/hooks/useBusNotice.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getBusNoticeInfo } from 'api/bus'; - -const BUS_NOTICE_KEY = 'bus-notice'; - -const useBusNotice = () => - useSuspenseQuery({ - queryKey: [BUS_NOTICE_KEY], - queryFn: getBusNoticeInfo, - }); - -export default useBusNotice; diff --git a/src/components/Callvan/components/AddPostForm/AddPostForm.module.scss b/src/components/Callvan/components/AddPostForm/AddPostForm.module.scss new file mode 100644 index 000000000..05bb0bd80 --- /dev/null +++ b/src/components/Callvan/components/AddPostForm/AddPostForm.module.scss @@ -0,0 +1,268 @@ +.page { + display: flex; + flex-direction: column; + width: 100%; + height: 100dvh; + background: #fff; + overflow: hidden; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + flex-shrink: 0; + position: relative; + } + + &__back-button { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 0; + z-index: 1; + width: 24px; + height: 24px; + + svg { + width: 24px; + height: 24px; + } + } + + &__title { + position: absolute; + left: 50%; + transform: translateX(-50%); + font-family: Pretendard, sans-serif; + font-size: 18px; + font-weight: 500; + line-height: 1.6; + color: #000; + margin: 0; + } + + &__header-spacer { + width: 24px; + height: 24px; + } + + &__body { + flex: 1; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + &__footer { + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 16px 24px; + border-top: 1px solid #f5f5f5; + background: #fff; + } + + &__footer-hint { + font-family: Pretendard, sans-serif; + font-size: 12px; + font-weight: 400; + line-height: 1.6; + color: #727272; + margin: 0; + } + + &__submit-button { + width: 100%; + height: 48px; + border-radius: 8px; + border: none; + background: #cacaca; + cursor: not-allowed; + font-family: Pretendard, sans-serif; + font-size: 16px; + font-weight: 600; + line-height: 1.6; + color: #fff; + box-shadow: + 0 1px 1px rgb(0 0 0 / 2%), + 0 2px 4px rgb(0 0 0 / 4%); + transition: background 0.15s; + + &--active { + background: #b611f5; + cursor: pointer; + + &:active { + background: #9b0fcf; + } + } + } +} + +.location-row { + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 12px 24px; + + &__swap { + display: flex; + align-items: center; + justify-content: center; + width: 31px; + height: 31px; + border-radius: 50%; + flex-shrink: 0; + margin-bottom: 2px; + padding: 8px 0; + } +} + +.location-field { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + width: 124px; + + &__label { + font-family: Pretendard, sans-serif; + font-size: 16px; + font-weight: 500; + line-height: 1.6; + color: #b611f5; + } + + &__button { + width: 100%; + padding: 8px 16px; + border-radius: 8px; + border: none; + background: #f5f5f5; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-size: 12px; + font-weight: 400; + line-height: 1.6; + color: #727272; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: background 0.15s; + + &--selected { + color: #000; + background-color: transparent; + font-size: 18px; + font-weight: 500; + line-height: 1.6; + } + } +} + +.form-section { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 24px; + + &__header { + display: flex; + align-items: center; + gap: 8px; + } + + &__title { + font-family: Pretendard, sans-serif; + font-size: 16px; + font-weight: 500; + line-height: 1.6; + color: #b611f5; + white-space: nowrap; + } + + &__hint { + font-family: Pretendard, sans-serif; + font-size: 12px; + font-weight: 400; + line-height: 1.6; + color: #727272; + + strong { + font-weight: 600; + } + } +} + +.participants-picker { + display: flex; + align-items: stretch; + border: 1px solid #cacaca; + border-radius: 4px; + overflow: hidden; + + &__display { + flex: 1; + display: flex; + align-items: center; + padding: 8px 16px; + font-family: Pretendard, sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 1.6; + color: #000; + } + + &__divider { + width: 1px; + background: #cacaca; + flex-shrink: 0; + } + + &__controls { + display: flex; + align-items: stretch; + } + + &__button { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + border: none; + background: none; + cursor: pointer; + color: #b611f5; + + &:disabled { + color: #cacaca; + cursor: not-allowed; + } + } + + &__icon { + font-size: 18px; + line-height: 1; + font-weight: 400; + } + + &__count { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + border-left: 1px solid #cacaca; + border-right: 1px solid #cacaca; + font-family: Pretendard, sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 1.6; + color: #000; + } +} diff --git a/src/components/Callvan/components/AddPostForm/index.tsx b/src/components/Callvan/components/AddPostForm/index.tsx new file mode 100644 index 000000000..c2ac7980d --- /dev/null +++ b/src/components/Callvan/components/AddPostForm/index.tsx @@ -0,0 +1,289 @@ +import { useCallback, useState } from 'react'; +import { useRouter } from 'next/router'; +import { cn } from '@bcsdlab/utils'; +import { CALLVAN_POST_LOCATION_LABEL, CallvanPostLocationType } from 'api/callvan/entity'; +import ArrowBackIcon from 'assets/svg/Callvan/arrow-back.svg'; +import SwapIcon from 'assets/svg/Callvan/swap.svg'; +import DateDropdown from 'components/Callvan/components/DateDropdown'; +import LocationBottomSheet from 'components/Callvan/components/LocationBottomSheet'; +import TimeDropdown from 'components/Callvan/components/TimeDropdown'; +import useCallvanToast from 'components/Callvan/hooks/useCallvanToast'; +import useCreateCallvan from 'components/Callvan/hooks/useCreateCallvan'; +import ROUTES from 'static/routes'; +import useLogger from 'utils/hooks/analytics/useLogger'; +import styles from './AddPostForm.module.scss'; + +interface FormState { + departureType: CallvanPostLocationType | null; + departureCustomName: string; + arrivalType: CallvanPostLocationType | null; + arrivalCustomName: string; + departureDate: Date; + departureHour: number; + departureMinute: number; + isPM: boolean; + maxParticipants: number; +} + +function formatTime(hour: number, minute: number, isPM: boolean): string { + const hour24 = isPM ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour; + return `${String(hour24).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; +} + +function formatDateParam(date: Date): string { + const y = date.getFullYear(); + const mo = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${mo}-${d}`; +} + +function getLocationLabel(type: CallvanPostLocationType | null, customName: string): string { + if (!type) return ''; + if (type === 'CUSTOM') return customName || '직접입력'; + return CALLVAN_POST_LOCATION_LABEL[type]; +} + +export default function AddPostForm() { + const router = useRouter(); + const logger = useLogger(); + const { open: openToast } = useCallvanToast(); + const { mutate, isPending } = useCreateCallvan(); + + const [form, setForm] = useState({ + departureType: null, + departureCustomName: '', + arrivalType: null, + arrivalCustomName: '', + departureDate: new Date(), + departureHour: 12, + departureMinute: 0, + isPM: false, + maxParticipants: 2, + }); + + const [departureSheetOpen, setDepartureSheetOpen] = useState(false); + const [arrivalSheetOpen, setArrivalSheetOpen] = useState(false); + + const isFormValid = + form.departureType !== null && + form.arrivalType !== null && + (form.departureType !== 'CUSTOM' || form.departureCustomName.trim() !== '') && + (form.arrivalType !== 'CUSTOM' || form.arrivalCustomName.trim() !== ''); + + const handleSwapLocation = useCallback(() => { + setForm((prev) => ({ + ...prev, + departureType: prev.arrivalType, + departureCustomName: prev.arrivalCustomName, + arrivalType: prev.departureType, + arrivalCustomName: prev.departureCustomName, + })); + }, []); + + const handleParticipantsChange = useCallback((delta: number) => { + setForm((prev) => ({ + ...prev, + maxParticipants: Math.min(11, Math.max(2, prev.maxParticipants + delta)), + })); + }, []); + + const handleSubmit = () => { + if (!form.departureType || !form.arrivalType || isPending) return; + + const selectedDateTime = new Date(form.departureDate); + const hour24 = form.isPM ? (form.departureHour === 12 ? 12 : form.departureHour + 12) : form.departureHour === 12 ? 0 : form.departureHour; + selectedDateTime.setHours(hour24, form.departureMinute, 0, 0); + + if (selectedDateTime < new Date()) { + openToast('현재 시각보다 이전 시각으로 모집글을 생성할 수 없습니다.'); + return; + } + + const departureTime = formatTime(form.departureHour, form.departureMinute, form.isPM); + + mutate( + { + departure_type: form.departureType, + departure_custom_name: form.departureType === 'CUSTOM' ? form.departureCustomName : null, + arrival_type: form.arrivalType, + arrival_custom_name: form.arrivalType === 'CUSTOM' ? form.arrivalCustomName : null, + departure_date: formatDateParam(form.departureDate), + departure_time: departureTime, + max_participants: form.maxParticipants, + }, + { + onSuccess: () => { + openToast('작성되었습니다.'); + router.replace({ pathname: ROUTES.Callvan(), query: { created: '1' } }); + }, + }, + ); + logger.actionEventClick({ event_label: 'callvan_write_done', team: 'CAMPUS', value: '' }); + }; + + const handleBack = () => { + logger.actionEventClick({ event_label: 'callvan_write_back', team: 'CAMPUS', value: '' }); + router.back(); + }; + + return ( +
+
+ +

콜밴팟

+
+
+ +
+ {/* 출발 / 도착 */} +
+
+ 출발 + +
+ + + +
+ 도착 + +
+
+ + {/* 출발일 */} +
+
+ 출발일 + 출발 날짜를 선택해주세요. +
+ setForm((prev) => ({ ...prev, departureDate: date }))} + /> +
+ + {/* 출발 시각 */} +
+
+ 출발 시각 + 출발 시각을 선택해주세요. +
+ + setForm((prev) => ({ ...prev, departureHour: hour, departureMinute: minute, isPM })) + } + /> +
+ + {/* 참여 인원 */} +
+
+ 참여 인원 + + 본인을 포함한 참여 인원 수를 선택해주세요. + +
+
+
+ {form.maxParticipants} 명 +
+
+
+ +
{form.maxParticipants}
+ +
+
+
+
+ + setDepartureSheetOpen(false)} + onConfirm={(type, customName) => { + setForm((prev) => ({ ...prev, departureType: type, departureCustomName: customName })); + setDepartureSheetOpen(false); + }} + /> + + setArrivalSheetOpen(false)} + onConfirm={(type, customName) => { + setForm((prev) => ({ ...prev, arrivalType: type, arrivalCustomName: customName })); + setArrivalSheetOpen(false); + }} + /> + +
+

※ 모든 항목을 다 작성해주세요.

+ +
+
+ ); +} diff --git a/src/components/Callvan/components/CallvanActionModal/CallvanActionModal.module.scss b/src/components/Callvan/components/CallvanActionModal/CallvanActionModal.module.scss new file mode 100644 index 000000000..5e08fe128 --- /dev/null +++ b/src/components/Callvan/components/CallvanActionModal/CallvanActionModal.module.scss @@ -0,0 +1,107 @@ +.modal { + &__overlay { + position: fixed; + inset: 0; + z-index: 200; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + } + + &__dim { + position: absolute; + inset: 0; + background: rgb(0 0 0 / 70%); + border: none; + padding: 0; + cursor: pointer; + } + + &__sheet { + position: relative; + z-index: 201; + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + max-width: 390px; + border-radius: 32px 32px 0 0; + background: #fff; + box-shadow: + 0 8px 20px rgb(0 0 0 / 6%), + 0 24px 60px rgb(0 0 0 / 12%); + padding-bottom: env(safe-area-inset-bottom, 0); + } + + &__title-area { + display: flex; + align-items: center; + justify-content: center; + padding: 12px 24px 12px 32px; + width: 100%; + box-sizing: border-box; + } + + &__title { + flex: 1; + font-family: Pretendard, sans-serif; + font-size: 18px; + font-weight: 600; + line-height: 1.6; + color: #b611f5; + margin: 0; + } + + &__body { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 16px 32px 12px; + width: 100%; + box-sizing: border-box; + border-top: 0.5px solid #e1e1e1; + } + + &__content { + display: flex; + flex-direction: column; + gap: 16px; + align-items: flex-start; + width: 100%; + } + + &__btn-confirm { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 10px 16px; + border-radius: 12px; + background: #b611f5; + border: none; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-size: 18px; + font-weight: 600; + line-height: 1.6; + color: #fff; + } + + &__btn-cancel { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 10px 16px; + border-radius: 12px; + background: #fff; + border: 1px solid #e1e1e1; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-size: 18px; + font-weight: 600; + line-height: 1.6; + color: #4b4b4b; + } +} diff --git a/src/components/Callvan/components/CallvanActionModal/index.tsx b/src/components/Callvan/components/CallvanActionModal/index.tsx new file mode 100644 index 000000000..a82341325 --- /dev/null +++ b/src/components/Callvan/components/CallvanActionModal/index.tsx @@ -0,0 +1,38 @@ +import styles from './CallvanActionModal.module.scss'; + +interface CallvanActionModalProps { + title: string; + confirmLabel: string; + cancelLabel: string; + onConfirm: () => void; + onCancel: () => void; +} + +export default function CallvanActionModal({ + title, + confirmLabel, + cancelLabel, + onConfirm, + onCancel, +}: CallvanActionModalProps) { + return ( +
+ + +
+
+
+
+ ); +} diff --git a/src/components/Callvan/components/CallvanCard/CallvanCard.module.scss b/src/components/Callvan/components/CallvanCard/CallvanCard.module.scss new file mode 100644 index 000000000..27b6c4659 --- /dev/null +++ b/src/components/Callvan/components/CallvanCard/CallvanCard.module.scss @@ -0,0 +1,315 @@ +@use "src/utils/scss/media" as media; + +.card { + padding: 8px 24px; + width: 100%; + box-sizing: border-box; + + &__inner { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + border: 0.5px solid #cacaca; + border-radius: 8px; + min-height: 1px; + min-width: 1px; + } + + &__content { + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-start; + justify-content: center; + + &--clickable { + cursor: pointer; + } + } + + &__text-card { + display: flex; + align-items: center; + gap: 8px; + } + + &__indicator { + display: flex; + align-items: center; + justify-content: center; + width: 8px; + flex-shrink: 0; + + svg { + width: 8px; + height: 35px; + } + } + + &__main-text { + display: flex; + flex-direction: column; + gap: 4px; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 1.6; + color: #000; + width: 129px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__location { + display: flex; + align-items: center; + gap: 4px; + } + + &__location-label { + flex-shrink: 0; + overflow: hidden; + } + + &__location-value { + max-width: 90px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 0; + } + + &__sub-text { + display: flex; + align-items: center; + gap: 8px; + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 1.6; + color: #4b4b4b; + white-space: nowrap; + } + + &__date-time { + display: flex; + align-items: center; + gap: 4px; + } + + &__date { + display: flex; + align-items: center; + gap: 2px; + } + + &__divider { + color: #4b4b4b; + } + + &__phone-placeholder { + width: 24px; + height: 24px; + flex-shrink: 0; + } + + &__count { + display: flex; + align-items: center; + gap: 4px; + + svg { + width: 16px; + height: 16px; + flex-shrink: 0; + } + } + + &__count--author { + display: flex; + align-items: center; + gap: 4px; + background: none; + border: none; + padding: 0; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 1.6; + color: #4b4b4b; + white-space: nowrap; + + & > svg:first-child { + width: 16px; + height: 16px; + flex-shrink: 0; + } + + & > svg:last-child { + width: 12px; + height: 12px; + flex-shrink: 0; + color: #4b4b4b; + } + } + + &__actions { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; + flex-shrink: 0; + } + + &__badge-group { + display: flex; + gap: 4px; + align-items: center; + } + + &__badge--recruiting { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 12px; + background-color: #b611f5; + border-radius: 4px; + border: none; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 1.6; + color: #fff; + white-space: nowrap; + + &:hover { + background-color: #980ac9; + } + + &:focus-visible { + outline: 2px solid #b611f5; + outline-offset: 2px; + } + } + + &__badge--reopen { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 8px; + background: transparent; + border: 0.5px solid #b611f5; + border-radius: 4px; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 1.6; + color: #b611f5; + white-space: nowrap; + + &:hover { + background-color: rgb(182 17 245 / 8%); + } + + &:focus-visible { + outline: 2px solid #b611f5; + outline-offset: 2px; + } + } + + &__badge--complete { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 8px; + background-color: #980ac9; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 1.6; + color: #fff; + white-space: nowrap; + + &:hover { + background-color: #7d08a4; + } + + &:focus-visible { + outline: 2px solid #980ac9; + outline-offset: 2px; + } + } + + &__badge--closeable { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 12px; + background: transparent; + border: 0.5px solid #b611f5; + border-radius: 4px; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 1.6; + color: #b611f5; + white-space: nowrap; + + &:hover { + background-color: rgb(182 17 245 / 8%); + } + + &:focus-visible { + outline: 2px solid #b611f5; + outline-offset: 2px; + } + } + + &__badge--closed { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 12px; + background: transparent; + border: 0.5px solid #727272; + border-radius: 4px; + cursor: default; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 1.6; + color: #727272; + white-space: nowrap; + } + + &__badge--joined { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 12px; + background-color: #f5ebff; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 1.6; + color: #7d08a4; + white-space: nowrap; + } + + &__badge--pending { + opacity: 0.6; + cursor: not-allowed; + } +} diff --git a/src/components/Callvan/components/CallvanCard/index.tsx b/src/components/Callvan/components/CallvanCard/index.tsx new file mode 100644 index 000000000..8cd4b4555 --- /dev/null +++ b/src/components/Callvan/components/CallvanCard/index.tsx @@ -0,0 +1,348 @@ +import { useRouter } from 'next/router'; +import { cn } from '@bcsdlab/utils'; +import { CallvanPost } from 'api/callvan/entity'; +import ChatIcon from 'assets/svg/Callvan/chat.svg'; +import ChevronRightIcon from 'assets/svg/Callvan/chevron-right.svg'; +import PeopleIcon from 'assets/svg/Callvan/people.svg'; +import PhoneCallingIcon from 'assets/svg/Callvan/phone-calling.svg'; +import RouteIndicatorIcon from 'assets/svg/Callvan/route-indicator.svg'; +import CallvanActionModal from 'components/Callvan/components/CallvanActionModal'; +import CloseConfirmModal from 'components/Callvan/components/CloseConfirmModal'; +import CompleteConfirmModal from 'components/Callvan/components/CompleteConfirmModal'; +import ReopenConfirmModal from 'components/Callvan/components/ReopenConfirmModal'; +import useCancelCallvan from 'components/Callvan/hooks/useCancelCallvan'; +import useCloseCallvan from 'components/Callvan/hooks/useCloseCallvan'; +import useCompleteCallvan from 'components/Callvan/hooks/useCompleteCallvan'; +import useJoinCallvan from 'components/Callvan/hooks/useJoinCallvan'; +import useReopenCallvan from 'components/Callvan/hooks/useReopenCallvan'; +import { DAYS } from 'static/day'; +import ROUTES from 'static/routes'; +import { ORDER_BASE_URL } from 'static/url'; +import useLogger from 'utils/hooks/analytics/useLogger'; +import useBooleanState from 'utils/hooks/state/useBooleanState'; +import useTokenState from 'utils/hooks/state/useTokenState'; +import { redirectToLogin } from 'utils/ts/auth'; +import styles from './CallvanCard.module.scss'; + +const CALLVAN_CATEGORY = '11'; + +interface CallvanCardProps { + post: CallvanPost; +} + +function getDayOfWeek(dateStr: string): string { + const date = new Date(dateStr); + return DAYS[date.getDay()]; +} + +function formatDate(dateStr: string): string { + const parts = dateStr.split('-'); + const month = parts[1]; + const day = parts[2]; + const dayOfWeek = getDayOfWeek(dateStr); + return `${month}.${day} (${dayOfWeek})`; +} + +function formatTime(timeStr: string): string { + return timeStr.slice(0, 5); +} + +export default function CallvanCard({ post }: CallvanCardProps) { + const router = useRouter(); + const token = useTokenState(); + const logger = useLogger(); + const [isCloseModalOpen, openCloseModal, closeCloseModal] = useBooleanState(false); + const [isReopenModalOpen, openReopenModal, closeReopenModal] = useBooleanState(false); + const [isCompleteModalOpen, openCompleteModal, closeCompleteModal] = useBooleanState(false); + const [isLoginModalOpen, openLoginModal, closeLoginModal] = useBooleanState(false); + const [isJoinModalOpen, openJoinModal, closeJoinModal] = useBooleanState(false); + const [isCancelModalOpen, openCancelModal, closeCancelModal] = useBooleanState(false); + + const { mutate: closePost } = useCloseCallvan(); + const { mutate: reopenPost } = useReopenCallvan(); + const { mutate: completePost } = useCompleteCallvan(); + const { mutate: joinPost, isPending: isJoinPending } = useJoinCallvan(); + const { mutate: cancelPost, isPending: isCancelPending } = useCancelCallvan(); + + const handleCloseConfirm = () => { + closePost(post.id); + closeCloseModal(); + }; + + const handleReopenConfirm = () => { + reopenPost(post.id); + closeReopenModal(); + }; + + const handleCompleteConfirm = () => { + completePost(post.id); + closeCompleteModal(); + }; + + const handleLoginConfirm = () => { + redirectToLogin(router.asPath); + }; + + const handleJoinConfirm = () => { + joinPost(post.id); + closeJoinModal(); + logger.actionEventClick({ event_label: 'callvan_join', team: 'CAMPUS', value: '예, 아니요' }); + }; + + const handleCancelConfirm = () => { + cancelPost(post.id); + closeCancelModal(); + logger.actionEventClick({ event_label: 'callvan_join_cancel', team: 'CAMPUS', value: '예, 아니요' }); + }; + + const handleChatClick = (e: React.MouseEvent) => { + e.stopPropagation(); + logger.actionEventClick({ event_label: 'callvan_chat', team: 'CAMPUS', value: '' }); + router.push(ROUTES.CallvanChat({ id: String(post.id) })); + }; + + const handleCallClick = (e: React.MouseEvent) => { + e.stopPropagation(); + logger.actionEventClick({ event_label: 'callvan_call', team: 'CAMPUS', value: '' }); + router.push(`${ORDER_BASE_URL}/shops/?category=${CALLVAN_CATEGORY}`); + }; + + const renderTopAction = () => { + if (post.is_author && post.status !== 'COMPLETED') { + return ( + + ); + } + if (post.is_joined && !post.is_author) { + return ( + + ); + } + return ; + }; + + const renderCount = () => { + if (post.is_author) { + return ( + + ); + } + return ( +
+ + + {post.current_participants}/{post.max_participants} + +
+ ); + }; + + const renderActionButton = () => { + if (post.status === 'COMPLETED') { + return ( + + ); + } + + if (post.is_author) { + if (post.status === 'RECRUITING') { + return ( + + ); + } + if (post.status === 'CLOSED') { + return ( +
+ + +
+ ); + } + return null; + } + + if (post.is_joined) { + return ( + + ); + } + + if (post.status === 'CLOSED') { + return ( + + ); + } + + return ( + + ); + }; + + return ( + <> +
router.push(ROUTES.CallvanParticipants({ postId: String(post.id) })) : undefined + } + onKeyDown={ + post.is_joined + ? (e) => { + if ((e.target as HTMLElement).closest('button')) { + return; + } + if (e.key === 'Enter' || e.key === ' ') { + router.push(ROUTES.CallvanParticipants({ postId: String(post.id) })); + } + } + : undefined + } + > +
+
+
+
+ +
+
+
+ 출발: + {post.departure} +
+
+ 도착: + {post.arrival} +
+
+
+
+
+ {formatDate(post.departure_date)} + {formatTime(post.departure_time)} +
+ | + {renderCount()} +
+
+
+ {renderTopAction()} + {renderActionButton()} +
+
+
+ {isCloseModalOpen && } + {isReopenModalOpen && } + {isCompleteModalOpen && } + {isLoginModalOpen && ( + + )} + {isJoinModalOpen && ( + + )} + {isCancelModalOpen && ( + + )} + + ); +} diff --git a/src/components/Callvan/components/CallvanChatRoom/CallvanChatRoom.module.scss b/src/components/Callvan/components/CallvanChatRoom/CallvanChatRoom.module.scss new file mode 100644 index 000000000..ac12eaa7a --- /dev/null +++ b/src/components/Callvan/components/CallvanChatRoom/CallvanChatRoom.module.scss @@ -0,0 +1,358 @@ +@use "src/utils/scss/media" as media; + +.chat-room { + display: flex; + flex-direction: column; + height: 100dvh; + background-color: #fff; + overflow: hidden; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px 12px; + background-color: #fff; + flex-shrink: 0; + } + + &__back-button { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 0; + flex-shrink: 0; + color: #000; + + svg { + width: 24px; + height: 24px; + } + } + + &__header-center { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + &__title-row { + display: flex; + align-items: center; + gap: 8px; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 15px; + line-height: 1.6; + color: #000; + white-space: nowrap; + } + + &__route { + display: flex; + align-items: center; + gap: 4px; + } + + &__route-text { + max-width: 75px; + overflow: hidden; + text-overflow: ellipsis; + } + + &__header-count { + display: flex; + align-items: center; + gap: 4px; + + svg { + width: 16px; + height: 16px; + flex-shrink: 0; + } + + span { + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + color: #4b4b4b; + white-space: nowrap; + } + } + + &__header-menu { + width: 24px; + height: 24px; + flex-shrink: 0; + } + + &__messages { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + } + + &__loading, + &__empty { + text-align: center; + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + color: #727272; + padding: 24px 0; + } + + &__date-badge { + display: flex; + justify-content: center; + padding: 12px 24px; + + span { + display: inline-flex; + align-items: center; + padding: 4px 12px; + background-color: #f5f5f5; + border-radius: 16px; + font-family: Pretendard, sans-serif; + font-weight: 500; + font-size: 12px; + color: #980ac9; + } + } + + &__message-row--mine { + display: flex; + align-items: flex-end; + justify-content: flex-end; + gap: 8px; + padding: 8px 24px; + } + + &__message-group--others { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 8px 24px; + } + + &__message-group--others-consecutive { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0 24px 8px; + } + + &__sender-row { + display: flex; + align-items: center; + gap: 8px; + } + + &__sender-info { + display: flex; + align-items: center; + gap: 4px; + } + + &__avatar { + width: 32px; + height: 32px; + border-radius: 8px; + border: 1px solid #ddb1fe; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 2px; + box-sizing: border-box; + color: #980ac9; + + svg { + width: 24px; + height: 24px; + } + } + + &__sender-name { + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + color: #4b4b4b; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__left-badge { + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 10px; + color: #b611f5; + flex-shrink: 0; + } + + &__message-row--others { + display: flex; + align-items: flex-end; + gap: 8px; + } + + &__bubble--mine { + max-width: 220px; + padding: 8px 12px; + background-color: rgba(#eee, 0.8); + border-radius: 8px; + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 1.6; + color: #000; + word-break: break-word; + } + + &__bubble--others { + max-width: 220px; + padding: 8px 12px; + background-color: #f5f5f5; + border-radius: 8px; + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 1.6; + color: #000; + word-break: break-word; + } + + &__message-text { + white-space: pre-wrap; + } + + &__bubble-image { + max-width: 220px; + border-radius: 8px; + display: flex; + overflow: hidden; + + img { + max-width: 100%; + height: auto; + object-fit: contain; + } + } + + &__textheader--mine { + display: flex; + flex-direction: column; + align-items: flex-end; + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 1.6; + white-space: nowrap; + flex-shrink: 0; + } + + &__textheader--others { + display: flex; + flex-direction: column; + align-items: flex-start; + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 1.6; + white-space: nowrap; + flex-shrink: 0; + } + + &__unread-count { + color: #1f1f1f; + overflow: hidden; + text-overflow: ellipsis; + } + + &__timestamp { + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + color: #727272; + white-space: nowrap; + flex-shrink: 0; + } + + &__send-bar { + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: 8px 24px; + background-color: #f5f5f5; + flex-shrink: 0; + } + + &__image-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: none; + border: none; + border-radius: 12px; + cursor: pointer; + padding: 0; + background-color: #fff; + flex-shrink: 0; + } + + &__input { + flex: 1; + min-height: 32px; + max-height: 100px; + padding: 8px 16px; + margin: 0 10px; + background-color: #fff; + border: none; + border-radius: 12px; + font-family: Pretendard, sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 1.5; + color: #000; + outline: none; + box-sizing: border-box; + resize: none; + overflow-y: auto; + + &::placeholder { + color: #727272; + } + } + + &__send-button { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 0; + color: #980ac9; + flex-shrink: 0; + + &:disabled { + color: #cacaca; + cursor: default; + } + + svg { + width: 32px; + height: 32px; + } + } +} diff --git a/src/components/Callvan/components/CallvanChatRoom/index.tsx b/src/components/Callvan/components/CallvanChatRoom/index.tsx new file mode 100644 index 000000000..59c35a077 --- /dev/null +++ b/src/components/Callvan/components/CallvanChatRoom/index.tsx @@ -0,0 +1,274 @@ +import { useRef, useState, useEffect, useMemo } from 'react'; +import { useRouter } from 'next/router'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { CallvanChatMessage } from 'api/callvan/entity'; +import { callvanQueries } from 'api/callvan/queries'; +import ArrowBackIcon from 'assets/svg/Callvan/arrow-back.svg'; +import ImageUploadIcon from 'assets/svg/Callvan/image-upload.svg'; +import PeopleIcon from 'assets/svg/Callvan/people.svg'; +import SendIcon from 'assets/svg/Callvan/send.svg'; +import { ParticipantAvatarIcon } from 'components/Callvan/components/ParticipantsList/ParticipantAvatarIcon'; +import useSendCallvanChat from 'components/Callvan/hooks/useSendCallvanChat'; +import { getParticipantColor } from 'components/Callvan/utils/participantColor'; +import useLogger from 'utils/hooks/analytics/useLogger'; +import useTokenState from 'utils/hooks/state/useTokenState'; +import useUploadFile from 'utils/hooks/uploadFile/useUploadFile'; +import styles from './CallvanChatRoom.module.scss'; + +interface CallvanChatRoomProps { + postId: number; +} + +function groupMessagesByDate(messages: CallvanChatMessage[]): { date: string; messages: CallvanChatMessage[] }[] { + const groups: { date: string; messages: CallvanChatMessage[] }[] = []; + + messages.forEach((msg) => { + const lastGroup = groups[groups.length - 1]; + if (lastGroup && lastGroup.date === msg.date) { + lastGroup.messages.push(msg); + } else { + groups.push({ date: msg.date, messages: [msg] }); + } + }); + + return groups; +} + +function formatKoreanDateString(dateStr: string): string { + const parts = dateStr.match(/\d+/g); + if (parts && parts.length >= 3) { + return `${parts[0]}년 ${parts[1]}월 ${parts[2]}일`; + } + return dateStr; +} + +export default function CallvanChatRoom({ postId }: CallvanChatRoomProps) { + const router = useRouter(); + const logger = useLogger(); + const token = useTokenState(); + const { data } = useSuspenseQuery(callvanQueries.chat(token ?? '', postId)); + const { data: postDetail } = useSuspenseQuery(callvanQueries.postDetail(token ?? '', postId)); + + const { mutate: sendMessage, isPending: isSending } = useSendCallvanChat(postId); + const [inputValue, setInputValue] = useState(''); + const messagesEndRef = useRef(null); + const fileInputRef = useRef(null); + const textareaRef = useRef(null); + const { uploadFile, isPending: isUploading } = useUploadFile(); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView(); + }, [data.messages]); + + const handleImageFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + try { + const { file_url } = await uploadFile({ domain: 'CALLVAN_CHAT', file }); + if (file_url) { + sendMessage({ is_image: true, content: file_url }); + } + } finally { + e.target.value = ''; + } + }; + + const handleImageClick = () => { + fileInputRef.current?.click(); + }; + + const handleSend = () => { + const content = inputValue.trim(); + if (!content || isSending) return; + + sendMessage( + { is_image: false, content }, + { + onSuccess: () => { + setInputValue(''); + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + } + }, + }, + ); + logger.actionEventClick({ event_label: 'callvan_chat_send', team: 'CAMPUS', value: '' }); + }; + + const handleInput = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.nativeEvent.isComposing) return; + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const senderColorMap = useMemo(() => { + const map = new Map(); + data.messages.forEach((msg) => { + if (!msg.is_mine && !map.has(msg.user_id)) { + map.set(msg.user_id, map.size); + } + }); + return map; + }, [data.messages]); + + const messageGroups = groupMessagesByDate(data.messages); + + return ( +
+
+ +
+
+ {postDetail.departure} + - + {postDetail.arrival} + {postDetail.departure_time} +
+
+ + + {postDetail.current_participants}/{postDetail.max_participants} + +
+
+
+
+ +
+ {messageGroups.length === 0 &&
아직 대화가 없습니다.
} + + {messageGroups.map((group) => ( +
+
+ {formatKoreanDateString(group.date)} +
+ + {group.messages.map((msg, idx) => { + const key = `${msg.user_id}-${msg.time}-${idx}`; + + if (msg.is_mine) { + return ( +
+ {msg.unread_count > 0 ? ( +
+ {msg.unread_count} + {msg.time} +
+ ) : ( + {msg.time} + )} + {msg.is_image ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + 업로드 이미지 +
+ ) : ( +
+ {msg.content} +
+ )} +
+ ); + } + + const showSender = idx === 0 || group.messages[idx - 1].user_id !== msg.user_id; + + return ( +
+ {showSender && ( +
+ +
+ {msg.sender_nickname} + {msg.is_left_user && (나간 사용자)} +
+
+ )} +
+ {msg.is_image ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + 업로드 이미지 +
+ ) : ( +
+ {msg.content} +
+ )} + {msg.unread_count > 0 ? ( +
+ {msg.unread_count} + {msg.time} +
+ ) : ( + {msg.time} + )} +
+
+ ); + })} +
+ ))} + +
+
+ +
+ + +