diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..fe030ed4 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm build *)", + "Bash(npm install *)", + "Bash(npx tsc *)", + "Bash(echo \"EXIT: $?\")", + "Bash(npm run *)", + "Read(//tmp/**)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:5173/)", + "Bash(kill %1)", + "Bash(wait)", + "Bash(curl -s \"https://api.themoviedb.org/3/search/movie?api_key=ca324ae23606391798bdf45057a9bf4f&query=inception&language=ko-KR&include_adult=false\")" + ] + } +} diff --git a/.claudeignore b/.claudeignore new file mode 100644 index 00000000..e69de29b diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..6d9eb9c8 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# 이 파일을 복사해 .env 로 만들고 본인 키를 채우세요. (.env 는 git에 커밋되지 않습니다) +VITE_TMDB_API_KEY=your_tmdb_api_key_here diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..ce760737 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# 모든 텍스트 파일의 줄바꿈을 저장소·작업트리 모두 LF로 통일한다. +# (Windows의 core.autocrlf 설정과 무관하게 CRLF 변환 경고가 더 이상 뜨지 않음) +* text=auto eol=lf + +# 바이너리로 다뤄 변환하지 않을 파일들 +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0e2fe2c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies +node_modules +.pnp +.pnp.js + +# build output +dist +dist-ssr +*.local +*.tsbuildinfo + +# env files (절대 커밋 금지 - TMDB API 키 보호) +.env +.env.* +!.env.example + +# logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# editor / os +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..c30c4e8a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Location + +All source code lives under `UMC-10th-mission-FE/`. Run all commands from that directory. + +## Commands + +```bash +# Install dependencies (pnpm) +pnpm install + +# Start dev server +pnpm dev + +# Type-check and build +pnpm build + +# Lint +pnpm lint +``` + +No test suite is configured. Use `pnpm build` to catch type errors. + +## Environment + +Create `UMC-10th-mission-FE/.env.local` with: + +``` +VITE_API_BASE_URL=http://localhost:8080 +``` + +## Architecture + +**Stack:** React 19 + TypeScript + Vite, TailwindCSS v4, TanStack Query v5, React Router v7, Axios, Zod + React Hook Form, Styled Components. + +**Entry point:** `src/main.tsx` → `src/App.tsx` + +### Routing (`src/App.tsx`) + +React Router v7 with a nested layout structure: + +``` +AppRoot (QueryClientProvider + AuthProvider) +└── HomeLayout (nav + sidebar + floating button) + ├── / (HomePage) + ├── /login, /signup, /v1/auth/google/callback + ├── /lps (LPListPage) + ├── /lps/:lpid (LPDetailPage) + └── PrivateLayout (redirects to /login if no accessToken) + ├── /mypage + └── /write +``` + +### Auth flow + +- `AuthContext` (`src/context/AuthContext.tsx`) holds `accessToken` in React state, initialized from `localStorage`. +- Tokens stored under keys in `src/constants/key.ts` (`accessToken`, `refreshToken`). +- `src/apis/axios.ts` — single `axiosInstance` with two interceptors: attaches Bearer token on every request, auto-refreshes on 401 (deduped with a shared `refreshPromise`), clears storage and redirects to `/login` on refresh failure. +- `PrivateLayout` reads `accessToken` from context and redirects unauthenticated users. + +### API layer (`src/apis/`) + +- `axios.ts` — configured `axiosInstance` (base URL from `VITE_API_BASE_URL`) +- `auth.ts` — sign-in, sign-up, logout, Google OAuth, my-info endpoints +- `lp.ts` — LP list (cursor-based), LP detail, LP comments + +### Data fetching hooks (`src/hooks/`) + +All hooks wrap TanStack Query: +- `useGetLpList` — `useInfiniteQuery` with cursor pagination; `queryKey: ["lps", sort]` +- `useGetLPDetail` — `useQuery` for a single LP +- `useGetLpComments` — comments for an LP +- `useGetMyInfo` — accepts `accessToken | null`; skips the query when null + +### Infinite scroll pattern + +`LPListPage` combines `useInfiniteQuery` + `react-intersection-observer`: a sentinel `
` at the bottom triggers `fetchNextPage()` via `useEffect` when `inView && hasNextPage && !isFetchingNextPage`. Initial load shows a grid of `` components; subsequent pages append more skeletons below the list while fetching. + +### Types (`src/types/`) + +- `CommonRes` — wrapper `{ status, message, data: T }` for all API responses +- `lp.ts` — `Lp`, `GetLpsResponse` +- `auth.ts` — auth DTOs + +### Styling + +Tailwind v4 utility classes throughout. Color palette: `#0f1014` backgrounds, `#FF1493` accent/primary, `#1a1a1a` / `#333` surfaces. `LpCardSkeleton` uses `animate-pulse` for loading states. diff --git a/MOVIE_SEARCH_README.md b/MOVIE_SEARCH_README.md new file mode 100644 index 00000000..d2f736c5 --- /dev/null +++ b/MOVIE_SEARCH_README.md @@ -0,0 +1,86 @@ +# 🎥 TMDB 영화 검색 + 렌더링 최적화 미션 + +Vite + React + TypeScript 로 만든 TMDB 영화 검색 사이트입니다. +`memo` / `useCallback` / `useMemo` 로 불필요한 리렌더를 제거하는 과정을 콘솔 로그로 확인할 수 있습니다. + +## 실행 방법 + +```bash +# 1. 의존성 설치 +npm install + +# 2. 환경변수 설정 (루트 .env) +# .env.example 을 복사해 .env 로 만들고 본인 TMDB 키를 넣으세요. +# VITE_TMDB_API_KEY=발급받은_키 +# ※ .env 는 .gitignore 에 포함되어 커밋되지 않습니다. + +# 3. 개발 서버 실행 +npm run dev # http://localhost:5173 + +# (선택) 타입체크 + 프로덕션 빌드 +npm run build +``` + +## 폴더 구조 (이번 미션에서 만든 파일) + +``` +. +├─ .env # VITE_TMDB_API_KEY (git 미추적) +├─ .env.example # 키 자리표시자 템플릿 +├─ .gitignore # .env 포함 +├─ index.html +├─ package.json +├─ tsconfig.json +├─ vite.config.ts +└─ src/ + ├─ main.tsx + ├─ App.tsx # MovieSearch 렌더 + ├─ index.css + ├─ vite-env.d.ts # VITE_TMDB_API_KEY 타입 선언 + ├─ apis/ + │ └─ tmdb.ts # search/movie 호출 (키는 import.meta.env 로만 사용) + ├─ types/ + │ └─ movie.ts # Movie / 응답 / Language 타입 + └─ components/ + ├─ MovieSearch.tsx # 부모: 검색 폼 + 상태 + 최적화 훅 + └─ MovieCard.tsx # 자식: memo 적용 카드 +``` + +## 단계별 구현 내용 + +### 1단계 — 기본 검색 기능 (`MovieSearch.tsx`) +- 상단 검색 영역을 `
` 으로 감싸 **엔터만으로도 검색** (`onSubmit` + `e.preventDefault()`). +- 영화 제목 `text input` — `placeholder="영화 제목을 입력하세요"`, 값은 `title` state. +- 성인 콘텐츠 `checkbox` — `includeAdult` boolean state → API `include_adult` 파라미터. +- 언어 `select` — 한국어(`ko-KR`)·영어(`en-US`)·일본어(`ja-JP`), `language` state → API `language` 파라미터. +- TMDB `search/movie` 호출 후 포스터·제목·평점·개요를 리스트로 렌더. +- **로딩 상태** 표시, **빈 검색어**(`trim()` 후 빈 값)는 호출하지 않음. + +### 2단계 — 리렌더 추적 로그 +- 부모(`MovieSearch`)와 자식(`MovieCard`)에 각각 `console.log` 삽입. +- 검색어를 타이핑하면 `title` state가 바뀌어 부모가 리렌더되고, + 최적화 전에는 **목록이 그대로인데도 모든 카드가 다시 렌더**되는 걸 콘솔에서 볼 수 있음. + +### 3단계 — 최적화 적용 +- **`memo`** (`MovieCard.tsx`): 카드 컴포넌트를 메모이제이션 → `movie`, `onSelect` props가 같으면 리렌더 건너뜀. +- **`useCallback`** (`MovieSearch.tsx`의 `handleSelect`): memo된 카드에 넘기는 핸들러의 **참조를 고정**. + 인라인 함수로 넘기면 매 렌더마다 새 함수가 되어 memo가 무력화되므로 필수. +- **`useMemo`** (`sortedMovies`): 평점순 정렬을 `movies`가 바뀔 때만 재계산. +- 반대로 **`handleSubmit`** 은 form에서만 쓰고 자식에게 넘기지 않으므로 `useCallback`으로 감싸지 않음(효과 없음). + +## 최적화 전 / 후 콘솔 로그 차이 + +검색 결과가 떠 있는 상태에서 **검색창에 글자 한 개를 타이핑**할 때: + +| 구분 | 부모 로그 | 카드 로그 | +| --- | --- | --- | +| **최적화 전** (memo·useCallback 없음) | `👪 [MovieSearch] render` 1회 | `🎬 [MovieCard] render` **카드 수만큼** (예: 20회) ❌ | +| **최적화 후** (memo + useCallback) | `👪 [MovieSearch] render` 1회 | `🎬 [MovieCard] render` **0회** ✅ | + +- 부모는 `title` state가 바뀌므로 어느 경우든 리렌더된다(정상). +- 핵심은 **목록이 바뀌지 않았는데 자식 카드가 다시 그려지지 않는 것**. + `movie` prop은 `useMemo`로 안정적인 배열에서 오고, `onSelect`는 `useCallback`으로 참조가 고정되어 + `memo`의 얕은 비교를 통과 → 카드 렌더가 생략된다. +- 새로 **검색을 실행**하면 `movies`가 바뀌어 `useMemo`가 재계산되고 카드들이 정상적으로 렌더된다. + +> 참고: `main.tsx`가 `StrictMode`라 개발 모드에서는 초기 렌더 로그가 의도적으로 2번씩 찍힐 수 있습니다(프로덕션 빌드에는 영향 없음). 타이핑 시 카드 로그가 사라지는지로 최적화 효과를 확인하세요. diff --git a/OPTIMIZATION_NOTES.md b/OPTIMIZATION_NOTES.md new file mode 100644 index 00000000..c39f59ea --- /dev/null +++ b/OPTIMIZATION_NOTES.md @@ -0,0 +1,135 @@ +# 🚀 성능 최적화 정리 (React.memo / useCallback / useMemo) + +> React DevTools **Profiler 탭**으로 리렌더를 관찰하고, 불필요한 상위/하위 리렌더를 +> `memo` · `useCallback` · `useMemo`로 제거한 과정을 정리한 문서입니다. + +## 0. Profiler로 무엇을 보나 + +1. Chrome 웹스토어에서 **React Developer Tools** 설치 → 개발 모드(`npm run dev`)로 앱 실행. +2. DevTools → **⚛️ Profiler** 탭 → 좌상단 **● Record** 클릭. +3. 영화 검색 / 필터링(검색어 타이핑, 언어 변경 등)을 수행 → **Stop**. +4. **Flamegraph / Ranked** 차트에서 *회색(렌더 안 됨)* vs *색칠(리렌더됨)* 을 확인. + - 설정 ⚙️ → **"Highlight updates when components render"** 를 켜면 화면에서도 리렌더된 컴포넌트에 테두리가 깜빡인다. + +관찰 포인트: **목록 데이터가 안 바뀌었는데도 영화 카드들이 리렌더되는가?** + +--- + +## 1. TMDB 영화 검색 — 최적화 전 / 후 + +### 문제 (최적화 전) + +검색창에 글자 하나를 타이핑 → 부모 `MovieSearch`의 `title` state 변경 → 부모 리렌더. +이때 아래 두 가지 때문에 **목록이 그대로인데도 모든 `MovieCard`가 같이 리렌더**된다. + +- `MovieCard`가 일반 컴포넌트라 부모가 렌더되면 무조건 자식도 렌더. +- 카드에 넘기는 `onSelect`가 **인라인 화살표 함수**라 매 렌더마다 새 참조. +- 정렬된 배열을 **렌더마다 새로 `sort()`** → 새 배열 참조. + +### 해결 (최적화 후) + +| 훅 | 위치 | 역할 | +| --- | --- | --- | +| `memo` | `MovieCard.tsx` | `movie`·`onSelect` props가 얕은 비교로 같으면 리렌더 건너뜀 | +| `useCallback` | `MovieSearch.tsx` `handleSelect`, `handleCloseModal` (deps `[]`) | memo된 카드에 넘기는 핸들러 참조 고정 → memo 유지 | +| `useMemo` | `MovieSearch.tsx` `sortedMovies` (deps `[movies]`) | 평점순 정렬을 `movies` 변경 시에만 재계산 + 안정적 배열 참조 | + +> `handleSubmit`은 form에서만 쓰고 자식에게 안 넘기므로 일부러 `useCallback`을 쓰지 않았다(효과 없음). + +### 콘솔 로그로 본 차이 (Profiler의 대용 증거) + +검색 결과 20개가 떠 있는 상태에서 **검색창에 글자 1개 입력** 시: + +| 구분 | `👪 MovieSearch` | `🎬 MovieCard` | +| --- | --- | --- | +| 최적화 전 | 1회 | **20회** ❌ | +| 최적화 후 | 1회 | **0회** ✅ | + +- 부모는 입력 state가 바뀌므로 어느 쪽이든 1회 렌더(정상). +- 핵심은 **목록 미변경 시 카드 리렌더가 0회**가 된 것. Profiler Flamegraph에서도 카드들이 회색(렌더 안 됨)으로 표시된다. +- 새로 **검색을 실행**하면 `movies`가 바뀌어 `useMemo` 재계산 + 카드 정상 렌더 → 의도대로 동작. + +**결론:** 현재 TMDB 앱은 상위 전체가 불필요하게 리렌더되는 구간이 없다(이미 최적화 완료 상태). + +--- + +## 2. LP 사이트 성능 개선 포인트 (3개 이상) + +> 현재 LP 프로젝트(`UMC-10th-mission-FE`)는 새 미션을 위해 working tree에서 제거된 상태라, +> 아래는 git 기록의 실제 코드(`LPListPage.tsx`, `CommentSection.tsx`)를 기준으로 한 개선안이다. +> 프로젝트를 복구(`git restore`)하면 그대로 적용 가능. + +### ① LP 카드를 별도 컴포넌트로 분리 + `React.memo` + +**문제:** `LPListPage`에서 카드를 인라인 `
`로 `.map()` 렌더 중. 정렬 토글(`setSort`)이나 +다음 페이지 로드(`isFetchingNextPage`) 등으로 페이지가 리렌더되면 **기존 카드 전부가 다시 렌더**된다. + +```tsx +// components/LpCard.tsx (신규) +import { memo } from "react"; +import type { Lp } from "../types/lp"; + +interface LpCardProps { + lp: Lp; + onClick: (id: number) => void; // 인라인 navigate 대신 id만 받는 안정적 핸들러 +} + +function LpCardBase({ lp, onClick }: LpCardProps) { + return ( +
onClick(lp.id)} className="..."> + {/* 기존 카드 마크업 */} +
+ ); +} +export default memo(LpCardBase); // props 같으면 리렌더 skip +``` + +### ② 카드 클릭 핸들러를 `useCallback`으로 고정 + +**문제:** 기존엔 카드마다 `onClick={() => navigate(`/lps/${lp.id}`)}` 인라인 함수 → memo를 무력화. + +```tsx +// LPListPage.tsx +const handleCardClick = useCallback( + (id: number) => navigate(`/lps/${id}`), + [navigate] +); +// ... +{lpList.map((lp) => )} +``` + +→ ①+② 조합으로, 정렬 토글/추가 로딩 시 **새로 들어온 카드만 렌더**되고 기존 카드는 skip. + +### ③ `flatMap` 결과를 `useMemo`로 메모이제이션 + +**문제:** `const lpList = data?.pages.flatMap(...) || []` 가 **렌더마다 새 배열** 생성 → +하위에 넘기면 참조가 매번 달라져 memo가 깨진다. + +```tsx +const lpList = useMemo( + () => data?.pages.flatMap((page) => page.data.data) ?? [], + [data] +); +``` + +### ④ (보너스) CommentSection 댓글 항목 분리 + memo + +**문제:** `CommentSection`은 `commentText`(입력창), `openMenuId`, `editingId` 등 state가 많아 +**댓글을 한 글자 타이핑할 때마다 댓글 리스트 전체가 리렌더**된다. + +```tsx +// 댓글 한 개를 CommentItem으로 분리하고 memo로 감싼다. +const CommentItem = memo(function CommentItem({ comment, myId, onEdit, onDelete }) { ... }); +// 부모에서 onEdit/onDelete는 useCallback으로 고정. +``` + +→ 입력창 타이핑 시 입력 영역만 리렌더되고, 기존 댓글들은 Profiler에서 회색(skip)으로 유지. + +--- + +## 적용한 최적화 요약 (한 줄) + +- **TMDB**: `MovieCard`(memo) + `handleSelect/handleCloseModal`(useCallback) + `sortedMovies`(useMemo) + → 타이핑 시 카드 리렌더 20회 → 0회. +- **LP**: 카드/댓글 컴포넌트 분리 후 `memo`, 클릭·수정·삭제 핸들러 `useCallback`, 목록 배열 `useMemo` + → 정렬 토글·추가 로딩·댓글 입력 시 기존 항목 리렌더 제거. diff --git a/index.html b/index.html new file mode 100644 index 00000000..43f5c985 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + TMDB 영화 검색 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..9f1a822e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1881 @@ +{ + "name": "tmdb-movie-search", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tmdb-movie-search", + "version": "0.0.0", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.17.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.7.2", + "vite": "^6.0.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "license": "MIT", + "dependencies": { + "react-router": "7.17.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..0250d575 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "tmdb-movie-search", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "tsc --noEmit" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.17.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.7.2", + "vite": "^6.0.5" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..0299c1c4 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,16 @@ +import { Routes, Route } from "react-router-dom"; +import MovieSearch from "./components/MovieSearch"; +import MovieDetailPage from "./pages/MovieDetailPage"; + +function App() { + return ( + + {/* 홈: 검색 페이지 */} + } /> + {/* 영화 상세 페이지 라우팅 (/movies/:movieId) */} + } /> + + ); +} + +export default App; diff --git a/src/apis/tmdb.ts b/src/apis/tmdb.ts new file mode 100644 index 00000000..e2cd4a16 --- /dev/null +++ b/src/apis/tmdb.ts @@ -0,0 +1,44 @@ +import type { Language, Movie, SearchMoviesResponse } from "../types/movie"; + +// 키는 절대 하드코딩하지 않고 환경변수로만 읽는다. (.env -> import.meta.env) +const API_KEY = import.meta.env.VITE_TMDB_API_KEY; +const BASE_URL = "https://api.themoviedb.org/3"; + +// 포스터 이미지 베이스 URL (카드용 w342) +export const POSTER_BASE_URL = "https://image.tmdb.org/t/p/w342"; +// 모달용 큰 포스터 (w500) +export const POSTER_BASE_URL_LARGE = "https://image.tmdb.org/t/p/w500"; + +interface SearchParams { + query: string; + includeAdult: boolean; + language: Language; +} + +export async function searchMovies({ + query, + includeAdult, + language, +}: SearchParams): Promise { + if (!API_KEY) { + throw new Error( + "VITE_TMDB_API_KEY 가 설정되지 않았습니다. 루트의 .env 파일을 확인하세요." + ); + } + + const url = new URL(`${BASE_URL}/search/movie`); + url.searchParams.set("api_key", API_KEY); + url.searchParams.set("query", query); + // 성인 콘텐츠 포함 여부 (checkbox state 반영) + url.searchParams.set("include_adult", String(includeAdult)); + // 언어 (select state 반영) + url.searchParams.set("language", language); + + const res = await fetch(url); + if (!res.ok) { + throw new Error(`TMDB 요청 실패: ${res.status}`); + } + + const data: SearchMoviesResponse = await res.json(); + return data.results; +} diff --git a/src/components/MovieCard.tsx b/src/components/MovieCard.tsx new file mode 100644 index 00000000..d6b0c8c1 --- /dev/null +++ b/src/components/MovieCard.tsx @@ -0,0 +1,45 @@ +import { memo } from "react"; +import type { Movie } from "../types/movie"; +import { POSTER_BASE_URL } from "../apis/tmdb"; + +interface MovieCardProps { + movie: Movie; + // 부모에서 useCallback으로 참조를 고정해 넘겨야 memo가 제대로 동작한다. + onSelect: (movie: Movie) => void; +} + +function MovieCardBase({ movie, onSelect }: MovieCardProps) { + // [2단계] 자식 렌더 추적용 로그. + // 최적화 전: 부모에서 검색어를 타이핑할 때마다 이 로그가 카드 수만큼 찍힌다. + // 최적화 후(memo + useCallback): props가 같으면 이 로그가 더 이상 찍히지 않는다. + console.log("🎬 [MovieCard] render:", movie.title); + + return ( +
  • onSelect(movie)}> + {movie.poster_path ? ( + {movie.title} + ) : ( +
    + No Image +
    + )} +
    +

    {movie.title}

    +

    ⭐ {movie.vote_average.toFixed(1)}

    +

    + {movie.overview || "개요 정보가 없습니다."} +

    +
    +
  • + ); +} + +// [3단계] memo: props(movie, onSelect)가 얕은 비교로 같으면 리렌더를 건너뛴다. +// onSelect가 매 렌더마다 새 함수면 memo가 무력화되므로 부모에서 useCallback 필수. +const MovieCard = memo(MovieCardBase); +export default MovieCard; diff --git a/src/components/MovieModal.tsx b/src/components/MovieModal.tsx new file mode 100644 index 00000000..9b0c21f5 --- /dev/null +++ b/src/components/MovieModal.tsx @@ -0,0 +1,111 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import type { Movie } from "../types/movie"; +import { POSTER_BASE_URL_LARGE } from "../apis/tmdb"; + +interface MovieModalProps { + movie: Movie; + onClose: () => void; +} + +function MovieModal({ movie, onClose }: MovieModalProps) { + const navigate = useNavigate(); + + // ESC 키로 닫기 + 모달이 열린 동안 배경 스크롤 잠금 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", handleKeyDown); + document.body.style.overflow = "hidden"; + + return () => { + window.removeEventListener("keydown", handleKeyDown); + document.body.style.overflow = ""; + }; + }, [onClose]); + + // IMDb 검색 URL (영화 제목을 인코딩) + const imdbUrl = `https://www.imdb.com/find?q=${encodeURIComponent( + movie.title + )}`; + + return ( + // overlay 클릭 시 닫힘 +
    + {/* 모달 박스 내부 클릭은 닫히지 않도록 버블링 차단 */} +
    e.stopPropagation()} + > + + + {/* 상단 포스터 (없으면 placeholder) */} + {movie.poster_path ? ( + {movie.title} + ) : ( +
    No Image
    + )} + +
    +

    {movie.title}

    + +
    + + ⭐ {movie.vote_average.toFixed(1)} + + + 개봉일: {movie.release_date || "정보 없음"} + +
    + +

    + {movie.overview || "줄거리 정보가 없습니다."} +

    + +
    + + IMDb에서 검색하기 + + {/* /movies/:movieId 로 라우팅 이동 (라우트가 바뀌며 모달은 자동 언마운트) */} + + +
    +
    +
    +
    + ); +} + +export default MovieModal; diff --git a/src/components/MovieSearch.tsx b/src/components/MovieSearch.tsx new file mode 100644 index 00000000..ab276c17 --- /dev/null +++ b/src/components/MovieSearch.tsx @@ -0,0 +1,132 @@ +import { useCallback, useMemo, useState, type FormEvent } from "react"; +import type { Language, Movie } from "../types/movie"; +import { searchMovies } from "../apis/tmdb"; +import MovieCard from "./MovieCard"; +import MovieModal from "./MovieModal"; + +function MovieSearch() { + // [1단계] 검색 폼 상태들 + const [title, setTitle] = useState(""); // text input 값 + const [includeAdult, setIncludeAdult] = useState(false); // checkbox 값 + const [language, setLanguage] = useState("ko-KR"); // select 값 + + // 검색 결과 / 로딩 / 에러 상태 + const [movies, setMovies] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // 상세 모달에 띄울 영화. null이면 모달이 닫힌 상태. + const [selectedMovie, setSelectedMovie] = useState(null); + + // [2단계] 부모 렌더 추적용 로그. + // 검색어를 한 글자 칠 때마다 title state가 바뀌어 이 로그가 찍힌다(부모 리렌더). + console.log("👪 [MovieSearch] render. title =", title); + + // form 제출 시에만 실제 검색을 수행한다 (엔터 입력만으로도 동작). + // 주의: handleSubmit은 form에서만 쓰고 memo된 자식에게 넘기지 않으므로 + // useCallback으로 감싸도 이득이 없다 -> 일부러 감싸지 않는다. + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + const trimmed = title.trim(); + if (!trimmed) return; // 빈 검색어는 API 호출하지 않음 + + setIsLoading(true); + setError(null); + try { + const results = await searchMovies({ + query: trimmed, + includeAdult, + language, + }); + setMovies(results); + } catch (err) { + setError(err instanceof Error ? err.message : "검색 중 오류가 발생했습니다."); + setMovies([]); + } finally { + setIsLoading(false); + } + }; + + // [3단계] useCallback: memo된 MovieCard에 넘기는 핸들러의 참조를 고정한다. + // setState 함수만 사용하고 외부 값에 의존하지 않으므로 deps는 빈 배열 -> 참조가 항상 동일. + // 이렇게 해야 타이핑(부모 리렌더) 시에도 카드의 onSelect prop이 바뀌지 않아 memo가 유지된다. + // 카드를 클릭하면 해당 영화를 selectedMovie로 지정 -> 상세 모달 오픈. + const handleSelect = useCallback((movie: Movie) => { + setSelectedMovie(movie); + }, []); + + // 모달 닫기: selectedMovie를 null로. 마찬가지로 참조 고정. + const handleCloseModal = useCallback(() => { + setSelectedMovie(null); + }, []); + + // [3단계] useMemo: 평점순(내림차순) 정렬은 movies가 바뀔 때만 다시 계산한다. + // 타이핑으로 부모가 리렌더돼도 movies 참조가 그대로면 정렬을 재실행하지 않고, + // 같은 배열 참조를 반환하므로 카드들의 movie prop도 안정적으로 유지된다. + const sortedMovies = useMemo(() => { + console.log("🧮 [useMemo] 평점순 정렬 계산"); + return [...movies].sort((a, b) => b.vote_average - a.vote_average); + }, [movies]); + + return ( +
    +

    🎥 TMDB 영화 검색

    + + {/* 검색 영역: form으로 감싸 엔터만으로도 검색 */} + + setTitle(e.target.value)} + /> + + + + + + + + + {/* 상태 표시 */} + {isLoading &&

    불러오는 중...

    } + {error &&

    {error}

    } + {!isLoading && !error && sortedMovies.length === 0 && ( +

    검색 결과가 여기에 표시됩니다.

    + )} + + {/* 결과 리스트 (평점순 정렬된 sortedMovies 사용) */} +
      + {sortedMovies.map((movie) => ( + + ))} +
    + + {/* selectedMovie가 있을 때만 상세 모달 렌더 */} + {selectedMovie && ( + + )} +
    + ); +} + +export default MovieSearch; diff --git a/src/index.css b/src/index.css new file mode 100644 index 00000000..e759145b --- /dev/null +++ b/src/index.css @@ -0,0 +1,310 @@ +:root { + /* 밝은 테마 팔레트 */ + --bg: #f4f5f8; + --surface: #ffffff; + --border: #e4e6eb; + --accent: #ff1493; + --text: #1c1d21; + --muted: #6b7280; + --shadow: 0 6px 20px rgba(0, 0, 0, 0.08); + + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + color: var(--text); + background-color: var(--bg); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background-color: var(--bg); +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 32px 20px 64px; +} + +.heading { + text-align: center; + margin-bottom: 24px; +} + +.search-form { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: center; + margin-bottom: 32px; +} + +.search-form__input { + flex: 1 1 280px; + padding: 10px 14px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + font-size: 15px; +} + +.search-form__input::placeholder { + color: var(--muted); +} + +.search-form__select { + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + font-size: 15px; +} + +.search-form__checkbox { + display: flex; + align-items: center; + gap: 6px; + color: var(--muted); + font-size: 14px; + white-space: nowrap; +} + +.search-form__button { + padding: 10px 22px; + border-radius: 8px; + border: none; + background: var(--accent); + color: white; + font-size: 15px; + font-weight: 600; + cursor: pointer; +} + +.search-form__button:hover { + opacity: 0.9; +} + +.status { + text-align: center; + color: var(--muted); + margin-top: 24px; +} + +.status--error { + color: #e0245e; +} + +.movie-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 20px; +} + +.movie-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + cursor: pointer; + display: flex; + flex-direction: column; + box-shadow: var(--shadow); + transition: transform 0.15s ease, border-color 0.15s ease; +} + +.movie-card:hover { + transform: translateY(-4px); + border-color: var(--accent); +} + +.movie-card__poster { + width: 100%; + aspect-ratio: 2 / 3; + object-fit: cover; + display: block; +} + +.movie-card__poster--empty { + display: flex; + align-items: center; + justify-content: center; + color: var(--muted); + background: #eceef2; +} + +.movie-card__body { + padding: 12px 14px 16px; +} + +.movie-card__title { + margin: 0 0 6px; + font-size: 16px; +} + +.movie-card__rating { + margin: 0 0 8px; + color: #e0a800; + font-size: 14px; +} + +.movie-card__overview { + margin: 0; + color: var(--muted); + font-size: 13px; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* ===== 상세 모달 ===== */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); /* 반투명 어두운 배경 */ + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + z-index: 1000; +} + +.modal { + position: relative; + background: var(--surface); + color: var(--text); + border-radius: 16px; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25); + width: 100%; + max-width: 520px; + max-height: 85vh; + overflow-y: auto; /* 정보가 길면 모달 내부 스크롤 */ +} + +.modal__close { + position: absolute; + top: 12px; + right: 12px; + width: 34px; + height: 34px; + border: none; + border-radius: 50%; + background: rgba(0, 0, 0, 0.55); + color: #fff; + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.modal__close:hover { + background: rgba(0, 0, 0, 0.75); +} + +.modal__poster { + width: 100%; + max-height: 420px; + object-fit: cover; + display: block; + border-radius: 16px 16px 0 0; +} + +.modal__poster--empty { + width: 100%; + height: 280px; + display: flex; + align-items: center; + justify-content: center; + color: var(--muted); + background: #eceef2; + border-radius: 16px 16px 0 0; +} + +.modal__body { + padding: 20px 24px 24px; +} + +.modal__title { + margin: 0 0 12px; + font-size: 22px; +} + +.modal__meta { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 16px; + font-size: 14px; + color: var(--muted); +} + +.modal__rating { + color: #e0a800; + font-weight: 600; +} + +.modal__overview { + margin: 0 0 24px; + line-height: 1.6; + font-size: 15px; +} + +.modal__actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.modal__imdb { + flex: 1 1 auto; + text-align: center; + padding: 11px 18px; + border-radius: 8px; + background: #f5c518; /* IMDb 옐로우 */ + color: #1c1d21; + font-weight: 700; + text-decoration: none; +} + +.modal__imdb:hover { + opacity: 0.9; +} + +.modal__detail-btn { + padding: 11px 18px; + border-radius: 8px; + border: none; + background: var(--accent); + color: #fff; + font-size: 15px; + font-weight: 600; + cursor: pointer; +} + +.modal__detail-btn:hover { + opacity: 0.9; +} + +.modal__close-btn { + padding: 11px 22px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + font-size: 15px; + cursor: pointer; +} + +.modal__close-btn:hover { + background: #f0f1f4; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 00000000..c04a288d --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import "./index.css"; +import App from "./App.tsx"; + +createRoot(document.getElementById("root")!).render( + + {/* SPA 라우팅: BrowserRouter로 전체를 감싼다 */} + + + + +); diff --git a/src/pages/MovieDetailPage.tsx b/src/pages/MovieDetailPage.tsx new file mode 100644 index 00000000..e505c8a6 --- /dev/null +++ b/src/pages/MovieDetailPage.tsx @@ -0,0 +1,21 @@ +import { Link, useParams } from "react-router-dom"; + +// 미션 2 요구사항: /movies/:movieId 라우팅 동작 확인용 페이지. +// (실제 디자인은 필요 없으므로 파라미터가 잘 넘어오는지만 보여준다.) +function MovieDetailPage() { + const { movieId } = useParams<{ movieId: string }>(); + + return ( +
    +

    🎬 영화 상세 페이지

    +

    + URL 파라미터 movieId = {movieId} +

    +

    + ← 검색 페이지로 돌아가기 +

    +
    + ); +} + +export default MovieDetailPage; diff --git a/src/types/movie.ts b/src/types/movie.ts new file mode 100644 index 00000000..89d3c5cc --- /dev/null +++ b/src/types/movie.ts @@ -0,0 +1,19 @@ +// TMDB search/movie 응답에서 우리가 사용하는 필드만 추린 타입 +export interface Movie { + id: number; + title: string; + overview: string; + poster_path: string | null; + vote_average: number; + adult: boolean; + release_date?: string; +} + +export interface SearchMoviesResponse { + page: number; + results: Movie[]; + total_pages: number; + total_results: number; +} + +export type Language = "ko-KR" | "en-US" | "ja-JP"; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 00000000..ca54fec6 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +// import.meta.env.VITE_TMDB_API_KEY 에 타입을 부여한다. +interface ImportMetaEnv { + readonly VITE_TMDB_API_KEY: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..54053432 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..0f32683a --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..b2c2d6b7 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// Vite는 프로젝트 루트의 .env에서 VITE_ 접두사 변수를 자동으로 로드한다. +export default defineConfig({ + plugins: [react()], +});