From 2f5c608cd69852503d45220acd45a704daff284f Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Thu, 5 Feb 2026 14:17:48 +0900 Subject: [PATCH 01/81] =?UTF-8?q?chore:=20gitignore=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 12684366..f8492389 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ bun.lock .env .env.local .env.*.local +.env.production +.env.development *.local !.env.local.example From 6a149093e625ecdcf72808519670776ae0aa8adc Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Fri, 6 Feb 2026 02:02:26 +0900 Subject: [PATCH 02/81] =?UTF-8?q?fix:=20dto=ED=83=80=EC=9E=85=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=94=EA=B8=B0=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/dto/video.dto.ts | 30 ++++++++++-------------------- src/api/endpoints/videos.ts | 6 +++--- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/api/dto/video.dto.ts b/src/api/dto/video.dto.ts index c34b3e5d..b2f19532 100644 --- a/src/api/dto/video.dto.ts +++ b/src/api/dto/video.dto.ts @@ -1,14 +1,5 @@ /** - * 영상 타임스탬프 댓글 생성 - */ -export interface CreateOpinionDto { - content: string; - /** 답글인 경우 부모 의견 내용 고쳐라이D */ - parentId?: string; -} - -/** - * 영상 녹화 시작 요청 DTO + * 영상 녹화 세션 생성 요청 DTO */ export interface StartVideoRequestDto { projectId: number; @@ -19,11 +10,17 @@ export interface StartVideoRequestDto { * 영상 녹화 시작 응답 DTO (success 데이터) */ export interface StartVideoResponseDto { - videoId: number; + videoId: string; +} +/** + * 청크 업로드 응답 DTO (success 데이터) + */ +export interface ChunkUploadResponseDto { + ok: boolean; } /** - * 영상 녹화 완료 요청 DTO + * 녹화 종료 및 영상 처리 시작 요청 DTO */ export interface FinishVideoRequestDto { slideLogs: Array<{ @@ -33,7 +30,7 @@ export interface FinishVideoRequestDto { } /** - * 영상 녹화 완료 응답 DTO (success 데이터) + * 녹화 종료 및 영상 처리 시작 응답 DTO (success 데이터) */ export interface FinishVideoResponseDto { videoId: string; @@ -44,10 +41,3 @@ export interface FinishVideoResponseDto { totalDurationMs: number; }>; } - -/** - * 청크 업로드 응답 DTO (success 데이터) - */ -export interface ChunkUploadResponseDto { - ok: boolean; -} diff --git a/src/api/endpoints/videos.ts b/src/api/endpoints/videos.ts index 6759d63a..d826f2d1 100644 --- a/src/api/endpoints/videos.ts +++ b/src/api/endpoints/videos.ts @@ -30,12 +30,12 @@ export const videosApi = { }, // POST /videos/{videoId}/finish - 녹화 종료 및 영상 처리 시작 - finishVideo: (videoId: number, data: FinishVideoRequestDto) => + finishVideo: (videoId: string, data: FinishVideoRequestDto) => apiClient.post>(`/videos/${videoId}/finish`, data), // GET /videos/{videoId} - 영상 상세 조회 - getVideoDetail: (videoId: number) => apiClient.get(`/videos/${videoId}`), + getVideoDetail: (videoId: string) => apiClient.get(`/videos/${videoId}`), // GET /videos/{videoId}/slides - 슬라이드 타임라인 조회 - getVideoSlides: (videoId: number) => apiClient.get(`/videos/${videoId}/slides`), + getVideoSlides: (videoId: string) => apiClient.get(`/videos/${videoId}/slides`), }; From 4f1993ac257367d11fb534623f8bd733c824ba57 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Fri, 6 Feb 2026 15:27:48 +0900 Subject: [PATCH 03/81] =?UTF-8?q?fix:=20ide=20=EC=98=A4=EB=A5=98=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 1a3e50db0fe6a74e59f5d1907e6b07f420a1f539 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Sat, 7 Feb 2026 09:35:23 +0900 Subject: [PATCH 04/81] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=94=94?= =?UTF-8?q?=EB=B2=A8=EB=A1=AD=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 1 + .gitignore | 2 ++ package.json | 3 ++- src/hooks/useMediaStream.ts | 3 ++- src/hooks/useVideoUpload.ts | 4 ++-- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.env.development b/.env.development index c9e0ce25..aadb2afd 100644 --- a/.env.development +++ b/.env.development @@ -3,3 +3,4 @@ VITE_API_URL=https://ttorang-server-407623424780.asia-northeast3.run.app/ VITE_APP_TITLE=또랑 (개발) +VITE_API_MOCKING=true diff --git a/.gitignore b/.gitignore index 4363ab31..f277acf0 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ bun.lock .env.development *.local !.env.local.example +.env.development +.env.production # Editor directories and files .vscode/* diff --git a/package.json b/package.json index 22d2e9fc..1cbeabaa 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "dev:local": "VITE_API_MOCKING=true vite", + "dev:local": " cross-env VITE_API_MOCKING=true vite", "build": "tsc -b && vite build", "type-check": "tsc --noEmit", "lint": "eslint .", @@ -48,6 +48,7 @@ "@typescript-eslint/eslint-plugin": "^8.50.1", "@typescript-eslint/parser": "^8.50.1", "@vitejs/plugin-react-swc": "^4.2.2", + "cross-env": "^10.1.0", "eslint": "^8", "eslint-config-prettier": "^10.1.8", "eslint-plugin-check-file": "^2.8.0", diff --git a/src/hooks/useMediaStream.ts b/src/hooks/useMediaStream.ts index ee87a60a..18809917 100644 --- a/src/hooks/useMediaStream.ts +++ b/src/hooks/useMediaStream.ts @@ -133,7 +133,8 @@ export const useMediaStream = (videoDeviceId?: string, audioDeviceId?: string) = return () => { cleanupStream(); }; - }, [videoDeviceId, audioDeviceId, startStream, cleanupStream]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [videoDeviceId, audioDeviceId]); return { stream, diff --git a/src/hooks/useVideoUpload.ts b/src/hooks/useVideoUpload.ts index e416e678..98379cc8 100644 --- a/src/hooks/useVideoUpload.ts +++ b/src/hooks/useVideoUpload.ts @@ -50,7 +50,7 @@ export const useVideoUpload = () => { throw new Error(startData.error?.reason || 'Video ID를 받지 못했습니다.'); } - const videoId = startData.success.videoId; + const videoId = Number(startData.success.videoId); const CHUNK_SIZE = 1024 * 1024; // 1MB const totalChunks = Math.ceil(videoBlob.size / CHUNK_SIZE); const chunks: Blob[] = []; @@ -93,7 +93,7 @@ export const useVideoUpload = () => { currentStep: 'finishing', }); - const finishResponse = await videosApi.finishVideo(videoId, { slideLogs }); + const finishResponse = await videosApi.finishVideo(videoId.toString(), { slideLogs }); const finishData: ApiResponse = finishResponse.data; if (finishData.resultType === 'FAILURE') { From 7e7c90d04eafc997a7af7a1ca618a5d572e7f20c Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Sun, 8 Feb 2026 01:33:29 +0900 Subject: [PATCH 05/81] =?UTF-8?q?feat:=20pd=5Fvid=5F04=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/dto/video.dto.ts | 18 ++++ src/api/endpoints/videos.ts | 20 ++++ src/components/video/VideoListPage.tsx | 130 ++++++++++++------------- src/hooks/useVideoUpload.ts | 25 +++++ src/hooks/useVideos.ts | 53 ++++++++++ src/mocks/handlers.ts | 64 +++++++++++- src/mocks/videos.ts | 118 +++++++++++++++++++++- src/types/video.ts | 23 +++++ 8 files changed, 383 insertions(+), 68 deletions(-) create mode 100644 src/hooks/useVideos.ts diff --git a/src/api/dto/video.dto.ts b/src/api/dto/video.dto.ts index b2f19532..0e5934e1 100644 --- a/src/api/dto/video.dto.ts +++ b/src/api/dto/video.dto.ts @@ -1,3 +1,21 @@ +export interface VideoDto { + videoId: string; + title: string; + status: 'processing' | 'ready' | 'failed'; + durationSeconds: number; + rootCommentCount: number; + replyCount: number; + reactionCount: number; + viewCount: number; + thumbnailUrl: string; + createdAt: string; +} +/** + * 내 영상 목록 조회 응답 DTO + */ +export interface GetMyVideosResponseDto { + videos: VideoDto[]; +} /** * 영상 녹화 세션 생성 요청 DTO */ diff --git a/src/api/endpoints/videos.ts b/src/api/endpoints/videos.ts index d826f2d1..999027ff 100644 --- a/src/api/endpoints/videos.ts +++ b/src/api/endpoints/videos.ts @@ -8,6 +8,8 @@ import type { } from '@/api/dto'; import type { ApiResponse } from '@/types/api'; +import type { GetMyVideosResponseDto } from '../dto/video.dto'; + export const videosApi = { // POST /videos/start - 영상 녹화 세션 생성 startVideo: (data: StartVideoRequestDto) => @@ -38,4 +40,22 @@ export const videosApi = { // GET /videos/{videoId}/slides - 슬라이드 타임라인 조회 getVideoSlides: (videoId: string) => apiClient.get(`/videos/${videoId}/slides`), + /** + * GET /me/videos - 내 영상 목록 조회 + */ + getMyVideos: (params?: { + search?: string; + filter?: '3m' | '5m'; // 'all' 제외, undefined로 전체 조회 + sort?: 'recent' | 'commentCount' | 'name'; + }) => { + const searchParams = new URLSearchParams(); + if (params?.search) searchParams.set('search', params.search); + if (params?.filter) searchParams.set('filter', params.filter); + if (params?.sort) searchParams.set('sort', params.sort); + + const queryString = searchParams.toString(); + const url = `/me/videos${queryString ? `?${queryString}` : ''}`; + + return apiClient.get>(url); + }, }; diff --git a/src/components/video/VideoListPage.tsx b/src/components/video/VideoListPage.tsx index 60ff02a3..feb195ab 100644 --- a/src/components/video/VideoListPage.tsx +++ b/src/components/video/VideoListPage.tsx @@ -1,35 +1,42 @@ import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import type { Presentation } from '@/types'; +import { useMyVideos } from '@/hooks/useVideos'; import type { FilterMode, SortMode, ViewMode } from '@/types/home'; +import type { VideoPresentation } from '@/types/video'; +import { CardView, ListView } from '../common'; import PresentationCard from '../presentation/PresentationCard'; import PresentationHeader from '../presentation/PresentationHeader'; import PresentationList from '../presentation/PresentationList'; import { RecordingEmptySection } from './RecordingEmptySection'; -interface MockVideo { - id: number; - title: string; - createdAt: string; - durationSeconds: number; - slideCount: number; - size: number; -} +const SKELETON_CARD_COUNT = 6; +const SKELETON_LIST_COUNT = 4; export default function VideoListPage() { const navigate = useNavigate(); const location = useLocation(); const [showSuccessToast, setShowSuccessToast] = useState(false); - const [projects, setProjects] = useState([]); const [query, setQuery] = useState(''); + const [appliedQuery, setAppliedQuery] = useState(''); const [sort, setSort] = useState('recent'); const [filter, setFilter] = useState('all'); const [viewMode, setViewMode] = useState('card'); - const hasProjects = projects.length > 0; + const { data, isLoading } = useMyVideos({ + search: appliedQuery, + filter, + sort, + }); + + const videos: VideoPresentation[] = data?.videos || []; + const totalCount = data?.total || 0; + + const isDebouncing = query.trim() !== appliedQuery.trim(); + const hasAppliedQuery = appliedQuery.trim().length > 0; + const hasResults = videos.length > 0; useEffect(() => { if (location.state?.uploadSuccess) { @@ -42,35 +49,12 @@ export default function VideoListPage() { }, [location, navigate]); useEffect(() => { - const loadMockVideos = () => { - try { - const storedData = localStorage.getItem('mockVideos'); - if (!storedData) { - setProjects([]); - return; - } - - const mockVideos: MockVideo[] = JSON.parse(storedData); - - const formattedProjects: Presentation[] = mockVideos.map((video) => ({ - projectId: String(video.id), - title: video.title, - thumbnailUrl: undefined, - slideCount: video.slideCount, - feedbackCount: 0, - durationSeconds: video.durationSeconds, - createdAt: video.createdAt, - updatedAt: video.createdAt, - })); - - setProjects(formattedProjects); - } catch { - setProjects([]); - } - }; - - loadMockVideos(); - }, []); + const timer = setTimeout(() => { + setAppliedQuery(query); + }, 300); + + return () => clearTimeout(timer); + }, [query]); const handleStartRecording = () => { navigate('/recording'); @@ -97,7 +81,7 @@ export default function VideoListPage() {

녹화된 영상

발표 연습 영상을 선택해서 확인하세요

- {hasProjects && ( + {!isLoading && totalCount > 0 && ( + + + ); + } + + if (error) { + return ( +
+
+

데이터를 불러올 수 없습니다

+

잠시 후 다시 시도해주세요.

+ +
+
+ ); + } + return ( -
+
+ {/* 업로드 성공 토스트 */} {showSuccessToast && ( -
- +
+ - 영상이 성공적으로 저장되었습니다! + 영상이 성공적으로 저장되었습니다!
)} -
-
-
-

녹화된 영상

-

발표 연습 영상을 선택해서 확인하세요

+ {/* 메인 컨텐츠 */} + {!isLoading && totalCount === 0 && !hasAppliedQuery ? ( +
+ +
+ ) : ( +
+ {/* 헤더 */} +
+

녹화된 영상

+

발표 연습 영상을 선택해서 확인하세요

- {!isLoading && totalCount > 0 && ( + + {/* 영상 녹화하기 버튼 */} +
- )} -
- - {!isLoading && totalCount === 0 && !hasAppliedQuery ? ( -
-
- ) : ( -
+ {/* 검색/필터 헤더 */} +
+
+ {/* 콘텐츠 영역 */} +
{isLoading || isDebouncing ? ( viewMode === 'card' ? ( -
+
{Array.from({ length: SKELETON_CARD_COUNT }).map((_, index) => ( ))}
) : ( -
+
{Array.from({ length: SKELETON_LIST_COUNT }).map((_, index) => ( ))} @@ -136,24 +182,24 @@ export default function VideoListPage() { item.projectId} - className="mt-6 grid grid-cols-2 gap-4 lg:grid-cols-3" - renderCard={(item) => } + className="grid grid-cols-2 gap-4 md:grid-cols-2 lg:grid-cols-3" + renderCard={(item) => } empty={null} /> ) : ( item.projectId} - className="mt-6 flex flex-col gap-3" - renderInfo={(item) => } + className="flex flex-col gap-3" + renderInfo={(item) => } empty={null} /> )}
)}
- )} -
+
+ )}
); } diff --git a/src/pages/VideoRecordPage.tsx b/src/pages/VideoRecordPage.tsx index 1033a8c0..8b1afc9e 100644 --- a/src/pages/VideoRecordPage.tsx +++ b/src/pages/VideoRecordPage.tsx @@ -84,7 +84,7 @@ export default function VideoRecordPage() { console.warn('localStorage 저장 실패 (무시):', err); } - navigate(`/${projectId}/video`, { + navigate(`/${projectId}/videos`, { state: { uploadSuccess: true, videoId }, replace: true, }); diff --git a/src/pages/index.ts b/src/pages/index.ts index 9f3a45ab..f9e021ae 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -7,3 +7,4 @@ export { default as VideoPage } from './VideoPage'; export { default as VideoRecordPage } from './VideoRecordPage'; export { default as FdSlidePage } from './FeedbackSlidePage'; export { default as FdVideoPage } from './FeedbackVideoPage'; +export { default as VideoListPage } from './VideoListPage'; diff --git a/src/router/Router.tsx b/src/router/Router.tsx index 64e60754..17be68ac 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -21,6 +21,7 @@ import { VideoPage, VideoRecordPage, } from '@/pages'; +import VideoListPage from '@/pages/VideoListPage'; export const router = createBrowserRouter([ { @@ -58,8 +59,8 @@ export const router = createBrowserRouter([ children: [ { index: true, element: }, { path: 'slide', element: }, - { path: 'video', element: }, { path: 'insight', element: }, + { path: 'videos', element: }, ], }, { diff --git a/src/types/navigation.ts b/src/types/navigation.ts index d8815efe..c497f20a 100644 --- a/src/types/navigation.ts +++ b/src/types/navigation.ts @@ -1,4 +1,4 @@ -export type TabKey = 'slide' | 'video' | 'insight'; +export type TabKey = 'slide' | 'videos' | 'insight'; export interface TabItem { key: TabKey; diff --git a/src/types/video.ts b/src/types/video.ts index adb8ac32..c3571433 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -76,6 +76,7 @@ export interface VideoFeedback { * Mock 비디오 데이터 타입 (localStorage용) */ export interface MockVideo { + projectId: string | readonly string[] | undefined; id: number; title: string; createdAt: string; From 7e3a56efbeddf1f9eff31d28171e9d720cfa8beb Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Sun, 8 Feb 2026 14:28:47 +0900 Subject: [PATCH 27/81] =?UTF-8?q?fix:=20active=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=EB=8F=84=20=EC=84=9C=EB=B2=84=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=9E=EC=B6=A4=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/video.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/video.ts b/src/constants/video.ts index bcc78367..bb7bb7b5 100644 --- a/src/constants/video.ts +++ b/src/constants/video.ts @@ -7,4 +7,4 @@ * 리액션/댓글의 타임스탬프 그룹화 윈도우 (초) * - 현재 시간 ± FEEDBACK_WINDOW 범위의 피드백을 하나의 그룹으로 처리 */ -export const FEEDBACK_WINDOW = 5; +export const FEEDBACK_WINDOW = 2; From 5e35b63df1b5c80e9a08d8d5d574f621ae6ed63e Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Sun, 8 Feb 2026 14:30:27 +0900 Subject: [PATCH 28/81] =?UTF-8?q?feat:=20=EB=B9=84=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=EB=A6=AC=EC=95=A1=EC=85=98=20=ED=83=80=EC=9E=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=EA=B3=BC=20=EC=97=B0=EA=B2=B0=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feedback/video/VideoPlaybackBar.tsx | 44 +++++++++- src/hooks/useVideoReactions.ts | 87 ++++++++++++------- src/mocks/handlers.ts | 21 ++++- 3 files changed, 115 insertions(+), 37 deletions(-) diff --git a/src/components/feedback/video/VideoPlaybackBar.tsx b/src/components/feedback/video/VideoPlaybackBar.tsx index 4d36ec84..24120a25 100644 --- a/src/components/feedback/video/VideoPlaybackBar.tsx +++ b/src/components/feedback/video/VideoPlaybackBar.tsx @@ -13,10 +13,46 @@ import playIcon from '@/assets/playbackBar-icons/play-icon.webp'; import fullscreenIcon from '@/assets/playbackBar-icons/sizeupdown-icon.webp'; import ProgressBar from '@/components/feedback/ProgressBar'; import VolumeControl from '@/components/feedback/video/VolumeControl'; +import { useVideoReactionTimeline } from '@/hooks/queries/useVideoReactionQueries'; import { useVideoFeedbackStore } from '@/stores/videoFeedbackStore'; +import type { ReactionType } from '@/types/script'; import type { SlideListItem } from '@/types/slide'; +import type { SegmentHighlight } from '@/types/video'; import { computeSegmentHighlightsFromFeedbacks } from '@/utils/video'; +const DEFAULT_TIMELINE_INTERVAL_MS = 5000; +const MAX_HIGHLIGHTS = 10; + +const buildHighlightsFromTimeline = ( + timeline: { + intervalMs: number; + markers: Array<{ timestampMs: number; emojiType: ReactionType; count: number }>; + }, + duration: number, + topN: number, +): SegmentHighlight[] => { + const intervalSec = timeline.intervalMs / 1000; + const highlights = timeline.markers + .map((marker) => { + const startTime = marker.timestampMs / 1000; + const endTime = Math.min(startTime + intervalSec, duration); + return { + startTime, + endTime, + topReactionType: marker.emojiType, + count: marker.count, + totalCount: marker.count, + } as SegmentHighlight; + }) + .filter((item) => item.totalCount > 0); + + return highlights + .slice() + .sort((a, b) => b.totalCount - a.totalCount) + .slice(0, topN) + .sort((a, b) => a.startTime - b.startTime); +}; + interface VideoPlaybackBarProps { videoElement: HTMLVideoElement | null; duration: number; @@ -40,6 +76,7 @@ export default function VideoPlaybackBar({ const currentTime = useVideoFeedbackStore((s) => s.currentTime); const updateCurrentTime = useVideoFeedbackStore((s) => s.updateCurrentTime); const video = useVideoFeedbackStore((s) => s.video); + const { data: timeline } = useVideoReactionTimeline(video?.videoId, DEFAULT_TIMELINE_INTERVAL_MS); const [isPlaying, setIsPlaying] = useState(false); const [volume, setVolume] = useState(1); @@ -47,8 +84,11 @@ export default function VideoPlaybackBar({ // 5초 버킷별 세그먼트 하이라이트 계산 (최다 리액션 기반) const segmentHighlights = useMemo(() => { if (!video) return []; - return computeSegmentHighlightsFromFeedbacks(video.feedbacks, video.duration); - }, [video]); + if (timeline) { + return buildHighlightsFromTimeline(timeline, video.duration, MAX_HIGHLIGHTS); + } + return computeSegmentHighlightsFromFeedbacks(video.feedbacks, video.duration, MAX_HIGHLIGHTS); + }, [video, timeline]); // 비디오 play/pause 이벤트 구독 useEffect(() => { diff --git a/src/hooks/useVideoReactions.ts b/src/hooks/useVideoReactions.ts index ffe0081a..2626416f 100644 --- a/src/hooks/useVideoReactions.ts +++ b/src/hooks/useVideoReactions.ts @@ -7,7 +7,7 @@ import type { Reaction, ReactionType } from '@/types/script'; import { showToast } from '@/utils/toast'; import { getOverlappingFeedbacks } from '@/utils/video'; -import { useToggleVideoReaction } from './queries/useVideoReactionQueries'; +import { useToggleVideoReaction, useVideoReactionWindow } from './queries/useVideoReactionQueries'; /** * 영상 리액션 관리 훅 @@ -24,50 +24,72 @@ export function useVideoReactions() { const toggleReactionStore = useVideoFeedbackStore((s) => s.toggleReaction); const { mutate: toggleReactionApi } = useToggleVideoReaction(); + const timestampMs = Math.round(currentTime * 1000); + const requestTimestampMs = Math.round(timestampMs / 1000) * 1000; + const windowMs = 2000; + const { data: windowSummary } = useVideoReactionWindow( + video?.videoId, + requestTimestampMs, + windowMs, + ); const reactions: Reaction[] = useMemo(() => { if (!video) { return REACTION_TYPES.map((type) => ({ type, count: 0, active: false })); } - // 현재 시간과 겹치는 모든 feedbacks 찾기 (±FEEDBACK_WINDOW 범위) + // active는 로컬 스토어 기준 (exclusive 규칙 유지) const overlappingFeedbacks = getOverlappingFeedbacks( video.feedbacks, currentTime, FEEDBACK_WINDOW, ); + const closestFeedback = + overlappingFeedbacks.length > 0 + ? overlappingFeedbacks.reduce((closest, current) => { + return Math.abs(current.timestampMs - currentTime * 1000) < + Math.abs(closest.timestampMs - currentTime * 1000) + ? current + : closest; + }) + : null; - // 겹치는 feedbacks이 없으면 기본값 - if (overlappingFeedbacks.length === 0) { - return REACTION_TYPES.map((type) => ({ type, count: 0, active: false })); - } - - // 가장 가까운 버킷 (active 상태 기준) - const closestFeedback = overlappingFeedbacks.reduce((closest, current) => { - return Math.abs(current.timestampMs - currentTime * 1000) < - Math.abs(closest.timestampMs - currentTime * 1000) - ? current - : closest; - }); + const activeMap: Record = REACTION_TYPES.reduce( + (acc, type) => { + acc[type] = closestFeedback?.reactions.find((r) => r.type === type)?.active ?? false; + return acc; + }, + {} as Record, + ); - // 모든 겹치는 feedbacks의 reactions을 합산 - return REACTION_TYPES.map((type) => { - // count: 모든 겹치는 feedbacks의 count 합산 - const totalCount = overlappingFeedbacks.reduce((sum, feedback) => { - const reaction = feedback.reactions.find((r) => r.type === type); - return sum + (reaction?.count || 0); - }, 0); + const countMap: Record = REACTION_TYPES.reduce( + (acc, type) => { + acc[type] = 0; + return acc; + }, + {} as Record, + ); - // active: 가장 가까운 버킷의 active 상태만 반영 - const closestReaction = closestFeedback.reactions.find((r) => r.type === type); + if (windowSummary) { + windowSummary.forEach((item) => { + countMap[item.emojiType] = item.count; + }); + } else if (overlappingFeedbacks.length > 0) { + // fallback: 로컬 store 기반 합산 + REACTION_TYPES.forEach((type) => { + countMap[type] = overlappingFeedbacks.reduce((sum, feedback) => { + const reaction = feedback.reactions.find((r) => r.type === type); + return sum + (reaction?.count || 0); + }, 0); + }); + } - return { - type, - count: totalCount, - active: closestReaction?.active || false, - }; - }); - }, [video, currentTime]); + return REACTION_TYPES.map((type) => ({ + type, + count: countMap[type], + active: activeMap[type], + })); + }, [video, currentTime, windowSummary]); const toggleReaction = (type: ReactionType) => { if (!video) return; @@ -77,7 +99,10 @@ export function useVideoReactions() { // 2. API 비동기 호출 toggleReactionApi( - { videoId: video.videoId, data: { emojiType: type, timestampMs: Math.round(currentTime) } }, + { + videoId: video.videoId, + data: { emojiType: type, timestampMs }, + }, { onError: () => { // 3. 실패 시 rollback diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index d7388de8..592c55ea 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -764,7 +764,14 @@ const reactionHandlers = [ await delay(100); const { slideId } = params as { slideId: string }; const reactions = slideReactions.get(slideId) ?? {}; - return ok({ slideId, reactions }); + const filled = { + fire: reactions.fire ?? 0, + good: reactions.good ?? 0, + bad: reactions.bad ?? 0, + sleepy: reactions.sleepy ?? 0, + confused: reactions.confused ?? 0, + }; + return ok({ slideId, reactions: filled }); }), // 프로젝트 슬라이드 리액션 총합 @@ -772,7 +779,13 @@ const reactionHandlers = [ await delay(100); const { projectId } = params as { projectId: string }; const projectSlides = slides.filter((s) => s.projectId === projectId); - const total: Record = {}; + const total: Record = { + fire: 0, + good: 0, + bad: 0, + sleepy: 0, + confused: 0, + }; let totalCount = 0; projectSlides.forEach((s) => { const r = slideReactions.get(s.slideId) ?? {}; @@ -788,8 +801,8 @@ const reactionHandlers = [ http.post(`${BASE_URL}/videos/:videoId/reactions`, async ({ params, request }) => { await delay(100); const { videoId } = params as { videoId: string }; - const body = (await request.json()) as { emojiType: string; timestampMs: number }; - return ok({ reactionId: nextId(), videoId, active: true, ...body }); + await request.json(); + return ok({ reactionId: nextId(), videoId, active: true }); }), // 영상 리액션 타임라인 From e5d0f58848a2e07378c35a4f1d4f64abc84a5e75 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Sun, 8 Feb 2026 14:36:51 +0900 Subject: [PATCH 29/81] =?UTF-8?q?fix:=20=ED=82=A4=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=A0=20=ED=95=B4=EA=B2=B0=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feedback/ProgressBar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/feedback/ProgressBar.tsx b/src/components/feedback/ProgressBar.tsx index d09bf12d..ad957327 100644 --- a/src/components/feedback/ProgressBar.tsx +++ b/src/components/feedback/ProgressBar.tsx @@ -156,11 +156,11 @@ export default function ProgressBar({ className="group relative h-1 w-full cursor-pointer rounded-full bg-[rgba(26,26,26,0.66)] transition-all duration-150 hover:h-1.5 hover:ring-2 hover:ring-[#4F5BFF]/30 select-none before:content-[''] before:absolute before:-inset-y-3 before:inset-x-0" > {/* 프로그레스바 위 흰색 마커 */} - {segmentHighlights?.map((segment) => { + {segmentHighlights?.map((segment, index) => { const percent = max > 0 ? (segment.startTime / max) * 100 : 0; return (
@@ -168,11 +168,11 @@ export default function ProgressBar({ })} {/* 세그먼트 하이라이트 (5초 버킷별 대표 리액션) */} - {segmentHighlights?.map((segment) => { + {segmentHighlights?.map((segment, index) => { const percent = max > 0 ? (segment.startTime / max) * 100 : 0; return (
Date: Sun, 8 Feb 2026 16:56:52 +0900 Subject: [PATCH 30/81] =?UTF-8?q?fix:=20videoId=20=ED=83=80=EC=9E=85=20str?= =?UTF-8?q?ing=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/video.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/video.ts b/src/types/video.ts index 1aadd435..7fc2aeec 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -58,7 +58,7 @@ export interface VideoTimestampFeedback { * 전체 영상의 모든 타임스탬프 피드백을 관리합니다. */ export interface VideoFeedback { - videoId: number; // 서버 API와 일관성 유지 + videoId: string; // 서버 API와 일관성 유지 videoUrl: string; title: string; duration: number; // 초 단위 From 773afdba9ff42acffadd0ede8cef9a860f2b325b Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Sun, 8 Feb 2026 16:57:11 +0900 Subject: [PATCH 31/81] =?UTF-8?q?fix:=20id=20=E2=86=92=20commentId?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useVideoComments.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/useVideoComments.ts b/src/hooks/useVideoComments.ts index 853edff9..327a518d 100644 --- a/src/hooks/useVideoComments.ts +++ b/src/hooks/useVideoComments.ts @@ -67,7 +67,7 @@ export function useVideoComments() { // 서버 ID 저장 (Model에서 serverId 추출) if (model && tempComment) { - updateCommentServerId(tempComment.id, model.serverId); + updateCommentServerId(tempComment.commentId, model.serverId); } } catch { showToast.error('댓글 등록에 실패했습니다.', '잠시 후 다시 시도해주세요.'); @@ -83,7 +83,7 @@ export function useVideoComments() { try { // parentId로 부모 댓글 찾기 (serverId 필요) const allComments = video?.feedbacks.flatMap((f) => f.comments) || []; - const parentComment = allComments.find((c) => c.id === parentId); + const parentComment = allComments.find((c) => c.commentId === parentId); if (!parentComment || !parentComment.serverId) { showToast.error('답글 등록에 실패했습니다.', '부모 댓글을 찾을 수 없습니다.'); @@ -100,7 +100,7 @@ export function useVideoComments() { // 서버 ID 저장 (Model에서 serverId 추출) if (model && tempReply) { - updateCommentServerId(tempReply.id, model.serverId); + updateCommentServerId(tempReply.commentId, model.serverId); } } catch (_error) { showToast.error('답글 등록에 실패했습니다.', '잠시 후 다시 시도해주세요.'); @@ -117,7 +117,7 @@ export function useVideoComments() { // 플랫 구조: 모든 댓글(답글 포함)이 같은 배열에 있음 const allComments = video.feedbacks.flatMap((f) => f.comments); - const targetComment = allComments.find((c) => c.id === commentId); + const targetComment = allComments.find((c) => c.commentId === commentId); if (!targetComment) { showToast.error('댓글을 찾을 수 없습니다.'); @@ -154,7 +154,7 @@ export function useVideoComments() { if (!video) return; const allComments = video.feedbacks.flatMap((f) => f.comments); - const targetComment = allComments.find((c) => c.id === commentId); + const targetComment = allComments.find((c) => c.commentId === commentId); if (!targetComment) { showToast.error('댓글을 찾을 수 없습니다.'); From 23e49fd92c837f634a71c5da62e04e001e2c842e Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Sun, 8 Feb 2026 17:02:26 +0900 Subject: [PATCH 32/81] =?UTF-8?q?fix:=20videoId=20=ED=83=80=EC=9E=85=20str?= =?UTF-8?q?ing=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/dto/video.dto.ts | 2 +- src/api/endpoints/videos.ts | 12 ++++++------ src/hooks/useFeedbackWebSocket.ts | 2 +- src/hooks/useVideoUpload.ts | 2 +- src/mocks/handlers.ts | 10 ++++------ src/mocks/videos.ts | 2 +- src/pages/feedback/useFeedbackVideo.ts | 2 +- src/types/websocket.ts | 6 +++--- 8 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/api/dto/video.dto.ts b/src/api/dto/video.dto.ts index 7262ab3d..df2acde9 100644 --- a/src/api/dto/video.dto.ts +++ b/src/api/dto/video.dto.ts @@ -10,7 +10,7 @@ export interface StartVideoRequestDto { * 영상 녹화 시작 응답 DTO (success 데이터) */ export interface StartVideoResponseDto { - videoId: number; + videoId: string; } /** diff --git a/src/api/endpoints/videos.ts b/src/api/endpoints/videos.ts index e4ff4ae2..54d0cce5 100644 --- a/src/api/endpoints/videos.ts +++ b/src/api/endpoints/videos.ts @@ -34,7 +34,7 @@ export const videosApi = { apiClient.post>('/videos/start', data), // POST /videos/{videoId}/chunks/{chunkIndex} - 청크 업로드 - uploadChunk: (videoId: number, chunkIndex: number, file: Blob) => { + uploadChunk: (videoId: string, chunkIndex: number, file: Blob) => { const formData = new FormData(); formData.append('file', file); @@ -50,21 +50,21 @@ export const videosApi = { }, // POST /videos/{videoId}/finish - 녹화 종료 및 영상 처리 시작 - finishVideo: (videoId: number, data: FinishVideoRequestDto) => + finishVideo: (videoId: string, data: FinishVideoRequestDto) => apiClient.post>(`/videos/${videoId}/finish`, data), // TODO: 연결? // POST /videos/{videoId}/comments - 영상 타임스탬프 댓글 생성 - createVideoComment: (videoId: number, data: CreateVideoCommentRequestDto) => + createVideoComment: (videoId: string, data: CreateVideoCommentRequestDto) => apiClient.post< ApiResponse<{ commentId: string; content: string; timestampMs: number; createdAt: string }> >(`/videos/${videoId}/comments`, data), // GET /videos/{videoId} - 영상 상세 조회 - getVideoDetail: (videoId: number) => apiClient.get(`/videos/${videoId}`), + getVideoDetail: (videoId: string) => apiClient.get(`/videos/${videoId}`), // GET /videos/{videoId}/slides - 슬라이드 타임라인 조회 - getVideoSlides: (videoId: number) => apiClient.get(`/videos/${videoId}/slides`), + getVideoSlides: (videoId: string) => apiClient.get(`/videos/${videoId}/slides`), }; // ============================================================================ @@ -80,7 +80,7 @@ export const videosApi = { * @returns Model - serverId와 content */ export async function createVideoComment( - videoId: number, + videoId: string, data: { content: string; timestampMs?: number }, ): Promise<{ serverId: string; content: string }> { const response = await apiClient.post>( diff --git a/src/hooks/useFeedbackWebSocket.ts b/src/hooks/useFeedbackWebSocket.ts index 537fb908..f0e60118 100644 --- a/src/hooks/useFeedbackWebSocket.ts +++ b/src/hooks/useFeedbackWebSocket.ts @@ -43,7 +43,7 @@ export function useFeedbackWebSocket({ projectId, enabled = true }: UseFeedbackW // WebSocket 페이로드에서 직접 Store 업데이트 const currentVideo = useVideoFeedbackStore.getState().video; - if (currentVideo && Number(data.videoId) === currentVideo.videoId) { + if (currentVideo && data.videoId === currentVideo.videoId) { // console.log('🔄 [Feedback WebSocket] Adding comment to store from WebSocket data...'); // WebSocket 데이터에서 받은 정보로 댓글 추가 diff --git a/src/hooks/useVideoUpload.ts b/src/hooks/useVideoUpload.ts index 99e674a8..359dca3c 100644 --- a/src/hooks/useVideoUpload.ts +++ b/src/hooks/useVideoUpload.ts @@ -41,7 +41,7 @@ export const useVideoUpload = () => { projectId: number, title: string, slideLogs: SlideLog[], - ): Promise => { + ): Promise => { setIsUploading(true); setError(null); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index e7e84e0a..592c55ea 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -814,7 +814,7 @@ const reactionHandlers = [ // MOCK_VIDEO feedbacks에서 타임라인 마커 생성 const markers: { timestampMs: number; emojiType: string; count: number }[] = []; - if (Number(videoId) === MOCK_VIDEO.videoId) { + if (videoId === MOCK_VIDEO.videoId) { MOCK_VIDEO.feedbacks.forEach((fb) => { const bucket = Math.floor(fb.timestampMs / intervalMs) * intervalMs; fb.reactions.forEach((r) => { @@ -834,7 +834,7 @@ const reactionHandlers = [ const windowMs = Number(url.searchParams.get('windowMs') ?? '2000'); const result: { emojiType: string; count: number }[] = []; - if (Number(videoId) === MOCK_VIDEO.videoId) { + if (videoId === MOCK_VIDEO.videoId) { const totals: Record = {}; MOCK_VIDEO.feedbacks.forEach((fb) => { if (fb.timestampMs >= timestampMs - windowMs && fb.timestampMs <= timestampMs + windowMs) { @@ -906,8 +906,7 @@ const videoHandlers = [ http.get(`${BASE_URL}/videos/:videoId`, async ({ params }) => { await delay(200); const { videoId } = params as { videoId: string }; - if (Number(videoId) !== MOCK_VIDEO.videoId) - return fail(404, 'V001', '영상을 찾을 수 없습니다.'); + if (videoId !== MOCK_VIDEO.videoId) return fail(404, 'V001', '영상을 찾을 수 없습니다.'); // MOCK_VIDEO feedbacks → Server timeline 포맷으로 변환 const timelineReactions: { timestampMs: number; emojiType: string; count: number }[] = []; @@ -962,8 +961,7 @@ const videoHandlers = [ http.get(`${BASE_URL}/videos/:videoId/slides`, async ({ params }) => { await delay(100); const { videoId } = params as { videoId: string }; - if (Number(videoId) !== MOCK_VIDEO.videoId) - return fail(404, 'V001', '영상을 찾을 수 없습니다.'); + if (videoId !== MOCK_VIDEO.videoId) return fail(404, 'V001', '영상을 찾을 수 없습니다.'); // p1 프로젝트의 처음 5개 슬라이드로 타임라인 생성 const p1Slides = slides .filter((s) => s.projectId === 'p1') diff --git a/src/mocks/videos.ts b/src/mocks/videos.ts index 01dce37b..6194899c 100644 --- a/src/mocks/videos.ts +++ b/src/mocks/videos.ts @@ -16,7 +16,7 @@ import { timeAgo } from './utils'; * - 상위 10개 세그먼트가 재생바에 하이라이트로 표시됨 */ export const MOCK_VIDEO: VideoFeedback = { - videoId: 1, + videoId: '1', videoUrl: '/p1.webm', title: '테스트 영상', duration: 596, diff --git a/src/pages/feedback/useFeedbackVideo.ts b/src/pages/feedback/useFeedbackVideo.ts index 8f1d130b..672645ae 100644 --- a/src/pages/feedback/useFeedbackVideo.ts +++ b/src/pages/feedback/useFeedbackVideo.ts @@ -16,7 +16,7 @@ import { formatVideoTimestamp } from '@/utils/format'; * 테스트용 하드코딩 비디오 ID * DB에서 ready 상태인 9초 영상 (id=26, project_id=136) */ -const TEST_VIDEO_ID = 26; +const TEST_VIDEO_ID = '26'; export function useFeedbackVideo() { // projectId는 라우트에서 추출하지만 현재 테스트 모드에서는 사용하지 않음 diff --git a/src/types/websocket.ts b/src/types/websocket.ts index 9ee27aaf..5c00b5d1 100644 --- a/src/types/websocket.ts +++ b/src/types/websocket.ts @@ -35,7 +35,7 @@ export interface RoomsListResponse { */ export interface NewCommentPayload { commentId: string; - videoId: number; + videoId: string; userId: string; content: string; timestamp: number; // 비디오 타임스탬프 (초 단위) @@ -50,7 +50,7 @@ export interface CommentDeletedPayload { */ export interface NewReactionPayload { reactionId: string; - videoId: number; + videoId: string; userId: string; emoji: string; timestamp: number; // 백엔드에서 실제로는 timestampMs를 timestamp라는 이름으로 전송 (밀리초 단위) @@ -61,7 +61,7 @@ export interface ReactionRemovedPayload { } export interface ReactionCountUpdatedPayload { - videoId: number; // number로 변경 (API와 일관성) + videoId: string; counts: Record; } From 1d6b3ea6089fe22f7cef182669f2f5d8a10e128b Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Sun, 8 Feb 2026 19:04:15 +0900 Subject: [PATCH 33/81] =?UTF-8?q?design:=20width=20max=EA=B0=92=20?= =?UTF-8?q?=EC=A7=80=EC=A0=95=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/TitleEditorPopover.tsx | 10 ++++++---- src/components/slide/script/SlideTitle.tsx | 8 +++++--- src/components/video/RecordingSection.tsx | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/common/TitleEditorPopover.tsx b/src/components/common/TitleEditorPopover.tsx index 5213132c..4491cf74 100644 --- a/src/components/common/TitleEditorPopover.tsx +++ b/src/components/common/TitleEditorPopover.tsx @@ -22,6 +22,7 @@ interface TitleEditorPopoverProps { isCollapsed?: boolean; ariaLabel: string; isPending?: boolean; + titleClassName?: string; } export function TitleEditorPopover({ @@ -31,6 +32,7 @@ export function TitleEditorPopover({ isCollapsed = false, ariaLabel, isPending = false, + titleClassName = 'max-w-60 truncate', }: TitleEditorPopoverProps) { const [editTitle, setEditTitle] = useState(title); @@ -45,9 +47,9 @@ export function TitleEditorPopover({ } @@ -67,9 +69,9 @@ export function TitleEditorPopover({ + +
+ + ); +} From 0eda3fabf90ae6b1e1c9a2a51312b56a5f01dd9b Mon Sep 17 00:00:00 2001 From: PeraSite Date: Mon, 9 Feb 2026 16:48:21 +0900 Subject: [PATCH 56/81] =?UTF-8?q?feat:=20Auth=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=84=B9=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/dev-test/DevTestPage.tsx | 2 + .../dev-test/sections/AuthTokenSection.tsx | 176 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/pages/dev-test/sections/AuthTokenSection.tsx diff --git a/src/pages/dev-test/DevTestPage.tsx b/src/pages/dev-test/DevTestPage.tsx index 28f26c70..c7cf8657 100644 --- a/src/pages/dev-test/DevTestPage.tsx +++ b/src/pages/dev-test/DevTestPage.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; +import { AuthTokenSection } from './sections/AuthTokenSection'; import { CommonUITestSection } from './sections/CommonUITestSection'; import { DropdownTestSection } from './sections/DropdownTestSection'; import { ErrorTestSection } from './sections/ErrorTestSection'; @@ -23,6 +24,7 @@ export default function DevTestPage() {
+ diff --git a/src/pages/dev-test/sections/AuthTokenSection.tsx b/src/pages/dev-test/sections/AuthTokenSection.tsx new file mode 100644 index 00000000..b3bb1a96 --- /dev/null +++ b/src/pages/dev-test/sections/AuthTokenSection.tsx @@ -0,0 +1,176 @@ +import { useState } from 'react'; + +import { sessionApi } from '@/api/endpoints/session'; +import { TextField } from '@/components/common'; +import { useAuthStore } from '@/stores/authStore'; +import { showToast } from '@/utils/toast'; + +type JwtDecodeResult = { ok: true; payload: unknown } | { ok: false; reason: string }; + +const decodeJwtPayload = (token: string | null): JwtDecodeResult => { + if (!token) { + return { ok: false, reason: '토큰 없음' }; + } + + const parts = token.split('.'); + if (parts.length < 2) { + return { ok: false, reason: 'JWT 형식이 아닙니다.' }; + } + + try { + const base64Url = parts[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '='); + const binary = window.atob(padded); + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); + const json = new TextDecoder().decode(bytes); + return { ok: true, payload: JSON.parse(json) }; + } catch { + return { ok: false, reason: 'payload 파싱 실패' }; + } +}; + +export function AuthTokenSection() { + const accessToken = useAuthStore((state) => state.accessToken); + const refreshToken = useAuthStore((state) => state.refreshToken); + const anonymous = useAuthStore((state) => state.anonymous); + const logout = useAuthStore((state) => state.logout); + + const [nextAccessToken, setNextAccessToken] = useState(''); + const [nextRefreshToken, setNextRefreshToken] = useState(''); + const [isCreatingAnonymousSession, setIsCreatingAnonymousSession] = useState(false); + const [sessionResult, setSessionResult] = useState(null); + + const accessPayload = decodeJwtPayload(accessToken); + const refreshPayload = decodeJwtPayload(refreshToken); + + const handleSaveTokens = () => { + const trimmedAccessToken = nextAccessToken.trim(); + const trimmedRefreshToken = nextRefreshToken.trim(); + + if (!trimmedAccessToken) { + showToast.warning('access token을 입력해주세요.'); + return; + } + + anonymous(trimmedAccessToken, trimmedRefreshToken || refreshToken || ''); + showToast.success('토큰을 저장했습니다.'); + }; + + const handleClearTokens = () => { + logout(); + setNextAccessToken(''); + setNextRefreshToken(''); + showToast.success('토큰을 삭제했습니다.'); + }; + + const handleCreateAnonymousSession = async () => { + setIsCreatingAnonymousSession(true); + + try { + const result = await sessionApi.createAnonymousSession(); + setSessionResult(JSON.stringify(result, null, 2)); + + if (result.resultType === 'SUCCESS') { + showToast.success('익명 세션 생성 성공'); + } else { + showToast.error('익명 세션 생성 실패', result.error.reason); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '익명 세션 생성 요청 실패'; + setSessionResult(errorMessage); + showToast.error('익명 세션 생성 요청 실패', errorMessage); + } finally { + setIsCreatingAnonymousSession(false); + } + }; + + return ( +
+

🔐 Auth Token Test

+ +
+

설명

+
    +
  • 현재 access token / refresh token 상태를 확인합니다.
  • +
  • access token만 입력해도 저장할 수 있습니다.
  • +
  • refresh token 미입력 시 기존 값을 유지합니다.
  • +
  • 익명 세션 생성 API를 직접 호출해서 응답을 확인할 수 있습니다.
  • +
+
+ +
+
+

현재 Access Token

+

{accessToken ?? '없음'}

+
+

Access Payload

+ {accessPayload.ok ? ( +
+                {JSON.stringify(accessPayload.payload, null, 2)}
+              
+ ) : ( +

{accessPayload.reason}

+ )} +
+
+
+

현재 Refresh Token

+

{refreshToken ?? '없음'}

+
+

Refresh Payload

+ {refreshPayload.ok ? ( +
+                {JSON.stringify(refreshPayload.payload, null, 2)}
+              
+ ) : ( +

{refreshPayload.reason}

+ )} +
+
+
+ +
+ setNextAccessToken(e.target.value)} + placeholder="새 access token 입력" + /> + setNextRefreshToken(e.target.value)} + placeholder="새 refresh token 입력 (선택)" + /> +
+ +
+ + + +
+ + {sessionResult && ( +
+

익명 세션 응답

+
{sessionResult}
+
+ )} +
+ ); +} From 08132b0cadb0b13a933179f93df080798e157b05 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 17:55:27 +0900 Subject: [PATCH 57/81] =?UTF-8?q?fix:=20gemini=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 - src/api/dto/video.dto.ts | 43 ++ src/api/endpoints/videos.ts | 7 +- .../presentation/PresentationCard.tsx | 8 +- .../presentation/PresentationList.tsx | 4 - src/components/video/VideoListPage.tsx | 145 ------ src/hooks/useFeedbackVideo.ts | 11 +- src/hooks/useMediaStream.ts | 3 +- src/hooks/useVideoUpload.ts | 1 + src/mocks/handlers.ts | 4 +- src/pages/VideoDetailPage.tsx | 421 ++++++++++++++++++ src/pages/VideoListPage.tsx | 25 +- src/pages/index.ts | 5 +- src/router/Router.tsx | 8 +- src/types/video.ts | 2 +- 15 files changed, 519 insertions(+), 170 deletions(-) delete mode 100644 src/components/video/VideoListPage.tsx create mode 100644 src/pages/VideoDetailPage.tsx diff --git a/.gitignore b/.gitignore index f277acf0..4363ab31 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,6 @@ bun.lock .env.development *.local !.env.local.example -.env.development -.env.production # Editor directories and files .vscode/* diff --git a/src/api/dto/video.dto.ts b/src/api/dto/video.dto.ts index f6e91f33..afb285d2 100644 --- a/src/api/dto/video.dto.ts +++ b/src/api/dto/video.dto.ts @@ -72,3 +72,46 @@ export interface GetProjectVideosResponseDto { videos: VideoDto[]; total: number; } +/** + * 영상 상세 조회 응답 DTO + */ +export interface GetVideoDetailResponseDto { + video: { + videoId: string; + title: string; + status: 'processing' | 'ready' | 'failed'; + durationSeconds: number; + width: number; + height: number; + fps: number; + hlsMasterUrl: string; + thumbnailUrl: string; + createdAt: string; + }; + timeline: { + reactions: Array<{ + timestampMs: number; + emojiType: string; + count: number; + }>; + comments: Array<{ + commentId: string; + timestampMs: number; + content: string; + createdAt: string; + user: { + userId: string; + name: string; + }; + replies?: Array<{ + replyId: string; + content: string; + createdAt: string; + user: { + userId: string; + name: string; + }; + }>; + }>; + }; +} diff --git a/src/api/endpoints/videos.ts b/src/api/endpoints/videos.ts index 0be9272c..64882c8f 100644 --- a/src/api/endpoints/videos.ts +++ b/src/api/endpoints/videos.ts @@ -9,7 +9,7 @@ import type { } from '@/api/dto'; import type { ApiResponse } from '@/types/api'; -import type { GetProjectVideosResponseDto } from '../dto/video.dto'; +import type { GetProjectVideosResponseDto, GetVideoDetailResponseDto } from '../dto/video.dto'; /** * DTO → Model 변환: CommentResponseDto를 앱 내부용 Model로 변환 @@ -35,7 +35,7 @@ export const videosApi = { apiClient.post>('/videos/start', data), // POST /videos/{videoId}/chunks/{chunkIndex} - 청크 업로드 - uploadChunk: (videoId: number, chunkIndex: number, file: Blob) => { + uploadChunk: (videoId: string, chunkIndex: number, file: Blob) => { const formData = new FormData(); formData.append('file', file); @@ -55,7 +55,8 @@ export const videosApi = { apiClient.post>(`/videos/${videoId}/finish`, data), // GET /videos/{videoId} - 영상 상세 조회 - getVideoDetail: (videoId: string) => apiClient.get(`/videos/${videoId}`), + getVideoDetail: (videoId: string) => + apiClient.get>(`/videos/${videoId}`), // GET /videos/{videoId}/slides - 슬라이드 타임라인 조회 getVideoSlides: (videoId: string) => apiClient.get(`/videos/${videoId}/slides`), diff --git a/src/components/presentation/PresentationCard.tsx b/src/components/presentation/PresentationCard.tsx index 8658bef3..15598758 100644 --- a/src/components/presentation/PresentationCard.tsx +++ b/src/components/presentation/PresentationCard.tsx @@ -103,10 +103,10 @@ function PresentationCard(props: Props) { confirmRename, } = useRename({ projectId, initialTitle: title }); - const isVideo = 'reactionCount' in rest && 'viewCount' in rest; - const totalCommentCount = isVideo ? (rest as VideoPresentation).commentCount : feedbackCount; - const reactionCount = isVideo ? (rest as VideoPresentation).reactionCount : 0; - const viewCount = isVideo ? (rest as VideoPresentation).viewCount : 0; + const isVideo = 'reactionCount' in props && 'viewCount' in props; + const totalCommentCount = isVideo ? (props as VideoPresentation).commentCount : feedbackCount; + const reactionCount = isVideo ? (props as VideoPresentation).reactionCount : 0; + const viewCount = isVideo ? (props as VideoPresentation).viewCount : 0; const isRenaming = isRenameModalOpen && isRenamePending; const handleCardClick = () => { diff --git a/src/components/presentation/PresentationList.tsx b/src/components/presentation/PresentationList.tsx index c5b9d29d..20d43ca7 100644 --- a/src/components/presentation/PresentationList.tsx +++ b/src/components/presentation/PresentationList.tsx @@ -24,10 +24,6 @@ type Props = (Presentation | VideoPresentation) & { mode?: 'slide' | 'videos'; }; -function isVideoPresentation(item: Presentation | VideoPresentation): item is VideoPresentation { - return 'reactionCount' in item && 'viewCount' in item; -} - function PresentationListSkeleton() { return (
diff --git a/src/components/video/VideoListPage.tsx b/src/components/video/VideoListPage.tsx deleted file mode 100644 index 902872e1..00000000 --- a/src/components/video/VideoListPage.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; - -import type { Presentation } from '@/types'; -import type { FilterMode, SortMode, ViewMode } from '@/types/home'; -import { showToast } from '@/utils/toast'; - -import PresentationCard from '../presentation/PresentationCard'; -import PresentationHeader from '../presentation/PresentationHeader'; -import PresentationList from '../presentation/PresentationList'; -import { RecordingEmptySection } from './RecordingEmptySection'; - -interface MockVideo { - id: number; - title: string; - createdAt: string; - durationSeconds: number; - slideCount: number; - size: number; -} - -export default function VideoListPage() { - const navigate = useNavigate(); - const location = useLocation(); - - const [projects, setProjects] = useState([]); - const [query, setQuery] = useState(''); - const [sort, setSort] = useState('recent'); - const [filter, setFilter] = useState('all'); - const [viewMode, setViewMode] = useState('card'); - - const hasProjects = projects.length > 0; - - useEffect(() => { - if (location.state?.uploadSuccess) { - showToast.success('영상이 성공적으로 저장되었습니다!'); - navigate(location.pathname, { replace: true, state: {} }); - } - }, [location, navigate]); - - useEffect(() => { - const loadMockVideos = () => { - try { - const storedData = localStorage.getItem('mockVideos'); - if (!storedData) { - setProjects([]); - return; - } - - const mockVideos: MockVideo[] = JSON.parse(storedData); - - const formattedProjects: Presentation[] = mockVideos.map((video) => ({ - projectId: String(video.id), - title: video.title, - thumbnailUrl: undefined, - slideCount: video.slideCount, - feedbackCount: 0, - durationSeconds: video.durationSeconds, - createdAt: video.createdAt, - updatedAt: video.createdAt, - })); - - setProjects(formattedProjects); - } catch { - setProjects([]); - } - }; - - loadMockVideos(); - }, []); - - const handleStartRecording = () => { - navigate('/recording'); - }; - - return ( -
-
-
-
-

녹화된 영상

-

발표 연습 영상을 선택해서 확인하세요

-
- {hasProjects && ( - - )} -
- - {!hasProjects ? ( - //
- // {Array.from({ length: 6 }).map((_, i) => - // viewMode === 'card' ? ( - // - // ) : ( - // - // ), - // )} - //
- // ) : ( -
- -
- ) : ( -
- - -
- {viewMode === 'card' ? ( -
- {projects.map((item) => ( - - ))} -
- ) : ( -
- {projects.map((item) => ( - - ))} -
- )} -
-
- )} -
-
- ); -} diff --git a/src/hooks/useFeedbackVideo.ts b/src/hooks/useFeedbackVideo.ts index 137bdc9e..a801076e 100644 --- a/src/hooks/useFeedbackVideo.ts +++ b/src/hooks/useFeedbackVideo.ts @@ -8,6 +8,7 @@ import { useParams } from 'react-router-dom'; import { videosApi } from '@/api/endpoints/videos'; import { useVideoComments } from '@/hooks/useVideoComments'; import { useVideoReactions } from '@/hooks/useVideoReactions'; +import { MOCK_VIDEO } from '@/mocks/videos'; import { useVideoFeedbackStore } from '@/stores/videoFeedbackStore'; import type { Comment } from '@/types/comment'; import { formatVideoTimestamp } from '@/utils/format'; @@ -41,8 +42,16 @@ export function useFeedbackVideo() { const projectSlides = useMemo(() => [], []); // TODO: 슬라이드 전환 시간 계산 - const slideChangeTimes = useMemo(() => [], []); + const slideChangeTimes = useMemo(() => { + if (projectSlides.length === 0) return []; + const videoDuration = MOCK_VIDEO.duration; // MOCK_VIDEOS → MOCK_VIDEO + const slideCount = projectSlides.length; + + return projectSlides.map( + (slide, i) => slide.startTime ?? Math.floor(i * (videoDuration / slideCount)), + ); + }, [projectSlides]); // 타임스탬프 프리픽스 (댓글 입력 시 자동 삽입) const timestampPrefix = useMemo(() => `${formatVideoTimestamp(currentTime)} `, [currentTime]); diff --git a/src/hooks/useMediaStream.ts b/src/hooks/useMediaStream.ts index 18809917..ee87a60a 100644 --- a/src/hooks/useMediaStream.ts +++ b/src/hooks/useMediaStream.ts @@ -133,8 +133,7 @@ export const useMediaStream = (videoDeviceId?: string, audioDeviceId?: string) = return () => { cleanupStream(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [videoDeviceId, audioDeviceId]); + }, [videoDeviceId, audioDeviceId, startStream, cleanupStream]); return { stream, diff --git a/src/hooks/useVideoUpload.ts b/src/hooks/useVideoUpload.ts index 740b5a89..0772b655 100644 --- a/src/hooks/useVideoUpload.ts +++ b/src/hooks/useVideoUpload.ts @@ -108,6 +108,7 @@ export const useVideoUpload = () => { const newVideo: MockVideo = { id: videoId, + projectId: projectId.toString(), title, createdAt: new Date().toISOString(), durationSeconds: Math.round(videoBlob.size / 1024 / 100), diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index bdc4307a..c9625131 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -8,11 +8,9 @@ import { HttpResponse, delay, http } from 'msw'; import type { VideoDto } from '@/api/dto/video.dto'; -import { createDefaultReactions } from '@/constants/reaction'; -import { FEEDBACK_WINDOW } from '@/constants/video'; import type { Presentation } from '@/types/presentation'; import type { SlideListItem } from '@/types/slide'; -import type { MockVideo, VideoFeedback, VideoTimestampFeedback } from '@/types/video'; +import type { MockVideo } from '@/types/video'; import { getMockProjectAnalyticsSummary, diff --git a/src/pages/VideoDetailPage.tsx b/src/pages/VideoDetailPage.tsx new file mode 100644 index 00000000..26de5076 --- /dev/null +++ b/src/pages/VideoDetailPage.tsx @@ -0,0 +1,421 @@ +// import { useCallback, useEffect, useRef, useState } from 'react'; +// import { useNavigate, useParams } from 'react-router-dom'; + +// import { videosApi, createVideoComment, createCommentReply, deleteVideoComment } from '@/api/endpoints/videos'; +// import { updateComment } from '@/api/endpoints/comments'; +// import type { GetVideoDetailResponseDto } from '@/api/dto/video.dto'; +// import { CommentInput } from '@/components/comment'; +// import CommentList from '@/components/comment/CommentList'; +// import FeedbackMobileLayout from '@/components/feedback/FeedbackMobileLayout'; +// import ScriptSection from '@/components/feedback/ScriptSection'; +// import SlideWebcamStage from '@/components/feedback/video/SlideWebcamStage'; +// import { useIsDesktop } from '@/hooks/useMediaQuery'; +// import { useAuthStore } from '@/stores/authStore'; + +// interface Comment { +// id: string; +// content: string; +// timestamp: number; +// createdAt: string; +// author: string; +// userId: string; +// replies: Array<{ +// id: string; +// content: string; +// createdAt: string; +// author: string; +// userId: string; +// }>; +// } + +// export default function VideoDetailPage() { +// const { projectId, videoId } = useParams<{ projectId: string; videoId: string }>(); +// const navigate = useNavigate(); +// const isDesktop = useIsDesktop(); +// const currentUser = useAuthStore((state) => state.user); + +// // 비디오 데이터 +// const [videoData, setVideoData] = useState(null); +// const [isLoading, setIsLoading] = useState(true); +// const [error, setError] = useState(null); + +// // 재생 상태 +// const [currentTime, setCurrentTime] = useState(0); + +// // 댓글 상태 +// const [comments, setComments] = useState([]); +// const [commentDraft, setCommentDraft] = useState(''); + +// // 슬라이드 데이터 +// const [projectSlides, setProjectSlides] = useState([]); +// const [slideChangeTimes, setSlideChangeTimes] = useState([]); + +// // 비디오 위치 계산을 위한 refs +// const desktopPlaceholderRef = useRef(null); +// const mobilePlaceholderRef = useRef(null); +// const [videoStyle, setVideoStyle] = useState({ +// position: 'fixed', +// opacity: 0, +// }); + +// // 비디오 데이터 로드 +// useEffect(() => { +// const loadVideoDetail = async () => { +// if (!videoId) return; + +// setIsLoading(true); +// setError(null); + +// try { +// const response = await videosApi.getVideoDetail(videoId); + +// if (response.data.resultType === 'FAILURE') { +// throw new Error(response.data.error?.reason || '영상을 불러올 수 없습니다'); +// } + +// const data = response.data.success!; +// setVideoData(data); + +// // 댓글 데이터 변환 +// const transformedComments: Comment[] = (data.timeline?.comments ?? []).map((comment) => ({ +// id: comment.commentId, +// content: comment.content, +// timestamp: comment.timestampMs / 1000, +// createdAt: comment.createdAt, +// author: comment.user.name, +// userId: comment.user.userId, +// replies: +// comment.replies?.map((reply) => ({ +// id: reply.replyId, +// content: reply.content, +// createdAt: reply.createdAt, +// author: reply.user.name, +// userId: reply.user.userId, +// })) ?? [], +// })); + +// setComments(transformedComments); + +// // TODO: 슬라이드 데이터 로드 +// // const slidesResponse = await videosApi.getVideoSlides(videoId); +// } catch (err) { +// const errorMessage = err instanceof Error ? err.message : '영상 로드 실패'; +// setError(errorMessage); +// } finally { +// setIsLoading(false); +// } +// }; + +// loadVideoDetail(); +// }, [videoId]); + +// // 비디오 위치 업데이트 +// useEffect(() => { +// const updateVideoPosition = () => { +// const primaryRef = isDesktop ? desktopPlaceholderRef : mobilePlaceholderRef; +// const fallbackRef = isDesktop ? mobilePlaceholderRef : desktopPlaceholderRef; + +// let rect = primaryRef.current?.getBoundingClientRect(); +// if (!rect || rect.width === 0 || rect.height === 0) { +// rect = fallbackRef.current?.getBoundingClientRect(); +// } + +// if (!rect || rect.width === 0 || rect.height === 0) return; + +// setVideoStyle({ +// position: 'fixed', +// top: rect.top, +// left: rect.left, +// width: rect.width, +// height: rect.height, +// zIndex: 20, +// opacity: 1, +// }); +// }; + +// const timers = [0, 50, 100, 200].map((delay) => setTimeout(updateVideoPosition, delay)); + +// const observer = new ResizeObserver(updateVideoPosition); +// if (desktopPlaceholderRef.current) observer.observe(desktopPlaceholderRef.current); +// if (mobilePlaceholderRef.current) observer.observe(mobilePlaceholderRef.current); + +// window.addEventListener('resize', updateVideoPosition); +// window.addEventListener('scroll', updateVideoPosition, true); + +// return () => { +// timers.forEach(clearTimeout); +// observer.disconnect(); +// window.removeEventListener('resize', updateVideoPosition); +// window.removeEventListener('scroll', updateVideoPosition, true); +// }; +// }, [isDesktop]); + +// const updateCurrentTime = useCallback((time: number) => { +// setCurrentTime(time); +// }, []); + +// const requestSeek = useCallback((time: number) => { +// setCurrentTime(time); +// }, []); + +// // 댓글 추가 - 실제 API 사용 +// const handleAddComment = useCallback(async () => { +// if (!commentDraft.trim() || !videoId) return; + +// try { +// // createVideoComment는 { serverId, content }만 반환 +// const result = await createVideoComment(Number(videoId), { +// content: commentDraft, +// timestampMs: Math.round(currentTime * 1000), +// }); + +// // 새 댓글을 로컬 상태에 추가 (임시 데이터) +// // 실제로는 서버에서 전체 댓글 정보를 다시 받아와야 함 +// const newComment: Comment = { +// id: result.serverId, +// content: result.content, +// timestamp: currentTime, +// createdAt: new Date().toISOString(), +// author: '나', // TODO: 현재 사용자 정보 가져오기 +// userId: 'me', +// replies: [], +// }; + +// setComments((prev) => [...prev, newComment]); +// setCommentDraft(''); +// } catch (err) { +// alert(err instanceof Error ? err.message : '댓글 추가에 실패했습니다.'); +// } +// }, [commentDraft, currentTime, videoId]); + +// const handleGoToTimeRef = useCallback( +// (time: number) => { +// requestSeek(time); +// }, +// [requestSeek] +// ); + +// // 답글 추가 - 실제 API 사용 +// const addReply = useCallback(async (commentId: string, content: string) => { +// try { +// const result = await createCommentReply(Number(commentId), { content }); + +// // 답글을 로컬 상태에 추가 +// setComments((prev) => +// prev.map((comment) => +// comment.id === commentId +// ? { +// ...comment, +// replies: [ +// ...comment.replies, +// { +// id: result.serverId, +// content: result.content, +// createdAt: new Date().toISOString(), +// author: currentUser?.name ?? '알 수 없음', +// userId: currentUser?.id ?? 'me', +// }, +// ], +// } +// : comment +// ) +// ); +// } catch (err) { +// alert(err instanceof Error ? err.message : '답글 추가에 실패했습니다.'); +// } +// }, []); + +// // 댓글 삭제 - 실제 API 사용 +// const handleDeleteComment = useCallback(async (commentId: string) => { +// if (!confirm('댓글을 삭제하시겠습니까?')) return; + +// try { +// await deleteVideoComment(Number(commentId)); +// setComments((prev) => prev.filter((comment) => comment.id !== commentId)); +// } catch (err) { +// console.error('댓글 삭제 실패:', err); +// alert(err instanceof Error ? err.message : '댓글 삭제에 실패했습니다.'); +// } +// }, []); + +// // 댓글 수정 - comments API 사용 +// const handleUpdateComment = useCallback(async (commentId: string, newContent: string) => { +// try { +// await updateComment(commentId, { content: newContent }); + +// setComments((prev) => +// prev.map((comment) => +// comment.id === commentId ? { ...comment, content: newContent } : comment +// ) +// ); +// } catch (err) { +// console.error('댓글 수정 실패:', err); +// alert(err instanceof Error ? err.message : '댓글 수정에 실패했습니다.'); +// } +// }, []); + +// const handleBack = () => { +// navigate(`/${projectId}/videos`); +// }; + +// const timestampPrefix = `[${Math.floor(currentTime / 60)}:${Math.floor(currentTime % 60).toString().padStart(2, '0')}] `; + +// // 로딩 중 +// if (isLoading) { +// return ( +//
+//
+//
+// ); +// } + +// // 에러 또는 데이터 없음 +// if (error || !videoData) { +// return ( +//
+//
+//

오류 발생

+//

{error || '영상을 찾을 수 없습니다'}

+// +//
+//
+// ); +// } + +// const webcamVideoUrl = videoData.video.hlsMasterUrl; + +// return ( +//
+// {/* 뒤로가기 헤더 (모바일) */} +//
+// +//

{videoData.video.title}

+//
+ +// {/* 데스크톱 뷰 */} +//
+// {/* 뒤로가기 버튼 */} +//
+// +//
+ +//
+// {/* 비디오 위치 placeholder */} +//
+//
+//
+// +//
+ +// +//
+ +// {/* 모바일 뷰 */} +//
+// } +// reactionSlot={null} +// scriptTabContent={ +// +// } +// commentTabContent={ +// <> +//
+// +//
+//
+// setCommentDraft('')} +// className="w-full" +// initialValueOnFocus={timestampPrefix} +// /> +//
+// +// } +// commentCount={comments.length} +// /> +//
+ +// {/* 단일 SlideWebcamStage - CSS로 위치 조정 */} +//
+// +//
+//
+// ); +// } diff --git a/src/pages/VideoListPage.tsx b/src/pages/VideoListPage.tsx index 5aac477d..388d3f4b 100644 --- a/src/pages/VideoListPage.tsx +++ b/src/pages/VideoListPage.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { CardView, ListView } from '@/components/common'; -import { ActionButton } from '@/components/common/ActionButton'; import PresentationCard from '@/components/presentation/PresentationCard'; import PresentationHeader from '@/components/presentation/PresentationHeader'; import PresentationList from '@/components/presentation/PresentationList'; @@ -12,6 +11,20 @@ import type { FilterMode, SortMode, ViewMode } from '@/types/home'; const SKELETON_CARD_COUNT = 6; const SKELETON_LIST_COUNT = 4; +interface Video { + id: number; + projectId: string; + title: string; + createdAt: string; + durationSeconds: number; + slideCount: number; + size: number; + videoData?: string; + videoUrl?: string; + thumbnailUrl?: string; + status: 'processing' | 'ready' | 'failed'; + durations?: { [key: number]: number }; +} export default function VideoListPage() { const navigate = useNavigate(); @@ -60,6 +73,16 @@ export default function VideoListPage() { const handleStartRecording = () => { navigate(`/${projectId}/video/record`); }; + const handleVideoClick = (video: Video) => { + if (video.status === 'ready') { + // 모달 대신 페이지로 이동 + navigate(`}/video/${video.id}`); + } else if (video.status === 'processing') { + alert('영상이 처리 중입니다. 잠시 후 다시 시도해주세요.'); + } else { + alert('영상 처리에 실패했습니다.'); + } + }; if (!projectId) { return ( diff --git a/src/pages/index.ts b/src/pages/index.ts index f9e021ae..b2c4944e 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -3,8 +3,9 @@ export { default as HomePage } from './HomePage'; export { default as OAuthCallbackPage } from './OAuthCallbackPage'; export { default as InsightPage } from './InsightPage'; export { default as SlidePage } from './SlidePage'; -export { default as VideoPage } from './VideoPage'; -export { default as VideoRecordPage } from './VideoRecordPage'; export { default as FdSlidePage } from './FeedbackSlidePage'; export { default as FdVideoPage } from './FeedbackVideoPage'; +export { default as VideoRecordPage } from './VideoRecordPage'; export { default as VideoListPage } from './VideoListPage'; +//export { default as VideoDetailPage } from './VideoDetailPage'; +export { default as VideoPage } from './VideoPage'; diff --git a/src/router/Router.tsx b/src/router/Router.tsx index e40976d7..cb99db16 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -19,10 +19,10 @@ import { InsightPage, OAuthCallbackPage, SlidePage, - VideoPage, + VideoListPage, VideoRecordPage, + // VideoDetailPage } from '@/pages'; -import VideoListPage from '@/pages/VideoListPage'; export const router = createBrowserRouter([ { @@ -71,6 +71,10 @@ export const router = createBrowserRouter([ ), children: [{ index: true, element: }], }, + // { + // path: '/video/:videoId', + // element: , + // }, { path: '/:projectId/video/record', element: , diff --git a/src/types/video.ts b/src/types/video.ts index eb13875d..35dc30cd 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -76,7 +76,7 @@ export interface VideoFeedback { * Mock 비디오 데이터 타입 (localStorage용) */ export interface MockVideo { - projectId: string | readonly string[] | undefined; + projectId: string; id: number; title: string; createdAt: string; From 27d1a1b58a58d10fcdd714237340ca9da69810c4 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 17:56:25 +0900 Subject: [PATCH 58/81] =?UTF-8?q?fix:=20=20=ED=83=80=EC=9E=85=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useVideoUpload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useVideoUpload.ts b/src/hooks/useVideoUpload.ts index 0772b655..1433f2a3 100644 --- a/src/hooks/useVideoUpload.ts +++ b/src/hooks/useVideoUpload.ts @@ -70,7 +70,7 @@ export const useVideoUpload = () => { }); for (let i = 0; i < chunks.length; i++) { - const uploadResponse = await videosApi.uploadChunk(videoId, i, chunks[i]); + const uploadResponse = await videosApi.uploadChunk(videoId.toString(), i, chunks[i]); if (uploadResponse.data.resultType === 'FAILURE') { throw new Error(uploadResponse.data.error?.reason || `청크 ${i} 업로드에 실패했습니다.`); From 3a24961343f829d0e920fb50c4bf62b6f7988d15 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 19:23:45 +0900 Subject: [PATCH 59/81] =?UTF-8?q?fix:=20dto=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/dto/index.ts | 15 +- src/api/dto/video.dto.ts | 282 ++++++------- src/api/endpoints/videos.ts | 77 ++-- src/hooks/useVideoUpload.ts | 9 +- src/mocks/handlers.ts | 413 +++++++++++-------- src/mocks/videos.ts | 793 +++++++++++++++++++----------------- 6 files changed, 856 insertions(+), 733 deletions(-) diff --git a/src/api/dto/index.ts b/src/api/dto/index.ts index d5cc323c..87d2f37b 100644 --- a/src/api/dto/index.ts +++ b/src/api/dto/index.ts @@ -39,14 +39,13 @@ export type { } from './presentations.dto'; // export type { UploadFileResponseDto } from './files.dto'; export type { - ChunkUploadResponseDto, - FinishVideoRequestDto, - FinishVideoResponseDto, - GetProjectVideosResponseDto, - GetVideoDetailResponseDto, - GetVideoSlidesResponseDto, - StartVideoRequestDto, - StartVideoResponseDto, + CreateStartVideoRequestDto, + CreateStartVideoResponseDto, + ReadProjectVideosResponseDto, + ReadVideoDetailResponseDto, + UpdateVideoRequestDto, + UpdateVideoResponseDto, + DeleteVideoResponseDto, VideoDetailDto, VideoListItemDto, VideoSlideTimelineItemDto, diff --git a/src/api/dto/video.dto.ts b/src/api/dto/video.dto.ts index b6f51c07..87c3ab6f 100644 --- a/src/api/dto/video.dto.ts +++ b/src/api/dto/video.dto.ts @@ -1,135 +1,33 @@ import type { ReactionType } from '@/types/script'; -export interface VideoDto { - videoId: string; - title: string; - status: 'processing' | 'ready' | 'failed'; - durationSeconds: number; - rootCommentCount: number; - replyCount: number; - reactionCount: number; - viewCount: number; - thumbnailUrl: string; - createdAt: string; -} - -/** - * 영상 타임스탬프 댓글 생성 - */ -export interface CreateCommentDto { - content: string; - - /** 답글인 경우 부모 댓글 ID */ - parentId?: string; -} - // ============================================================================ -// 영상 녹화 관련 DTO +// 공통 타입 // ============================================================================ -/** - * 영상 녹화 세션 생성 요청 DTO - */ -export interface StartVideoRequestDto { - projectId: number; - title: string; -} - -/** - * 영상 녹화 시작 응답 DTO (success 데이터) - */ -export interface StartVideoResponseDto { - videoId: string; -} -/** - * 청크 업로드 응답 DTO (success 데이터) - */ -export interface ChunkUploadResponseDto { - ok: boolean; -} +export type VideoStatus = 'processing' | 'ready' | 'failed'; -/** - * 녹화 종료 및 영상 처리 시작 요청 DTO - */ -export interface FinishVideoRequestDto { - slideLogs: Array<{ - slideId: number; - timestampMs: number; - }>; -} +// ============================================================================ +// 영상 기본 정보 DTO +// ============================================================================ /** - * 녹화 종료 및 영상 처리 시작 응답 DTO (success 데이터) + * 영상 목록 항목 (간략 정보) */ -export interface FinishVideoResponseDto { +export interface VideoListItemDto { + viewCount: number; + reactionCount: number; + replyCount: number; + rootCommentCount: number; videoId: string; - status: string; - slideCount: number; - slideDurations: Array<{ - slideId: string; - totalDurationMs: number; - }>; + title: string; + status: VideoStatus; + durationSeconds: number; + thumbnailUrl: string | null; + createdAt: string; } -/** - * 프로젝트별 영상 목록 조회 응답 DTO - * GET /presentations/:projectId/videos - */ -export interface GetProjectVideosResponseDto { - videos: VideoDto[]; - total: number; -} -/** - * 영상 상세 조회 응답 DTO - */ -export interface GetVideoDetailResponseDto { - video: { - videoId: string; - title: string; - status: 'processing' | 'ready' | 'failed'; - durationSeconds: number; - width: number; - height: number; - fps: number; - hlsMasterUrl: string; - thumbnailUrl: string; - createdAt: string; - }; - timeline: { - reactions: Array<{ - timestampMs: number; - emojiType: string; - count: number; - }>; - comments: Array<{ - commentId: string; - timestampMs: number; - content: string; - createdAt: string; - user: { - userId: string; - name: string; - }; - replies?: Array<{ - replyId: string; - content: string; - createdAt: string; - user: { - userId: string; - name: string; - }; - }>; - }>; - }; -} - -// ============================================================================ -// 영상 상세 조회 DTO — GET /videos/:videoId -// ============================================================================ - -export type VideoStatus = 'processing' | 'ready' | 'failed'; /** - * 영상 상세 정보 + * 영상 상세 정보 (전체 정보) */ export interface VideoDetailDto { videoId: string; @@ -144,6 +42,10 @@ export interface VideoDetailDto { createdAt: string; } +// ============================================================================ +// 타임라인 관련 DTO +// ============================================================================ + /** * 타임라인 리액션 항목 */ @@ -161,6 +63,16 @@ export interface VideoTimelineCommentUserDto { name: string; } +/** + * 타임라인 답글 항목 + */ +export interface VideoTimelineReplyDto { + replyId: string; + content: string; + createdAt: string; + user: VideoTimelineCommentUserDto; +} + /** * 타임라인 댓글 항목 */ @@ -170,6 +82,7 @@ export interface VideoTimelineCommentDto { content: string; createdAt: string; user: VideoTimelineCommentUserDto; + replies?: VideoTimelineReplyDto[]; } /** @@ -180,39 +93,102 @@ export interface VideoTimelineDto { comments: VideoTimelineCommentDto[]; } +// ============================================================================ +// 댓글 생성 DTO +// ============================================================================ + /** - * GET /videos/:videoId 응답 DTO + * 댓글 생성 요청 + * POST /videos/:videoId/comments */ -export interface GetVideoDetailResponseDto { - video: VideoDetailDto; - timeline: VideoTimelineDto; +export interface CreateCommentRequestDto { + content: string; + /** 답글인 경우 부모 댓글 ID */ + parentId?: string; + /** 타임스탬프 (밀리초 단위, 영상 전용) */ + timestampMs?: number; } // ============================================================================ -// 영상 목록 조회 DTO — GET /presentations/:projectId/videos +// 영상 녹화 관련 DTO // ============================================================================ /** - * 영상 목록 항목 + * 영상 녹화 시작 요청 + * POST /videos/start */ -export interface VideoListItemDto { - videoId: string; +export interface CreateStartVideoRequestDto { + projectId: number; title: string; - status: VideoStatus; - durationSeconds: number; - thumbnailUrl: string | null; - createdAt: string; } /** - * GET /presentations/:projectId/videos 응답 DTO + * 영상 녹화 시작 응답 + */ +export interface CreateStartVideoResponseDto { + videoId: string; +} + +/** + * 청크 업로드 응답 + * POST /videos/:videoId/chunks */ -export interface GetProjectVideosResponseDto { +export interface CreateChunkUploadResponseDto { + ok: boolean; +} + +/** + * 녹화 종료 요청 + * POST /videos/:videoId/finish + */ +export interface CreateFinishVideoRequestDto { + slideLogs: Array<{ + slideId: number; + timestampMs: number; + }>; +} + +/** + * 녹화 종료 응답 + */ +export interface CreateFinishVideoResponseDto { + videoId: string; + status: string; + slideCount: number; + slideDurations: Array<{ + slideId: string; + totalDurationMs: number; + }>; +} + +// ============================================================================ +// 영상 목록 조회 (READ) DTO +// ============================================================================ + +/** + * 프로젝트별 영상 목록 조회 응답 + * GET /presentations/:projectId/videos + */ +export interface ReadProjectVideosResponseDto { videos: VideoListItemDto[]; + total?: number; // 페이지네이션용 (옵션) } // ============================================================================ -// 영상-슬라이드 타임라인 DTO — GET /videos/:videoId/slides +// 영상 상세 조회 (READ) DTO +// ============================================================================ + +/** + * 영상 상세 조회 응답 + * GET /videos/:videoId + */ +export interface ReadVideoDetailResponseDto { + video: VideoDetailDto; + timeline: VideoTimelineDto; +} + +// ============================================================================ +// 영상-슬라이드 타임라인 조회 (READ) DTO // ============================================================================ /** @@ -224,8 +200,42 @@ export interface VideoSlideTimelineItemDto { } /** - * GET /videos/:videoId/slides 응답 DTO + * 영상-슬라이드 타임라인 조회 응답 + * GET /videos/:videoId/slides */ -export interface GetVideoSlidesResponseDto { +export interface ReadVideoSlidesResponseDto { slides: VideoSlideTimelineItemDto[]; } + +// ============================================================================ +// 영상 수정 (UPDATE) DTO +// ============================================================================ + +/** + * 영상 제목 수정 요청 + * PATCH /videos/:videoId + */ +export interface UpdateVideoRequestDto { + title: string; +} + +/** + * 영상 수정 응답 + */ +export interface UpdateVideoResponseDto { + videoId: string; + title: string; +} + +// ============================================================================ +// 영상 삭제 (DELETE) DTO +// ============================================================================ + +/** + * 영상 삭제 응답 + * DELETE /videos/:videoId + */ +export interface DeleteVideoResponseDto { + videoId: string; + deleted: boolean; +} diff --git a/src/api/endpoints/videos.ts b/src/api/endpoints/videos.ts index c5b89e52..762067f5 100644 --- a/src/api/endpoints/videos.ts +++ b/src/api/endpoints/videos.ts @@ -1,49 +1,34 @@ import { apiClient } from '@/api/client'; +import type { CreateCommentResponseDto, CreateReplyCommentResponseDto } from '@/api/dto'; import type { - ChunkUploadResponseDto, - CreateCommentResponseDto, - CreateReplyCommentResponseDto, - FinishVideoRequestDto, - FinishVideoResponseDto, - GetVideoDetailResponseDto, - GetVideoSlidesResponseDto, - StartVideoRequestDto, - StartVideoResponseDto, -} from '@/api/dto'; + CreateChunkUploadResponseDto, + CreateCommentRequestDto, + CreateFinishVideoRequestDto, + CreateFinishVideoResponseDto, + CreateStartVideoRequestDto, + CreateStartVideoResponseDto, + ReadProjectVideosResponseDto, + ReadVideoDetailResponseDto, + ReadVideoSlidesResponseDto, +} from '@/api/dto/video.dto'; import type { ApiResponse } from '@/types/api'; -import type { GetProjectVideosResponseDto, GetVideoDetailResponseDto } from '../dto/video.dto'; - -/** - * DTO → Model 변환: CommentResponseDto를 앱 내부용 Model로 변환 - * 주의: 서버 응답에서 댓글은 'commentId', 답글은 'replyId'를 사용할 수 있음 - */ -function commentDtoToModel(dto: CreateCommentResponseDto & { replyId?: string }): { - serverId: string; - content: string; -} { - return { - serverId: dto.commentId ?? dto.replyId ?? '', - content: dto.content, - }; -} - // ============================================================================ // 레거시 videosApi 객체 (하위 호환성 유지) // useVideoUpload 등 기존 코드에서 사용 중 // ============================================================================ export const videosApi = { // POST /videos/start - 영상 녹화 세션 생성 - startVideo: (data: StartVideoRequestDto) => - apiClient.post>('/videos/start', data), + startVideo: (data: CreateStartVideoRequestDto) => + apiClient.post>('/videos/start', data), // POST /videos/{videoId}/chunks/{chunkIndex} - 청크 업로드 uploadChunk: (videoId: string, chunkIndex: number, file: Blob) => { const formData = new FormData(); formData.append('file', file); - return apiClient.post>( - `/videos/${videoId}/chunks/${chunkIndex}`, + return apiClient.post>( + `/videos/${encodeURIComponent(videoId)}/chunks/${chunkIndex}`, formData, { headers: { @@ -54,16 +39,24 @@ export const videosApi = { }, // POST /videos/{videoId}/finish - 녹화 종료 및 영상 처리 시작 - finishVideo: (videoId: string, data: FinishVideoRequestDto) => - apiClient.post>(`/videos/${videoId}/finish`, data), + finishVideo: (videoId: string, data: CreateFinishVideoRequestDto) => + apiClient.post>( + `/videos/${encodeURIComponent(videoId)}/finish`, + data, + ), // GET /videos/{videoId} - 영상 상세 조회 getVideoDetail: (videoId: string) => - apiClient.get>(`/videos/${videoId}`), + apiClient.get>( + `/videos/${encodeURIComponent(videoId)}`, + ), // GET /videos/{videoId}/slides - 슬라이드 타임라인 조회 getVideoSlides: (videoId: string) => - apiClient.get>(`/videos/${videoId}/slides`), + apiClient.get>( + `/videos/${encodeURIComponent(videoId)}/slides`, + ), + /** * GET /presentations/:projectId/videos - 프로젝트별 영상 목록 조회 */ @@ -81,9 +74,9 @@ export const videosApi = { if (params?.sort) searchParams.set('sort', params.sort); const queryString = searchParams.toString(); - const url = `/presentations/${projectId}/videos${queryString ? `?${queryString}` : ''}`; + const url = `/presentations/${encodeURIComponent(projectId)}/videos${queryString ? `?${queryString}` : ''}`; - return apiClient.get>(url); + return apiClient.get>(url); }, }; @@ -101,10 +94,10 @@ export const videosApi = { */ export async function createVideoComment( videoId: string, - data: { content: string; timestampMs?: number }, + data: CreateCommentRequestDto & { timestampMs?: number }, ): Promise<{ serverId: string; content: string }> { const response = await apiClient.post>( - `/videos/${videoId}/comments`, + `/videos/${encodeURIComponent(videoId)}/comments`, data, ); @@ -126,10 +119,10 @@ export async function createVideoComment( */ export async function createCommentReply( commentId: string, - data: { content: string }, + data: CreateCommentRequestDto, ): Promise<{ serverId: string; content: string }> { const response = await apiClient.post>( - `/comments/${commentId}/replies`, + `/comments/${encodeURIComponent(commentId)}/replies`, data, ); @@ -148,7 +141,9 @@ export async function createCommentReply( * @param commentId - 댓글 ID */ export async function deleteVideoComment(commentId: string): Promise { - const response = await apiClient.delete>(`/comments/${commentId}`); + const response = await apiClient.delete>( + `/comments/${encodeURIComponent(commentId)}`, + ); if (response.data.resultType === 'FAILURE') { throw new Error(response.data.error.reason); diff --git a/src/hooks/useVideoUpload.ts b/src/hooks/useVideoUpload.ts index ea69280c..f823c0e9 100644 --- a/src/hooks/useVideoUpload.ts +++ b/src/hooks/useVideoUpload.ts @@ -1,6 +1,7 @@ import { useState } from 'react'; -import type { FinishVideoResponseDto, StartVideoResponseDto } from '@/api/dto'; +import type { CreateStartVideoResponseDto } from '@/api/dto'; +import type { CreateFinishVideoResponseDto } from '@/api/dto/video.dto'; import { videosApi } from '@/api/endpoints/videos'; import type { ApiResponse } from '@/types/api'; import type { MockVideo } from '@/types/video'; @@ -55,7 +56,7 @@ export const useVideoUpload = () => { }); const startResponse = await videosApi.startVideo({ projectId, title }); - const startData: ApiResponse = startResponse.data; + const startData: ApiResponse = startResponse.data; if (startData.resultType === 'FAILURE' || !startData.success?.videoId) { throw new Error(startData.error?.reason || 'Video ID를 받지 못했습니다.'); @@ -105,7 +106,7 @@ export const useVideoUpload = () => { }); const finishResponse = await videosApi.finishVideo(videoId.toString(), { slideLogs }); - const finishData: ApiResponse = finishResponse.data; + const finishData: ApiResponse = finishResponse.data; if (finishData.resultType === 'FAILURE') { throw new Error(finishData.error?.reason || '영상 처리에 실패했습니다.'); @@ -143,7 +144,7 @@ export const useVideoUpload = () => { currentStep: 'done', }); - return videoId; + return videoId.toString(); } catch (err: unknown) { let errorMessage = '업로드 중 오류가 발생했습니다.'; diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 09b4a456..0a047edd 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -7,11 +7,21 @@ */ import { HttpResponse, delay, http } from 'msw'; -import type { VideoDto } from '@/api/dto/video.dto'; +import type { CreateCommentResponseDto, CreateReplyCommentResponseDto } from '@/api/dto'; +import type { + CreateChunkUploadResponseDto, + CreateCommentRequestDto, + CreateFinishVideoRequestDto, + CreateFinishVideoResponseDto, + CreateStartVideoRequestDto, + CreateStartVideoResponseDto, + ReadProjectVideosResponseDto, + ReadVideoDetailResponseDto, + ReadVideoSlidesResponseDto, +} from '@/api/dto/video.dto'; +import type { ApiResponse } from '@/types'; import type { Presentation } from '@/types/presentation'; -import type { SlideListItem } from '@/types/slide'; import type { SlideDetail } from '@/types/slide'; -import type { MockVideo } from '@/types/video'; import { getMockProjectAnalyticsSummary, @@ -21,7 +31,15 @@ import { import { MOCK_PROJECTS } from './projects'; import { MOCK_SLIDES } from './slides'; import { MOCK_CURRENT_USER, MOCK_USERS } from './users'; -import { MOCK_VIDEO } from './videos'; +import { + filterVideosBySearch, + filterVideosByStatus, + mockSlideTimelines, + mockTimelines, + mockVideoDetails, + mockVideoList, + sortVideos, +} from './videos'; // ── URL Constants ──────────────────────────────────────────── const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080'; @@ -425,7 +443,10 @@ const scriptHandlers = [ // 버전 추가 const versions = scriptVersions.get(slideId) ?? []; - const nextVer = versions.length > 0 ? Math.max(...versions.map((v) => v.versionNumber)) + 1 : 1; + const nextVer = + versions.length > 0 + ? Math.max(...versions.map((v: { versionNumber: number }) => v.versionNumber)) + 1 + : 1; versions.push({ versionNumber: nextVer, scriptText: text, @@ -458,7 +479,9 @@ const scriptHandlers = [ const { slideId } = params as { slideId: string }; const body = (await request.json()) as { version: number }; const versions = scriptVersions.get(slideId) ?? []; - const target = versions.find((v) => v.versionNumber === body.version); + const target = versions.find( + (v: { versionNumber: number }) => v.versionNumber === body.version, + ); if (!target) return fail(404, 'SC001', '해당 버전을 찾을 수 없습니다.'); const slide = slides.find((s) => s.slideId === slideId); @@ -815,16 +838,17 @@ const reactionHandlers = [ const url = new URL(request.url); const intervalMs = Number(url.searchParams.get('intervalMs') ?? '5000'); - // MOCK_VIDEO feedbacks에서 타임라인 마커 생성 + // mock 타임라인에서 마커 생성 const markers: { timestampMs: number; emojiType: string; count: number }[] = []; - if (videoId === MOCK_VIDEO.videoId) { - MOCK_VIDEO.feedbacks.forEach((fb) => { - const bucket = Math.floor(fb.timestampMs / intervalMs) * intervalMs; - fb.reactions.forEach((r) => { - if (r.count > 0) markers.push({ timestampMs: bucket, emojiType: r.type, count: r.count }); - }); + const timeline = mockTimelines[videoId]; + + if (timeline) { + timeline.reactions.forEach((r) => { + const bucket = Math.floor(r.timestampMs / intervalMs) * intervalMs; + markers.push({ timestampMs: bucket, emojiType: r.emojiType, count: r.count }); }); } + return ok({ intervalMs, markers }); }), @@ -837,17 +861,18 @@ const reactionHandlers = [ const windowMs = Number(url.searchParams.get('windowMs') ?? '2000'); const result: { emojiType: string; count: number }[] = []; - if (videoId === MOCK_VIDEO.videoId) { + const timeline = mockTimelines[videoId]; + + if (timeline) { const totals: Record = {}; - MOCK_VIDEO.feedbacks.forEach((fb) => { - if (fb.timestampMs >= timestampMs - windowMs && fb.timestampMs <= timestampMs + windowMs) { - fb.reactions.forEach((r) => { - totals[r.type] = (totals[r.type] ?? 0) + r.count; - }); + timeline.reactions.forEach((r) => { + if (r.timestampMs >= timestampMs - windowMs && r.timestampMs <= timestampMs + windowMs) { + totals[r.emojiType] = (totals[r.emojiType] ?? 0) + r.count; } }); Object.entries(totals).forEach(([type, count]) => result.push({ emojiType: type, count })); } + return ok(result); }), ]; @@ -857,172 +882,195 @@ const reactionHandlers = [ // ═══════════════════════════════════════════════════════════════ const videoHandlers = [ - // 녹화 시작 + // POST /videos/start - 영상 녹화 세션 생성 http.post(`${BASE_URL}/videos/start`, async ({ request }) => { await delay(200); - const body = (await request.json()) as { projectId: number; title: string }; - return ok({ videoId: nextId(), ...body }); + const body = (await request.json()) as CreateStartVideoRequestDto; + const videoId = String(Date.now()); + + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: { + videoId, + }, + error: null, + }); }), - // 청크 업로드 - http.post(`${BASE_URL}/videos/:videoId/chunks/:chunkIndex`, async () => { + // POST /videos/:videoId/chunks/:chunkIndex - 청크 업로드 + http.post(`${BASE_URL}/videos/:videoId/chunks/:chunkIndex`, async ({ params }) => { await delay(100); - return ok({ ok: true }); + const { videoId, chunkIndex } = params; + + console.log(`[MSW] Chunk ${chunkIndex} uploaded for video ${videoId}`); + + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: { + ok: true, + }, + error: null, + }); }), - // 녹화 완료 + // POST /videos/:videoId/finish - 녹화 종료 http.post(`${BASE_URL}/videos/:videoId/finish`, async ({ params, request }) => { await delay(300); - const { videoId } = params as { videoId: string }; - const body = (await request.json()) as { - slideLogs: { slideId: number; timestampMs: number }[]; - }; - return ok({ - videoId, - status: 'processing', - slideCount: body.slideLogs.length, - slideDurations: body.slideLogs.map((log) => ({ - slideId: String(log.slideId), - totalDurationMs: 30000, - })), + const { videoId } = params; + const body = (await request.json()) as CreateFinishVideoRequestDto; + + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: { + videoId: String(videoId), + status: 'processing', + slideCount: body.slideLogs.length, + slideDurations: body.slideLogs.map((log) => ({ + slideId: String(log.slideId), + totalDurationMs: log.timestampMs, + })), + }, + error: null, }); }), - // 프로젝트별 영상 목록 (검색/필터/정렬 지원) + // GET /presentations/:projectId/videos - 프로젝트별 영상 목록 조회 http.get(`${BASE_URL}/presentations/:projectId/videos`, async ({ params, request }) => { await delay(200); - const { projectId } = params as { projectId: string }; + const { projectId } = params; const url = new URL(request.url); const search = url.searchParams.get('search') || ''; - const filter = url.searchParams.get('filter') || 'all'; + const filter = url.searchParams.get('filter') || ''; const sort = url.searchParams.get('sort') || 'recent'; - console.log(`[MSW] GET /presentations/${projectId}/videos`, { search, filter, sort }); - - // localStorage에서 읽기 - const storedData = localStorage.getItem('mockVideos'); - if (!storedData) { - return ok({ videos: [], total: 0 }); - } - - const localVideos: MockVideo[] = JSON.parse(storedData) as MockVideo[]; - - // 해당 프로젝트의 영상만 필터링 - const projectVideos = localVideos.filter((video) => video.projectId === projectId); - - // VideoDto 형식으로 변환 - let videos: VideoDto[] = projectVideos.map((video) => ({ - videoId: String(video.id), - title: video.title, - status: 'ready' as const, - durationSeconds: video.durationSeconds, - rootCommentCount: video.rootCommentCount, - replyCount: video.replyCount, - reactionCount: video.reactionCount, - viewCount: video.viewCount, - thumbnailUrl: `https://example.com/thumb${video.id}.jpg`, - createdAt: video.createdAt, - })); + // 필터링 & 정렬 + let videos = mockVideoList; + videos = filterVideosBySearch(videos, search); + videos = filterVideosByStatus(videos, filter); + videos = sortVideos(videos, sort); + + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: { + videos, + total: videos.length, + }, + error: null, + }); + }), - // 검색 필터링 - if (search) { - videos = videos.filter((video) => video.title.toLowerCase().includes(search.toLowerCase())); + // GET /videos/:videoId - 영상 상세 조회 + http.get(`${BASE_URL}/videos/:videoId`, async ({ params }) => { + await delay(200); + const { videoId } = params; + const video = mockVideoDetails[String(videoId)]; + const timeline = mockTimelines[String(videoId)]; + + if (!video) { + return HttpResponse.json>( + { + resultType: 'FAILURE', + error: { + errorCode: 'VIDEO_NOT_FOUND', + reason: '영상을 찾을 수 없습니다.', + data: null, + }, + success: null, + }, + { status: 404 }, + ); } - // 듀레이션 필터링 - if (filter === '3m') { - videos = videos.filter((video) => video.durationSeconds <= 180); - } else if (filter === '5m') { - videos = videos.filter((video) => video.durationSeconds <= 300); - } + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: { + video, + timeline: timeline || { reactions: [], comments: [] }, + }, + error: null, + }); + }), - // 정렬 - if (sort === 'recent') { - videos.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - } else if (sort === 'commentCount') { - videos.sort( - (a, b) => b.rootCommentCount + b.replyCount - (a.rootCommentCount + a.replyCount), + // GET /videos/:videoId/slides - 슬라이드 타임라인 조회 + http.get(`${BASE_URL}/videos/:videoId/slides`, async ({ params }) => { + await delay(150); + const { videoId } = params; + const slides = mockSlideTimelines[String(videoId)]; + + if (!slides) { + return HttpResponse.json>( + { + resultType: 'FAILURE', + error: { + errorCode: 'VIDEO_NOT_FOUND', + reason: '영상을 찾을 수 없습니다.', + data: null, + }, + success: null, + }, + { status: 404 }, ); - } else if (sort === 'name') { - videos.sort((a, b) => a.title.localeCompare(b.title, 'ko')); } - return ok({ videos, total: videos.length }); + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: { + slides, + }, + error: null, + }); }), - // 영상 상세 (timeline 포함) - http.get(`${BASE_URL}/videos/:videoId`, async ({ params }) => { + // POST /videos/:videoId/comments - 댓글 작성 + http.post(`${BASE_URL}/videos/:videoId/comments`, async ({ params, request }) => { await delay(200); - const { videoId } = params as { videoId: string }; - if (videoId !== MOCK_VIDEO.videoId) return fail(404, 'V001', '영상을 찾을 수 없습니다.'); - - // MOCK_VIDEO feedbacks → Server timeline 포맷으로 변환 - const timelineReactions: { timestampMs: number; emojiType: string; count: number }[] = []; - const timelineComments: { - commentId: string; - timestampMs: number; - content: string; - createdAt: string; - user: { userId: string; name: string }; - }[] = []; - - MOCK_VIDEO.feedbacks.forEach((fb) => { - fb.reactions.forEach((r) => { - if (r.count > 0) { - timelineReactions.push({ - timestampMs: fb.timestampMs * 1000, // seconds → ms - emojiType: r.type, - count: r.count, - }); - } - }); - fb.comments.forEach((c) => { - const user = MOCK_USERS.find((u) => u.id === c.userId); - timelineComments.push({ - commentId: c.commentId, - timestampMs: fb.timestampMs, - content: c.content, - createdAt: c.createdAt, - user: { userId: c.userId, name: user?.name ?? '알 수 없음' }, - }); - }); + const { videoId } = params; + const body = (await request.json()) as CreateCommentRequestDto & { timestampMs?: number }; + + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: { + commentId: `c${Date.now()}`, + content: body.content, + createdAt: new Date().toISOString(), + userId: MOCK_CURRENT_USER.id, + }, + error: null, }); + }), - return ok({ - video: { - videoId: MOCK_VIDEO.videoId, - title: MOCK_VIDEO.title, - status: 'ready', - durationSeconds: MOCK_VIDEO.duration, - width: 1920, - height: 1080, - fps: 30, - hlsMasterUrl: MOCK_VIDEO.videoUrl, - thumbnailUrl: null, + // POST /comments/:commentId/replies - 답글 작성 + http.post(`${BASE_URL}/comments/:commentId/replies`, async ({ params, request }) => { + await delay(200); + const { commentId } = params; + const body = (await request.json()) as CreateCommentRequestDto; + + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: { + replyId: `r${Date.now()}`, + content: body.content, createdAt: new Date().toISOString(), + userId: MOCK_CURRENT_USER.id, + parentCommentId: String(commentId), }, - timeline: { reactions: timelineReactions, comments: timelineComments }, + error: null, }); }), - // 영상-슬라이드 타임라인 - http.get(`${BASE_URL}/videos/:videoId/slides`, async ({ params }) => { - await delay(100); - const { videoId } = params as { videoId: string }; - if (videoId !== MOCK_VIDEO.videoId) return fail(404, 'V001', '영상을 찾을 수 없습니다.'); - // p1 프로젝트의 처음 5개 슬라이드로 타임라인 생성 - const p1Slides = slides - .filter((s) => s.projectId === 'p1') - .sort((a, b) => a.slideNum - b.slideNum) - .slice(0, 5); - return ok({ - slides: p1Slides.map((s, i) => ({ - slideId: s.slideId, - timestampMs: i * Math.floor((MOCK_VIDEO.duration * 1000) / 5), - })), + // DELETE /comments/:commentId - 댓글 삭제 + http.delete(`${BASE_URL}/comments/:commentId`, async ({ params }) => { + await delay(150); + const { commentId } = params; + + return HttpResponse.json>({ + resultType: 'SUCCESS', + success: null, + error: null, }); }), ]; + // ═══════════════════════════════════════════════════════════════ // SHARES — /presentations/:projectId/shares, /shares/:shareToken // ═══════════════════════════════════════════════════════════════ @@ -1058,7 +1106,7 @@ const shareHandlers = [ sharedContentSummary: { scope: body.scope, projectTitle: project?.title ?? '', - videoTitle: body.videoId ? MOCK_VIDEO.title : null, + videoTitle: body.videoId ? mockVideoDetails[body.videoId]?.title : null, videoCreatedAt: body.videoId ? now : null, thumbnailUrl: project?.thumbnailUrl ?? null, }, @@ -1079,6 +1127,8 @@ const shareHandlers = [ .filter((s) => s.projectId === link.projectId) .sort((a, b) => a.slideNum - b.slideNum); + const videoDetail = link.videoId ? mockVideoDetails[link.videoId] : null; + return ok({ message: '공유된 프로젝트에 접속했습니다.', sessionInfo: { @@ -1098,11 +1148,11 @@ const shareHandlers = [ scriptText: s.script ?? '', })), video: - link.scope === 'slides_script_video' + link.scope === 'slides_script_video' && videoDetail ? { - videoId: MOCK_VIDEO.videoId, - videoUrl: MOCK_VIDEO.videoUrl, - thumbnailUrl: null, + videoId: videoDetail.videoId, + videoUrl: videoDetail.hlsMasterUrl, + thumbnailUrl: videoDetail.thumbnailUrl, } : null, }, @@ -1123,7 +1173,7 @@ const shareHandlers = [ shareToken: l.shareToken, isActive: true, viewCount: l.viewCount, - videoTitle: l.videoId ? MOCK_VIDEO.title : null, + videoTitle: l.videoId ? mockVideoDetails[l.videoId]?.title : null, createdAt: l.createdAt, })), ); @@ -1136,20 +1186,23 @@ const shareHandlers = [ const page = Number(url.searchParams.get('page') ?? '1'); const pageSize = Number(url.searchParams.get('pageSize') ?? '10'); - const allVideos = [ - { - id: MOCK_VIDEO.videoId, - title: MOCK_VIDEO.title, - thumbnailUrl: null as string | null, - createdAt: new Date().toISOString(), - }, - ]; + const allVideos = mockVideoList.map((v) => ({ + id: v.videoId, + title: v.title, + thumbnailUrl: v.thumbnailUrl, + createdAt: v.createdAt, + })); + const start = (page - 1) * pageSize; const paged = allVideos.slice(start, start + pageSize); return ok({ videos: paged, - pagination: { currentPage: page, totalCount: allVideos.length, hasNext: false }, + pagination: { + currentPage: page, + totalCount: allVideos.length, + hasNext: start + pageSize < allVideos.length, + }, }); }), ]; @@ -1210,18 +1263,34 @@ const analyticsHandlers = [ }), // 영상 타임라인 분석 - http.get(`${BASE_URL}/videos/:videoId/analytics/timeline`, async () => { + http.get(`${BASE_URL}/videos/:videoId/analytics/timeline`, async ({ params }) => { await delay(100); - const timeline = []; - for (let t = 0; t <= MOCK_VIDEO.duration * 1000; t += 30000) { - timeline.push({ - timestampMs: t, - reactionCount: Math.floor(Math.random() * 10), - commentCount: Math.floor(Math.random() * 3), - feedbackCount: Math.floor(Math.random() * 13), - }); + const { videoId } = params as { videoId: string }; + const timeline = mockTimelines[videoId]; + + const result = []; + if (timeline) { + // 30초 간격으로 데이터 생성 + const duration = mockVideoDetails[videoId]?.durationSeconds || 600; + for (let t = 0; t <= duration * 1000; t += 30000) { + const reactionsInWindow = timeline.reactions.filter( + (r) => r.timestampMs >= t - 15000 && r.timestampMs < t + 15000, + ); + const commentsInWindow = timeline.comments.filter( + (c) => c.timestampMs >= t - 15000 && c.timestampMs < t + 15000, + ); + + result.push({ + timestampMs: t, + reactionCount: reactionsInWindow.reduce((sum, r) => sum + r.count, 0), + commentCount: commentsInWindow.length, + feedbackCount: + reactionsInWindow.reduce((sum, r) => sum + r.count, 0) + commentsInWindow.length, + }); + } } - return ok({ timeline }); + + return ok({ timeline: result }); }), // 페이지뷰 기록 diff --git a/src/mocks/videos.ts b/src/mocks/videos.ts index 4867c810..97ae2abc 100644 --- a/src/mocks/videos.ts +++ b/src/mocks/videos.ts @@ -1,432 +1,481 @@ -// src/mocks/videos.ts -import type { MockVideo, VideoFeedback } from '@/types/video'; +import type { + VideoDetailDto, + VideoListItemDto, + VideoSlideTimelineItemDto, + VideoTimelineCommentDto, + VideoTimelineDto, + VideoTimelineReactionDto, +} from '@/api/dto'; -import { MOCK_USERS } from './users'; -import { timeAgo } from './utils'; +// ============================================================================ +// Mock 영상 목록 데이터 +// ============================================================================ -/** - * Mock Videos 목록 (영상 목록 조회용) - */ -export const MOCK_VIDEOS: MockVideo[] = [ +export const mockVideoList: VideoListItemDto[] = [ { - id: 1, - projectId: 'p1', + videoId: '1', title: '테스트 영상', - createdAt: '2026-02-03T13:20:00.000Z', + status: 'ready', durationSeconds: 596, - slideCount: 10, - rootCommentCount: 8, - replyCount: 7, - reactionCount: 156, - viewCount: 42, + thumbnailUrl: 'https://picsum.photos/seed/video1/640/360', + createdAt: '2026-02-03T13:20:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, { - id: 2, - projectId: 'p1', + videoId: '2', title: '내 발표 리허설', - createdAt: '2026-02-03T10:30:00.000Z', + status: 'ready', durationSeconds: 95, - slideCount: 5, - rootCommentCount: 2, - replyCount: 1, - reactionCount: 7, - viewCount: 4, + thumbnailUrl: 'https://picsum.photos/seed/video2/640/360', + createdAt: '2026-02-03T10:30:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, { - id: 3, - projectId: 'p1', + videoId: '3', title: 'Q4 마케팅 전략 발표', - createdAt: '2026-02-02T15:45:00.000Z', + status: 'ready', durationSeconds: 420, - slideCount: 15, - rootCommentCount: 5, - replyCount: 3, - reactionCount: 34, - viewCount: 18, + thumbnailUrl: 'https://picsum.photos/seed/video3/640/360', + createdAt: '2026-02-02T15:45:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, { - id: 4, - projectId: 'p2', + videoId: '4', title: '신제품 소개', - createdAt: '2026-02-01T09:00:00.000Z', + status: 'ready', durationSeconds: 180, - slideCount: 8, - rootCommentCount: 3, - replyCount: 2, - reactionCount: 15, - viewCount: 12, + thumbnailUrl: 'https://picsum.photos/seed/video4/640/360', + createdAt: '2026-02-01T09:00:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, { - id: 5, - projectId: 'p2', + videoId: '5', title: '프로젝트 최종 발표', - createdAt: '2026-01-31T14:20:00.000Z', + status: 'ready', durationSeconds: 300, - slideCount: 12, - rootCommentCount: 7, - replyCount: 5, - reactionCount: 48, - viewCount: 25, + thumbnailUrl: 'https://picsum.photos/seed/video5/640/360', + createdAt: '2026-01-31T14:20:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, { - id: 6, - projectId: 'p2', + videoId: '6', title: '주간 업무 보고', - createdAt: '2026-01-30T11:00:00.000Z', + status: 'ready', durationSeconds: 240, - slideCount: 6, - rootCommentCount: 1, + thumbnailUrl: 'https://picsum.photos/seed/video6/640/360', + createdAt: '2026-01-30T11:00:00.000Z', + viewCount: 0, + reactionCount: 0, replyCount: 0, - reactionCount: 5, - viewCount: 8, + rootCommentCount: 0, }, { - id: 7, - projectId: 'p1', + videoId: '7', title: '고객 피드백 분석', - createdAt: '2026-01-29T16:30:00.000Z', + status: 'processing', durationSeconds: 360, - slideCount: 18, - rootCommentCount: 4, - replyCount: 2, - reactionCount: 22, - viewCount: 15, + thumbnailUrl: null, + createdAt: '2026-01-29T16:30:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, { - id: 8, - projectId: 'p1', + videoId: '8', title: '2026 전략 수립', - createdAt: '2026-01-28T13:15:00.000Z', + status: 'ready', durationSeconds: 480, - slideCount: 20, - rootCommentCount: 9, - replyCount: 6, - reactionCount: 67, - viewCount: 31, + thumbnailUrl: 'https://picsum.photos/seed/video8/640/360', + createdAt: '2026-01-28T13:15:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, { - id: 9, - projectId: 'p2', + videoId: '9', title: '팀 빌딩 아이디어', - createdAt: '2026-01-27T10:45:00.000Z', + status: 'ready', durationSeconds: 150, - slideCount: 7, - rootCommentCount: 6, - replyCount: 8, - reactionCount: 41, - viewCount: 22, + thumbnailUrl: 'https://picsum.photos/seed/video9/640/360', + createdAt: '2026-01-27T10:45:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, { - id: 10, - projectId: 'p2', + videoId: '10', title: '디자인 시스템 소개', - createdAt: '2026-01-26T14:00:00.000Z', + status: 'failed', durationSeconds: 270, - slideCount: 11, - rootCommentCount: 3, - replyCount: 1, - reactionCount: 18, - viewCount: 10, + thumbnailUrl: null, + createdAt: '2026-01-26T14:00:00.000Z', + viewCount: 0, + reactionCount: 0, + replyCount: 0, + rootCommentCount: 0, }, ]; -/** - * 테스트용 웹캠 영상 피드백 데이터 (영상 상세/재생 페이지용) - * - duration: 596초 (9:56) - * - 영상 전체에 걸쳐 다양한 시간대에 feedbacks 분포 - * - 상위 10개 세그먼트가 재생바에 하이라이트로 표시됨 - */ -export const MOCK_VIDEO: VideoFeedback = { - videoId: '1', - videoUrl: '/p1.webm', - title: '테스트 영상', - duration: 596, - comments: [], - reactionEvents: [], - feedbacks: [ - // ===== 앞부분 (0~60초) ===== - // 3초 - fire 우세 (총합: 15) - { - timestampMs: 3_000, - comments: [ - { - commentId: 'vc-1', - userId: MOCK_USERS[0].id, - content: '오프닝이 멋있네요!', - createdAt: timeAgo(2, 'minute'), - isMine: true, - ref: { kind: 'video' as const, seconds: 3 }, - }, - ], - reactions: [ - { type: 'fire' as const, count: 8, active: true }, - { type: 'sleepy' as const, count: 0, active: false }, - { type: 'good' as const, count: 5, active: false }, - { type: 'bad' as const, count: 1, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +// ============================================================================ +// Mock 영상 상세 데이터 +// ============================================================================ - // 15초 - good 우세 (총합: 12) - { - timestampMs: 15_000, - comments: [ - { - commentId: 'vc-3', - userId: MOCK_USERS[2].id, - content: '배경 음악이 좋습니다.', - createdAt: timeAgo(5, 'minute'), - isMine: false, - ref: { kind: 'video' as const, seconds: 15 }, - }, - ], - reactions: [ - { type: 'fire' as const, count: 2, active: false }, - { type: 'sleepy' as const, count: 1, active: false }, - { type: 'good' as const, count: 7, active: false }, - { type: 'bad' as const, count: 1, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +export const mockVideoDetails: Record = { + '1': { + videoId: '1', + title: '테스트 영상', + status: 'ready', + durationSeconds: 596, + width: 1920, + height: 1080, + fps: 30, + hlsMasterUrl: '/p1.webm', + thumbnailUrl: 'https://picsum.photos/seed/video1/640/360', + createdAt: '2026-02-03T13:20:00.000Z', + }, + '2': { + videoId: '2', + title: '내 발표 리허설', + status: 'ready', + durationSeconds: 95, + width: 1920, + height: 1080, + fps: 30, + hlsMasterUrl: 'https://example.com/videos/2/master.m3u8', + thumbnailUrl: 'https://picsum.photos/seed/video2/640/360', + createdAt: '2026-02-03T10:30:00.000Z', + }, + '3': { + videoId: '3', + title: 'Q4 마케팅 전략 발표', + status: 'ready', + durationSeconds: 420, + width: 1920, + height: 1080, + fps: 30, + hlsMasterUrl: 'https://example.com/videos/3/master.m3u8', + thumbnailUrl: 'https://picsum.photos/seed/video3/640/360', + createdAt: '2026-02-02T15:45:00.000Z', + }, +}; - // 28초 - sleepy 우세 (총합: 8) - { - timestampMs: 28_000, - comments: [], - reactions: [ - { type: 'fire' as const, count: 1, active: false }, - { type: 'sleepy' as const, count: 5, active: false }, - { type: 'good' as const, count: 1, active: false }, - { type: 'bad' as const, count: 0, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +// ============================================================================ +// Mock 타임라인 데이터 +// ============================================================================ - // 45초 - fire 우세 (총합: 20) - 인기 구간! - { - timestampMs: 45_000, - comments: [ - { - commentId: 'vc-45-1', - userId: MOCK_USERS[1].id, - content: '여기 정말 좋아요!', - createdAt: timeAgo(10, 'minute'), - isMine: false, - ref: { kind: 'video' as const, seconds: 45 }, +export const mockTimelines: Record = { + '1': { + reactions: [ + { + timestampMs: 3000, + emojiType: 'fire', + count: 8, + }, + { + timestampMs: 15000, + emojiType: 'good', + count: 7, + }, + { + timestampMs: 28000, + emojiType: 'sleepy', + count: 5, + }, + { + timestampMs: 45000, + emojiType: 'fire', + count: 12, + }, + { + timestampMs: 90000, + emojiType: 'confused', + count: 6, + }, + { + timestampMs: 150000, + emojiType: 'fire', + count: 15, + }, + { + timestampMs: 210000, + emojiType: 'good', + count: 9, + }, + { + timestampMs: 270000, + emojiType: 'bad', + count: 4, + }, + { + timestampMs: 330000, + emojiType: 'fire', + count: 10, + }, + { + timestampMs: 390000, + emojiType: 'good', + count: 8, + }, + { + timestampMs: 450000, + emojiType: 'fire', + count: 9, + }, + { + timestampMs: 520000, + emojiType: 'sleepy', + count: 3, + }, + { + timestampMs: 560000, + emojiType: 'confused', + count: 100, + }, + { + timestampMs: 590000, + emojiType: 'fire', + count: 12, + }, + ], + comments: [ + { + commentId: 'vc-1', + timestampMs: 3000, + content: '오프닝이 멋있네요!', + createdAt: new Date(Date.now() - 2 * 60 * 1000).toISOString(), + user: { + userId: 'user1', + name: '김예원', }, - ], - reactions: [ - { type: 'fire' as const, count: 12, active: true }, - { type: 'sleepy' as const, count: 0, active: false }, - { type: 'good' as const, count: 6, active: true }, - { type: 'bad' as const, count: 1, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, - - // ===== 중간부분 (60~300초) ===== - // 90초 - confused 우세 (총합: 10) - { - timestampMs: 90_000, - comments: [ - { - commentId: 'vc-90-1', - userId: MOCK_USERS[3].id, - content: '이 부분이 좀 어려웠어요.', - createdAt: timeAgo(8, 'minute'), - isMine: false, - ref: { kind: 'video' as const, seconds: 90 }, + }, + { + commentId: 'vc-3', + timestampMs: 15000, + content: '배경 음악이 좋습니다.', + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + user: { + userId: 'user2', + name: '이철수', }, - ], - reactions: [ - { type: 'fire' as const, count: 1, active: false }, - { type: 'sleepy' as const, count: 2, active: false }, - { type: 'good' as const, count: 1, active: false }, - { type: 'bad' as const, count: 0, active: false }, - { type: 'confused' as const, count: 6, active: false }, - ], - }, - - // 150초 - fire 우세 (총합: 25) - 가장 인기 구간! - { - timestampMs: 150_000, - comments: [ - { - commentId: 'vc-150-1', - userId: MOCK_USERS[0].id, - content: '하이라이트 부분이네요!', - createdAt: timeAgo(15, 'minute'), - isMine: true, - ref: { kind: 'video' as const, seconds: 150 }, + }, + { + commentId: 'vc-45-1', + timestampMs: 45000, + content: '여기 정말 좋아요!', + createdAt: new Date(Date.now() - 10 * 60 * 1000).toISOString(), + user: { + userId: 'user3', + name: '박영희', + }, + }, + { + commentId: 'vc-90-1', + timestampMs: 90000, + content: '이 부분이 좀 어려웠어요.', + createdAt: new Date(Date.now() - 8 * 60 * 1000).toISOString(), + user: { + userId: 'user4', + name: '최민수', + }, + }, + { + commentId: 'vc-150-1', + timestampMs: 150000, + content: '하이라이트 부분이네요!', + createdAt: new Date(Date.now() - 15 * 60 * 1000).toISOString(), + user: { + userId: 'user1', + name: '김예원', + }, + replies: [ + { + replyId: 'vc-150-2', + content: '완전 동의합니다!', + createdAt: new Date(Date.now() - 14 * 60 * 1000).toISOString(), + user: { + userId: 'user5', + name: '정지훈', + }, + }, + ], + }, + { + commentId: 'vc-270-1', + timestampMs: 270000, + content: '이 부분은 개선이 필요해 보여요.', + createdAt: new Date(Date.now() - 20 * 60 * 1000).toISOString(), + user: { + userId: 'user2', + name: '이철수', + }, + }, + { + commentId: 'vc-330-1', + timestampMs: 330000, + content: '다시 재미있어졌네요!', + createdAt: new Date(Date.now() - 25 * 60 * 1000).toISOString(), + user: { + userId: 'user3', + name: '박영희', }, - { - commentId: 'vc-150-2', - userId: MOCK_USERS[4].id, - content: '완전 동의합니다!', - createdAt: timeAgo(14, 'minute'), - isMine: false, - parentId: 'vc-150-1', - isReply: true, + }, + { + commentId: 'vc-450-1', + timestampMs: 450000, + content: '클라이막스 부분이네요!', + createdAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(), + user: { + userId: 'user4', + name: '최민수', + }, + }, + { + commentId: 'vc-560-1', + timestampMs: 560000, + content: '엔딩 크레딧도 예쁘네요!', + createdAt: new Date(Date.now() - 35 * 60 * 1000).toISOString(), + user: { + userId: 'user5', + name: '정지훈', + }, + }, + { + commentId: 'vc-590-1', + timestampMs: 590000, + content: '마무리가 정말 좋았어요!', + createdAt: new Date(Date.now() - 40 * 60 * 1000).toISOString(), + user: { + userId: 'user1', + name: '김예원', + }, + }, + ], + }, + '2': { + reactions: [], + comments: [], + }, + '3': { + reactions: [ + { + timestampMs: 60000, + emojiType: 'good', + count: 10, + }, + ], + comments: [ + { + commentId: 'c3', + timestampMs: 120000, + content: 'Q4 전략 잘 봤습니다', + createdAt: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(), + user: { + userId: 'user6', + name: '강민지', }, - ], - reactions: [ - { type: 'fire' as const, count: 15, active: true }, - { type: 'sleepy' as const, count: 0, active: false }, - { type: 'good' as const, count: 8, active: false }, - { type: 'bad' as const, count: 1, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, + }, + ], + }, +}; - // 210초 - good 우세 (총합: 14) - { - timestampMs: 210_000, - comments: [], - reactions: [ - { type: 'fire' as const, count: 3, active: false }, - { type: 'sleepy' as const, count: 1, active: false }, - { type: 'good' as const, count: 9, active: false }, - { type: 'bad' as const, count: 0, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +// ============================================================================ +// Mock 슬라이드 타임라인 데이터 +// ============================================================================ - // 270초 - bad 우세 (총합: 7) - { - timestampMs: 270_000, - comments: [ - { - commentId: 'vc-270-1', - userId: MOCK_USERS[2].id, - content: '이 부분은 개선이 필요해 보여요.', - createdAt: timeAgo(20, 'minute'), - isMine: false, - ref: { kind: 'video' as const, seconds: 270 }, - }, - ], - reactions: [ - { type: 'fire' as const, count: 0, active: false }, - { type: 'sleepy' as const, count: 1, active: false }, - { type: 'good' as const, count: 1, active: false }, - { type: 'bad' as const, count: 4, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +export const mockSlideTimelines: Record = { + '1': [ + { slideId: '1', timestampMs: 0 }, + { slideId: '2', timestampMs: 60000 }, + { slideId: '3', timestampMs: 120000 }, + { slideId: '4', timestampMs: 180000 }, + { slideId: '5', timestampMs: 240000 }, + { slideId: '6', timestampMs: 300000 }, + { slideId: '7', timestampMs: 360000 }, + { slideId: '8', timestampMs: 420000 }, + { slideId: '9', timestampMs: 480000 }, + { slideId: '10', timestampMs: 540000 }, + ], + '2': [ + { slideId: '1', timestampMs: 0 }, + { slideId: '2', timestampMs: 19000 }, + { slideId: '3', timestampMs: 38000 }, + { slideId: '4', timestampMs: 57000 }, + { slideId: '5', timestampMs: 76000 }, + ], + '3': [ + { slideId: '1', timestampMs: 0 }, + { slideId: '2', timestampMs: 28000 }, + { slideId: '3', timestampMs: 56000 }, + { slideId: '4', timestampMs: 84000 }, + { slideId: '5', timestampMs: 112000 }, + { slideId: '6', timestampMs: 140000 }, + { slideId: '7', timestampMs: 168000 }, + { slideId: '8', timestampMs: 196000 }, + { slideId: '9', timestampMs: 224000 }, + { slideId: '10', timestampMs: 252000 }, + { slideId: '11', timestampMs: 280000 }, + { slideId: '12', timestampMs: 308000 }, + { slideId: '13', timestampMs: 336000 }, + { slideId: '14', timestampMs: 364000 }, + { slideId: '15', timestampMs: 392000 }, + ], +}; - // ===== 후반부 (300~500초) ===== - // 330초 - fire 우세 (총합: 18) - { - timestampMs: 330_000, - comments: [ - { - commentId: 'vc-330-1', - userId: MOCK_USERS[1].id, - content: '다시 재미있어졌네요!', - createdAt: timeAgo(25, 'minute'), - isMine: false, - ref: { kind: 'video' as const, seconds: 330 }, - }, - ], - reactions: [ - { type: 'fire' as const, count: 10, active: false }, - { type: 'sleepy' as const, count: 1, active: false }, - { type: 'good' as const, count: 5, active: false }, - { type: 'bad' as const, count: 1, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +// ============================================================================ +// Helper 함수 +// ============================================================================ - // 390초 - good 우세 (총합: 11) - { - timestampMs: 390_000, - comments: [], - reactions: [ - { type: 'fire' as const, count: 2, active: false }, - { type: 'sleepy' as const, count: 0, active: false }, - { type: 'good' as const, count: 8, active: false }, - { type: 'bad' as const, count: 0, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +/** + * 검색어로 영상 필터링 + */ +export function filterVideosBySearch( + videos: VideoListItemDto[], + search: string, +): VideoListItemDto[] { + if (!search) return videos; + const lowerSearch = search.toLowerCase(); + return videos.filter((v) => v.title.toLowerCase().includes(lowerSearch)); +} - // 450초 - fire 우세 (총합: 16) - { - timestampMs: 450_000, - comments: [ - { - commentId: 'vc-450-1', - userId: MOCK_USERS[3].id, - content: '클라이막스 부분이네요!', - createdAt: timeAgo(30, 'minute'), - isMine: false, - ref: { kind: 'video' as const, seconds: 450 }, - }, - ], - reactions: [ - { type: 'fire' as const, count: 9, active: false }, - { type: 'sleepy' as const, count: 0, active: false }, - { type: 'good' as const, count: 5, active: false }, - { type: 'bad' as const, count: 1, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +/** + * 상태로 영상 필터링 + */ +export function filterVideosByStatus( + videos: VideoListItemDto[], + status: string, +): VideoListItemDto[] { + if (!status || status === 'all') return videos; + return videos.filter((v) => v.status === status); +} - // ===== 엔딩부분 (500~596초) ===== - // 520초 - sleepy 우세 (총합: 6) - { - timestampMs: 520_000, - comments: [], - reactions: [ - { type: 'fire' as const, count: 1, active: false }, - { type: 'sleepy' as const, count: 3, active: false }, - { type: 'good' as const, count: 1, active: false }, - { type: 'bad' as const, count: 0, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, +/** + * 정렬 + */ +export function sortVideos(videos: VideoListItemDto[], sort: string): VideoListItemDto[] { + const sorted = [...videos]; - // 560초 - confused 우세 (총합: 113) - 엔딩 크레딧 - { - timestampMs: 560_000, - comments: [ - { - commentId: 'vc-560-1', - userId: MOCK_USERS[4].id, - content: '엔딩 크레딧도 예쁘네요!', - createdAt: timeAgo(35, 'minute'), - isMine: false, - ref: { kind: 'video' as const, seconds: 560 }, - }, - ], - reactions: [ - { type: 'fire' as const, count: 3, active: false }, - { type: 'sleepy' as const, count: 1, active: false }, - { type: 'good' as const, count: 7, active: false }, - { type: 'bad' as const, count: 1, active: false }, - { type: 'confused' as const, count: 100, active: false }, - ], - }, + if (sort === 'oldest') { + return sorted.reverse(); + } - // 590초 - fire 우세 (총합: 22) - 마지막 장면 인기! - { - timestampMs: 590_000, - comments: [ - { - commentId: 'vc-590-1', - userId: MOCK_USERS[0].id, - content: '마무리가 정말 좋았어요!', - createdAt: timeAgo(40, 'minute'), - isMine: true, - ref: { kind: 'video' as const, seconds: 590 }, - }, - ], - reactions: [ - { type: 'fire' as const, count: 12, active: false }, - { type: 'sleepy' as const, count: 0, active: false }, - { type: 'good' as const, count: 8, active: false }, - { type: 'bad' as const, count: 1, active: false }, - { type: 'confused' as const, count: 1, active: false }, - ], - }, - ], -}; + // 기본: 최신순 (이미 최신순으로 정렬되어 있음) + return sorted; +} From 1891c9b920d9668470113d4e2023d6b9107d6e40 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 19:24:57 +0900 Subject: [PATCH 60/81] =?UTF-8?q?fix:=20dto=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFeedbackVideo.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/hooks/useFeedbackVideo.ts diff --git a/src/hooks/useFeedbackVideo.ts b/src/hooks/useFeedbackVideo.ts new file mode 100644 index 00000000..e69de29b From f9c750ad76e5a3a671a51e2db02de2477fdbd634 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 19:35:23 +0900 Subject: [PATCH 61/81] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/presentation/PresentationCard.tsx | 2 -- src/mocks/handlers.ts | 12 ++++-------- src/pages/VideoListPage.tsx | 11 ----------- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/components/presentation/PresentationCard.tsx b/src/components/presentation/PresentationCard.tsx index 15598758..56a06f21 100644 --- a/src/components/presentation/PresentationCard.tsx +++ b/src/components/presentation/PresentationCard.tsx @@ -80,12 +80,10 @@ function PresentationCard(props: Props) { highlightQuery = '', updatedAt, durationSeconds, - // eslint-disable-next-line @typescript-eslint/no-unused-vars slideCount, feedbackCount, thumbnailUrl, mode = 'slide', - ...rest } = props; const navigate = useNavigate(); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 0a047edd..7c020c40 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -885,7 +885,7 @@ const videoHandlers = [ // POST /videos/start - 영상 녹화 세션 생성 http.post(`${BASE_URL}/videos/start`, async ({ request }) => { await delay(200); - const body = (await request.json()) as CreateStartVideoRequestDto; + await request.json(); const videoId = String(Date.now()); return HttpResponse.json>({ @@ -935,9 +935,8 @@ const videoHandlers = [ }), // GET /presentations/:projectId/videos - 프로젝트별 영상 목록 조회 - http.get(`${BASE_URL}/presentations/:projectId/videos`, async ({ params, request }) => { + http.get(`${BASE_URL}/presentations/:projectId/videos`, async ({ request }) => { await delay(200); - const { projectId } = params; const url = new URL(request.url); const search = url.searchParams.get('search') || ''; const filter = url.searchParams.get('filter') || ''; @@ -1022,9 +1021,8 @@ const videoHandlers = [ }), // POST /videos/:videoId/comments - 댓글 작성 - http.post(`${BASE_URL}/videos/:videoId/comments`, async ({ params, request }) => { + http.post(`${BASE_URL}/videos/:videoId/comments`, async ({ request }) => { await delay(200); - const { videoId } = params; const body = (await request.json()) as CreateCommentRequestDto & { timestampMs?: number }; return HttpResponse.json>({ @@ -1059,10 +1057,8 @@ const videoHandlers = [ }), // DELETE /comments/:commentId - 댓글 삭제 - http.delete(`${BASE_URL}/comments/:commentId`, async ({ params }) => { + http.delete(`${BASE_URL}/comments/:commentId`, async () => { await delay(150); - const { commentId } = params; - return HttpResponse.json>({ resultType: 'SUCCESS', success: null, diff --git a/src/pages/VideoListPage.tsx b/src/pages/VideoListPage.tsx index 388d3f4b..84e8c300 100644 --- a/src/pages/VideoListPage.tsx +++ b/src/pages/VideoListPage.tsx @@ -73,17 +73,6 @@ export default function VideoListPage() { const handleStartRecording = () => { navigate(`/${projectId}/video/record`); }; - const handleVideoClick = (video: Video) => { - if (video.status === 'ready') { - // 모달 대신 페이지로 이동 - navigate(`}/video/${video.id}`); - } else if (video.status === 'processing') { - alert('영상이 처리 중입니다. 잠시 후 다시 시도해주세요.'); - } else { - alert('영상 처리에 실패했습니다.'); - } - }; - if (!projectId) { return (
From 4efeb69c6d199543766ffc5a1cc4b9a6c37e60ee Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 19:52:35 +0900 Subject: [PATCH 62/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/presentation/PresentationCard.tsx | 1 + src/mocks/handlers.ts | 1 - src/mocks/videos.ts | 9 ++++++--- src/pages/VideoListPage.tsx | 14 -------------- src/pages/feedback/useFeedbackVideo.ts | 11 +++++++++-- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/components/presentation/PresentationCard.tsx b/src/components/presentation/PresentationCard.tsx index 56a06f21..03668316 100644 --- a/src/components/presentation/PresentationCard.tsx +++ b/src/components/presentation/PresentationCard.tsx @@ -181,6 +181,7 @@ function PresentationCard(props: Props) {
{Math.ceil(durationSeconds / 60)}분 + {slideCount}페이지
diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 7c020c40..22636eb2 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -13,7 +13,6 @@ import type { CreateCommentRequestDto, CreateFinishVideoRequestDto, CreateFinishVideoResponseDto, - CreateStartVideoRequestDto, CreateStartVideoResponseDto, ReadProjectVideosResponseDto, ReadVideoDetailResponseDto, diff --git a/src/mocks/videos.ts b/src/mocks/videos.ts index 97ae2abc..430afc96 100644 --- a/src/mocks/videos.ts +++ b/src/mocks/videos.ts @@ -2,15 +2,18 @@ import type { VideoDetailDto, VideoListItemDto, VideoSlideTimelineItemDto, - VideoTimelineCommentDto, VideoTimelineDto, - VideoTimelineReactionDto, } from '@/api/dto'; // ============================================================================ // Mock 영상 목록 데이터 // ============================================================================ - +export const MOCK_VIDEO = { + videoId: '1', + videoUrl: '/p1.webm', + title: '테스트 영상', + duration: 596, +}; export const mockVideoList: VideoListItemDto[] = [ { videoId: '1', diff --git a/src/pages/VideoListPage.tsx b/src/pages/VideoListPage.tsx index 84e8c300..92c541a4 100644 --- a/src/pages/VideoListPage.tsx +++ b/src/pages/VideoListPage.tsx @@ -11,20 +11,6 @@ import type { FilterMode, SortMode, ViewMode } from '@/types/home'; const SKELETON_CARD_COUNT = 6; const SKELETON_LIST_COUNT = 4; -interface Video { - id: number; - projectId: string; - title: string; - createdAt: string; - durationSeconds: number; - slideCount: number; - size: number; - videoData?: string; - videoUrl?: string; - thumbnailUrl?: string; - status: 'processing' | 'ready' | 'failed'; - durations?: { [key: number]: number }; -} export default function VideoListPage() { const navigate = useNavigate(); diff --git a/src/pages/feedback/useFeedbackVideo.ts b/src/pages/feedback/useFeedbackVideo.ts index 49704a37..cafa3696 100644 --- a/src/pages/feedback/useFeedbackVideo.ts +++ b/src/pages/feedback/useFeedbackVideo.ts @@ -19,6 +19,12 @@ import { formatVideoTimestamp } from '@/utils/format'; */ const TEST_VIDEO_ID = '26'; +// 슬라이드 타입 정의 추가 +interface ProjectSlide { + startTime?: number; + // ... 필요한 다른 필드들 +} + export function useFeedbackVideo() { // projectId는 라우트에서 추출하지만 현재 테스트 모드에서는 사용하지 않음 useParams<{ projectId: string }>(); @@ -39,19 +45,20 @@ export function useFeedbackVideo() { const [commentDraft, setCommentDraft] = useState(''); // TODO: 실제 API로 프로젝트 슬라이드 조회 - const projectSlides = useMemo(() => [], []); + const projectSlides = useMemo(() => [], []); // TODO: 슬라이드 전환 시간 계산 const slideChangeTimes = useMemo(() => { if (projectSlides.length === 0) return []; - const videoDuration = MOCK_VIDEO.duration; // MOCK_VIDEOS → MOCK_VIDEO + const videoDuration = MOCK_VIDEO.duration; const slideCount = projectSlides.length; return projectSlides.map( (slide, i) => slide.startTime ?? Math.floor(i * (videoDuration / slideCount)), ); }, [projectSlides]); + // 타임스탬프 프리픽스 (댓글 입력 시 자동 삽입) const timestampPrefix = useMemo(() => `${formatVideoTimestamp(currentTime)} `, [currentTime]); From 6cc712726b4c4b8a0361e2c2846d7ceae35579ba Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 20:08:44 +0900 Subject: [PATCH 63/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feedback/video/SlideWebcamStage.tsx | 1 + src/components/video/RecordingSection.tsx | 11 +++++++---- src/hooks/useMediaStream.ts | 4 +++- src/hooks/useVideoComments.ts | 4 ++-- src/pages/feedback/useFeedbackVideo.ts | 4 +++- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/feedback/video/SlideWebcamStage.tsx b/src/components/feedback/video/SlideWebcamStage.tsx index 4417594a..0bf9effe 100644 --- a/src/components/feedback/video/SlideWebcamStage.tsx +++ b/src/components/feedback/video/SlideWebcamStage.tsx @@ -7,6 +7,7 @@ * - "작은 박스(PiP)"를 hover하면 디밍+텍스트, 클릭하면 슬라이드/웹캠 위치가 토글됨 * - HLS(.m3u8) 스트리밍 지원 (hls.js) */ +/* eslint-disable no-console */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { ReactNode } from 'react'; diff --git a/src/components/video/RecordingSection.tsx b/src/components/video/RecordingSection.tsx index d318f900..31c17318 100644 --- a/src/components/video/RecordingSection.tsx +++ b/src/components/video/RecordingSection.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import IconArrowLeft from '@/assets/icons/icon-arrow-left.svg?react'; import IconArrowRight from '@/assets/icons/icon-arrow-right.svg?react'; @@ -37,7 +37,10 @@ export const RecordingSection = ({ const { data: presentation } = usePresentation(projectId); const { data: slidesData } = useSlides(projectId); - const slidesList = (slidesData || []) as SlideDetail[]; + const slidesList = useMemo( + () => slidesData?.map((slide) => ({ id: slide.slideId, url: slide.imageUrl })) ?? [], + [slidesData], + ); const totalPages = slidesList.length > 0 ? slidesList.length : 1; const [currentPage, setCurrentPage] = useState(1); @@ -54,12 +57,12 @@ export const RecordingSection = ({ const getSlideImgUrl = useCallback( (p: number) => { const slide = slidesList[p - 1]; - return slide ? slide.imageUrl : ''; + return slide ? slide.url : ''; }, [slidesList], ); - const currentSlideId = slidesList[currentPage - 1]?.slideId; + const currentSlideId = slidesList[currentPage - 1]?.id; const { data: scriptData } = useScript(currentSlideId ?? ''); useEffect(() => { diff --git a/src/hooks/useMediaStream.ts b/src/hooks/useMediaStream.ts index 9ea1831e..2bcb5bb8 100644 --- a/src/hooks/useMediaStream.ts +++ b/src/hooks/useMediaStream.ts @@ -91,7 +91,9 @@ export const useMediaStream = (videoDeviceId?: string, audioDeviceId?: string) = updateVolume(); } catch (err) { - console.error('오디오 분석 설정 실패:', err); + if (import.meta.env.DEV) { + console.error('오디오 분석 설정 실패:', err); + } } }, [cleanupAudioAnalysis], diff --git a/src/hooks/useVideoComments.ts b/src/hooks/useVideoComments.ts index f73f80f9..2ba6a4d9 100644 --- a/src/hooks/useVideoComments.ts +++ b/src/hooks/useVideoComments.ts @@ -115,7 +115,7 @@ export function useVideoComments() { if (model && tempReply) { updateCommentServerId(tempReply.commentId, model.serverId); } - } catch (_error) { + } catch { showToast.error('답글 등록에 실패했습니다.', '잠시 후 다시 시도해주세요.'); } }; @@ -153,7 +153,7 @@ export function useVideoComments() { await deleteVideoComment(targetComment.serverId); showToast.success('댓글이 삭제되었습니다.'); - } catch (_error) { + } catch { showToast.error('댓글 삭제에 실패했습니다.', '잠시 후 다시 시도해주세요.'); } }; diff --git a/src/pages/feedback/useFeedbackVideo.ts b/src/pages/feedback/useFeedbackVideo.ts index cafa3696..2c7dddcd 100644 --- a/src/pages/feedback/useFeedbackVideo.ts +++ b/src/pages/feedback/useFeedbackVideo.ts @@ -119,7 +119,9 @@ export function useFeedbackVideo() { initVideo(videoData); } catch (error) { - console.error('[useFeedbackVideo] 비디오 로드 실패, 폴백 사용:', error); + if (import.meta.env.DEV) { + console.error('[useFeedbackVideo] 비디오 로드 실패, 폴백 사용:', error); + } if (cancelled) return; // 서버 요청 실패 시 폴백: videoId만 실제 값 사용 From 214f1310451bd8e9caf61a17bcfd1edaa8ecf3fe Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 20:23:16 +0900 Subject: [PATCH 64/81] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.ts | 2 +- src/api/dto/auth.dto.ts | 11 ++++- src/api/dto/index.ts | 1 + src/api/endpoints/auth.ts | 71 +-------------------------------- src/mocks/handlers.ts | 36 ++++++++++++----- src/pages/OAuthCallbackPage.tsx | 30 +++++++------- src/stores/authStore.ts | 6 +-- src/types/auth.ts | 2 +- src/utils/jwt.ts | 23 +++++++++++ 9 files changed, 80 insertions(+), 102 deletions(-) create mode 100644 src/utils/jwt.ts diff --git a/src/api/client.ts b/src/api/client.ts index 2fdbd8ef..30390485 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -40,7 +40,7 @@ export const apiClient = axios.create({ timeout: API_TIMEOUT_MS, // 쿠키 설정 - withCredentials: false, + withCredentials: true, headers: { 'Content-Type': 'application/json', diff --git a/src/api/dto/auth.dto.ts b/src/api/dto/auth.dto.ts index abfe776c..e5e34b29 100644 --- a/src/api/dto/auth.dto.ts +++ b/src/api/dto/auth.dto.ts @@ -14,10 +14,10 @@ export interface SocialLoginUserResponseDto { /** * 소셜 로그인 성공 응답의 토큰 정보 + * refreshToken은 HttpOnly 쿠키로 전달되므로 응답 body에 포함되지 않음 */ export interface SocialLoginTokensResponseDto { accessToken: string; - refreshToken: string; } /** @@ -29,4 +29,13 @@ export interface SocialLoginSuccessResponseDto { tokens: SocialLoginTokensResponseDto; } +/** + * JWT payload 타입 (서버 JWT에서 디코딩) + */ +export interface JwtPayloadDto { + id: string; + email: string; + sessionId: string; +} + //위 형식대로 response, request dto 작성 부탁드립니다. diff --git a/src/api/dto/index.ts b/src/api/dto/index.ts index d5cc323c..fed56534 100644 --- a/src/api/dto/index.ts +++ b/src/api/dto/index.ts @@ -4,6 +4,7 @@ */ export type { + JwtPayloadDto, SocialLoginSuccessResponseDto, SocialLoginTokensResponseDto, SocialLoginUserResponseDto, diff --git a/src/api/endpoints/auth.ts b/src/api/endpoints/auth.ts index bad0cfce..3375e10a 100644 --- a/src/api/endpoints/auth.ts +++ b/src/api/endpoints/auth.ts @@ -1,74 +1,7 @@ /** * @file auth.ts * @description 인증 관련 API 엔드포인트 - */ -import { apiClient } from '@/api/client'; -import type { SocialLoginSuccessResponseDto } from '@/api/dto'; -import type { ApiResponse } from '@/types/api'; - -/** - * Google OAuth 콜백 처리 - * - * @param code - OAuth 인증 코드 - * @returns 로그인 성공 정보 (user, tokens) - * - * @example - * const result = await getGoogleCallback('authorization-code'); - */ -export async function getGoogleCallback(code: string): Promise { - const response = await apiClient.get>( - `/auth/google/callback`, - { params: { code } }, - ); - - if (response.data.resultType === 'SUCCESS') { - return response.data.success; - } - throw new Error(response.data.error.reason); -} - -/** - * Kakao OAuth 콜백 처리 - * - * @param code - OAuth 인증 코드 - * @returns 로그인 성공 정보 (user, tokens) - * - * @example - * const result = await getKakaoCallback('authorization-code'); - */ -export async function getKakaoCallback(code: string): Promise { - const response = await apiClient.get>( - `/auth/kakao/callback`, - { - params: { code }, - }, - ); - - if (response.data.resultType === 'SUCCESS') { - return response.data.success; - } - throw new Error(response.data.error.reason); -} - -/** - * Naver OAuth 콜백 처리 - * - * @param code - OAuth 인증 코드 - * @returns 로그인 성공 정보 (user, tokens) * - * @example - * const result = await getNaverCallback('authorization-code'); + * OAuth 콜백은 서버가 리다이렉트로 처리하므로 클라이언트 API 호출 불필요. + * OAuthCallbackPage에서 URL 파라미터 + JWT 디코딩으로 처리합니다. */ -export async function getNaverCallback(code: string): Promise { - const response = await apiClient.get>( - `/auth/naver/callback`, - { - params: { code }, - }, - ); - - if (response.data.resultType === 'SUCCESS') { - return response.data.success; - } - throw new Error(response.data.error.reason); -} diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 592c55ea..2fcedeca 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -128,6 +128,15 @@ slides.forEach((s) => { // AUTH — /auth/:provider/callback, /auth/logout, /users/:userId // ═══════════════════════════════════════════════════════════════ +/** + * Mock JWT 생성 (payload만 유효, 서명은 더미) + */ +function createMockJwt(payload: Record): string { + const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); + const body = btoa(JSON.stringify(payload)); + return `${header}.${body}.mock-signature`; +} + const authCallbackHandler = async ({ request }: { request: Request }) => { await delay(200); const url = new URL(request.url); @@ -135,17 +144,22 @@ const authCallbackHandler = async ({ request }: { request: Request }) => { if (!code) return fail(400, 'A001', '인증 코드가 없습니다.'); const user = MOCK_CURRENT_USER; - return ok({ - message: '소셜 로그인 성공!', - user: { - id: user.id, - email: user.email, - name: user.name, - sessionId: user.sessionId, - }, - tokens: { - accessToken: `mock-access-token-${user.id}`, - refreshToken: `mock-refresh-token-${user.id}`, + const accessToken = createMockJwt({ + id: user.id, + email: user.email, + sessionId: user.sessionId, + }); + + // 새 플로우: accessToken + sessionId를 URL 파라미터로, refreshToken은 HttpOnly 쿠키로 전달 + const callbackUrl = new URL('/auth/callback', url.origin); + callbackUrl.searchParams.set('accessToken', accessToken); + callbackUrl.searchParams.set('sessionId', user.sessionId); + + return new HttpResponse(null, { + status: 302, + headers: { + Location: callbackUrl.toString(), + 'Set-Cookie': `refreshToken=mock-refresh-token-${user.id}; HttpOnly; Path=/; SameSite=Lax`, }, }); }; diff --git a/src/pages/OAuthCallbackPage.tsx b/src/pages/OAuthCallbackPage.tsx index 1cf1efce..3809cad8 100644 --- a/src/pages/OAuthCallbackPage.tsx +++ b/src/pages/OAuthCallbackPage.tsx @@ -1,9 +1,10 @@ import { useEffect, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import type { JwtPayloadDto } from '@/api/dto'; import { Spinner } from '@/components/common'; import { useAuthStore } from '@/stores/authStore'; -import type { AuthProvider } from '@/types/auth'; +import { parseJwtPayload } from '@/utils/jwt'; export default function OAuthCallbackPage() { const [searchParams] = useSearchParams(); @@ -12,17 +13,11 @@ export default function OAuthCallbackPage() { const isProcessing = useRef(false); useEffect(() => { - // 백엔드에서 리다이렉트 시 전달받은 토큰들 const accessToken = searchParams.get('accessToken'); - const refreshToken = searchParams.get('refreshToken'); - const userId = searchParams.get('userId'); - const userName = searchParams.get('userName'); - const userEmail = searchParams.get('userEmail'); - const sessionId = searchParams.get('sessionId'); - const provider = searchParams.get('provider') as AuthProvider | null; + const sessionIdParam = searchParams.get('sessionId'); - // 필수 파라미터가 없으면 홈으로 - if (!accessToken || !refreshToken || !userId) { + // 필수 파라미터가 없으면 팝업 닫기 또는 홈으로 + if (!accessToken) { if (window.opener) { window.close(); } else { @@ -34,17 +29,20 @@ export default function OAuthCallbackPage() { if (isProcessing.current) return; isProcessing.current = true; - // 로그인 처리 + // JWT 디코딩으로 유저 정보 추출 + const payload = parseJwtPayload(accessToken); + const userId = payload?.id ?? ''; + const userEmail = payload?.email ?? ''; + const sessionId = sessionIdParam ?? payload?.sessionId ?? ''; + login( { id: userId, - email: userEmail || '', - name: userName || '', - sessionId: sessionId || '', - provider: provider || 'google', + email: userEmail, + name: userEmail.split('@')[0] || userEmail, + sessionId, }, accessToken, - refreshToken, ); closeLoginModal(); diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 2d6067ab..edee548c 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -15,7 +15,7 @@ interface AuthState { refreshToken: string | null; isLoginModalOpen: boolean; - login: (user: User, accessToken: string, refreshToken: string) => void; + login: (user: User, accessToken: string) => void; anonymous: (accessToken: string, refreshToken: string) => void; logout: () => void; updateUser: (user: Partial) => void; @@ -32,12 +32,12 @@ export const useAuthStore = create()( refreshToken: null, isLoginModalOpen: false, - login: (user, accessToken, refreshToken) => { + login: (user, accessToken) => { set( { user, accessToken, - refreshToken, + refreshToken: null, }, false, 'auth/login', diff --git a/src/types/auth.ts b/src/types/auth.ts index b293ba81..b504f1ca 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -2,7 +2,7 @@ export type AuthProvider = 'google' | 'kakao' | 'naver'; export interface User { id: string; - name: string; + name?: string; email: string; sessionId: string; provider?: AuthProvider; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 00000000..e85c9886 --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,23 @@ +/** + * JWT payload base64 디코딩 유틸리티 + * + * 서명 검증 불필요 (서버가 검증함), payload만 추출합니다. + */ +export function parseJwtPayload(token: string): T | null { + try { + const base64Url = token.split('.')[1]; + if (!base64Url) return null; + + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const json = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')) + .join(''), + ); + + return JSON.parse(json) as T; + } catch { + return null; + } +} From 655b320bada50c0e4ed5cfd81cf2b7276690ebb4 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 20:30:44 +0900 Subject: [PATCH 65/81] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/endpoints/auth.ts | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/api/endpoints/auth.ts diff --git a/src/api/endpoints/auth.ts b/src/api/endpoints/auth.ts deleted file mode 100644 index 3375e10a..00000000 --- a/src/api/endpoints/auth.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @file auth.ts - * @description 인증 관련 API 엔드포인트 - * - * OAuth 콜백은 서버가 리다이렉트로 처리하므로 클라이언트 API 호출 불필요. - * OAuthCallbackPage에서 URL 파라미터 + JWT 디코딩으로 처리합니다. - */ From 22882e729340c9657a4e2fdd9c50e4e38d1858c3 Mon Sep 17 00:00:00 2001 From: sandy Date: Mon, 9 Feb 2026 20:38:43 +0900 Subject: [PATCH 66/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInsightPageModel.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/hooks/useInsightPageModel.ts b/src/hooks/useInsightPageModel.ts index a8c2984d..524d539f 100644 --- a/src/hooks/useInsightPageModel.ts +++ b/src/hooks/useInsightPageModel.ts @@ -2,6 +2,12 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import type { + SlideAnalyticsDto, + SlideRetentionDto, + VideoExitAnalyticsDto, + VideoRetentionDto, +} from '@/api/dto/analytics.dto'; import type { ChartDataPoint, InsightModel, InsightTopSlide } from '@/components/insight/types'; import { useSlideReactionSummaries } from '@/hooks/queries/useReaction.ts'; import { useSlides } from '@/hooks/queries/useSlides'; @@ -12,7 +18,7 @@ import { useSlideRetention, useVideoAnalytics, useVideoRetention, -} from '@/hooks/useAnalytics'; +} from '@/hooks/useAnalytics.ts'; import type { DropOffSlide, DropOffTime, SummaryStat } from '@/types/insight'; import type { SlideListItem } from '@/types/slide'; import { formatVideoTimestamp } from '@/utils/format'; @@ -92,7 +98,7 @@ export function useInsightPageModel(): InsightModel { // ---- Top slides ---- const topSlides = useMemo(() => { - const analyticsSlides = slideAnalytics?.slides ?? []; + const analyticsSlides: SlideAnalyticsDto[] = slideAnalytics?.slides ?? []; if (!analyticsSlides.length) return []; const { slideIndexById, slideById } = slideDataMaps; @@ -131,7 +137,7 @@ export function useInsightPageModel(): InsightModel { }, [slides]); const dropOffSlides: DropOffSlide[] = useMemo(() => { - const items = slideAnalytics?.slides ?? []; + const items: SlideAnalyticsDto[] = slideAnalytics?.slides ?? []; return items .slice() .sort((a, b) => b.exitCount - a.exitCount) @@ -155,7 +161,7 @@ export function useInsightPageModel(): InsightModel { }, [slideAnalytics]); const dropOffTimes: DropOffTime[] = useMemo(() => { - const items = videoExitAnalytics?.exits ?? []; + const items: VideoExitAnalyticsDto[] = videoExitAnalytics?.exits ?? []; return items .slice() .sort((a, b) => b.exitCount - a.exitCount) @@ -182,7 +188,7 @@ export function useInsightPageModel(): InsightModel { const videoChartData = useMemo(() => { if (!videoRetentionRes?.videoRetention) return []; - return videoRetentionRes.videoRetention.map((item) => ({ + return videoRetentionRes.videoRetention.map((item: VideoRetentionDto) => ({ label: formatVideoTimestamp(item.timestampMs / 1000), // x축: 00:00 value: Math.round(normalizeRate(item.retentionRate)), // y축: 0~100% tooltipTitle: formatVideoTimestamp(item.timestampMs / 1000), @@ -193,7 +199,7 @@ export function useInsightPageModel(): InsightModel { const slideChartData = useMemo(() => { if (!slideRetentionRes?.slideRetention) return []; - return slideRetentionRes.slideRetention.map((item) => ({ + return slideRetentionRes.slideRetention.map((item: SlideRetentionDto) => ({ label: `S${item.slideNum}`, // x축: S1, S2 value: Math.round(normalizeRate(item.retentionRate)), tooltipTitle: item.title || `슬라이드 ${item.slideNum}`, // 툴팁: 제목 From fddb03a5d5b1443518c887d0a24b0aaf5b684ff9 Mon Sep 17 00:00:00 2001 From: sandy Date: Mon, 9 Feb 2026 20:45:39 +0900 Subject: [PATCH 67/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=9E=AC=EC=88=98=EC=A0=95=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInsightPageModel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useInsightPageModel.ts b/src/hooks/useInsightPageModel.ts index 524d539f..b7db19ba 100644 --- a/src/hooks/useInsightPageModel.ts +++ b/src/hooks/useInsightPageModel.ts @@ -9,8 +9,6 @@ import type { VideoRetentionDto, } from '@/api/dto/analytics.dto'; import type { ChartDataPoint, InsightModel, InsightTopSlide } from '@/components/insight/types'; -import { useSlideReactionSummaries } from '@/hooks/queries/useReaction.ts'; -import { useSlides } from '@/hooks/queries/useSlides'; import { useProjectAnalyticsSummary, useRecentComments, @@ -18,7 +16,9 @@ import { useSlideRetention, useVideoAnalytics, useVideoRetention, -} from '@/hooks/useAnalytics.ts'; +} from '@/hooks/queries/useAnalytics'; +import { useSlideReactionSummaries } from '@/hooks/queries/useReaction.ts'; +import { useSlides } from '@/hooks/queries/useSlides'; import type { DropOffSlide, DropOffTime, SummaryStat } from '@/types/insight'; import type { SlideListItem } from '@/types/slide'; import { formatVideoTimestamp } from '@/utils/format'; From 5fe1c9cc51605dd7c6e191d4be42421b04324369 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 20:47:54 +0900 Subject: [PATCH 68/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feedback/ScriptSection.tsx | 4 ++-- src/components/video/RecordingSection.tsx | 1 - src/pages/FeedbackVideoPage.tsx | 1 + src/pages/feedback/useFeedbackVideo.ts | 9 ++------ src/pages/index.ts | 2 +- src/router/Router.tsx | 26 ++++++++++------------- src/types/slide.ts | 3 +-- 7 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/components/feedback/ScriptSection.tsx b/src/components/feedback/ScriptSection.tsx index f2d9da10..25172aef 100644 --- a/src/components/feedback/ScriptSection.tsx +++ b/src/components/feedback/ScriptSection.tsx @@ -8,12 +8,12 @@ import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react'; import { Skeleton } from '@/components/common'; -import type { SlideDetail } from '@/types/slide'; +import type { SlideListItem } from '@/types'; import { formatVideoTimestamp } from '@/utils/format'; import { getSlideIndexFromTime } from '@/utils/video'; interface ScriptSectionProps { - slides: SlideDetail[]; + slides: SlideListItem[]; slideChangeTimes: number[]; currentTime: number; onSeek?: (time: number) => void; diff --git a/src/components/video/RecordingSection.tsx b/src/components/video/RecordingSection.tsx index 31c17318..00b933ea 100644 --- a/src/components/video/RecordingSection.tsx +++ b/src/components/video/RecordingSection.tsx @@ -6,7 +6,6 @@ import { Logo, SlideImage } from '@/components/common'; import { usePresentation } from '@/hooks/queries/usePresentations'; import { useScript } from '@/hooks/queries/useScript'; import { useSlides } from '@/hooks/queries/useSlides'; -import type { SlideDetail } from '@/types/slide'; import { useRecorder } from '../../hooks/useRecorder'; import StopButton from './StopButton'; diff --git a/src/pages/FeedbackVideoPage.tsx b/src/pages/FeedbackVideoPage.tsx index 8b325b22..962b6179 100644 --- a/src/pages/FeedbackVideoPage.tsx +++ b/src/pages/FeedbackVideoPage.tsx @@ -26,6 +26,7 @@ export default function FeedbackVideoPage() { const isDesktop = useIsDesktop(); const ctx = useFeedbackVideo(); const videoId = useVideoFeedbackStore((s) => s.video?.videoId); + const { isLoading, currentTime, diff --git a/src/pages/feedback/useFeedbackVideo.ts b/src/pages/feedback/useFeedbackVideo.ts index 2c7dddcd..3b62495f 100644 --- a/src/pages/feedback/useFeedbackVideo.ts +++ b/src/pages/feedback/useFeedbackVideo.ts @@ -10,6 +10,7 @@ import { useVideoComments } from '@/hooks/useVideoComments'; import { useVideoReactions } from '@/hooks/useVideoReactions'; import { MOCK_VIDEO } from '@/mocks/videos'; import { useVideoFeedbackStore } from '@/stores/videoFeedbackStore'; +import type { SlideListItem } from '@/types'; import type { Comment } from '@/types/comment'; import { formatVideoTimestamp } from '@/utils/format'; @@ -19,12 +20,6 @@ import { formatVideoTimestamp } from '@/utils/format'; */ const TEST_VIDEO_ID = '26'; -// 슬라이드 타입 정의 추가 -interface ProjectSlide { - startTime?: number; - // ... 필요한 다른 필드들 -} - export function useFeedbackVideo() { // projectId는 라우트에서 추출하지만 현재 테스트 모드에서는 사용하지 않음 useParams<{ projectId: string }>(); @@ -45,7 +40,7 @@ export function useFeedbackVideo() { const [commentDraft, setCommentDraft] = useState(''); // TODO: 실제 API로 프로젝트 슬라이드 조회 - const projectSlides = useMemo(() => [], []); + const projectSlides = useMemo(() => [], []); // TODO: 슬라이드 전환 시간 계산 const slideChangeTimes = useMemo(() => { diff --git a/src/pages/index.ts b/src/pages/index.ts index b2c4944e..91286c84 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -4,7 +4,7 @@ export { default as OAuthCallbackPage } from './OAuthCallbackPage'; export { default as InsightPage } from './InsightPage'; export { default as SlidePage } from './SlidePage'; export { default as FdSlidePage } from './FeedbackSlidePage'; -export { default as FdVideoPage } from './FeedbackVideoPage'; +export { default as FeedbackVideoPage } from './FeedbackVideoPage'; export { default as VideoRecordPage } from './VideoRecordPage'; export { default as VideoListPage } from './VideoListPage'; //export { default as VideoDetailPage } from './VideoDetailPage'; diff --git a/src/router/Router.tsx b/src/router/Router.tsx index cb99db16..7260a5ef 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -8,20 +8,18 @@ import { PresentationTitleEditor, ShareButton, } from '@/components/common'; -// TODO: 컴포넌트 교체 import FeedbackHeaderCenter from '@/components/feedback/FeedbackHeaderCenter'; import FeedbackHeaderLeft from '@/components/feedback/FeedbackHeaderLeft'; import { DevTestPage, FdSlidePage, - FdVideoPage, + FeedbackVideoPage, HomePage, InsightPage, OAuthCallbackPage, SlidePage, VideoListPage, VideoRecordPage, - // VideoDetailPage } from '@/pages'; export const router = createBrowserRouter([ @@ -67,23 +65,21 @@ export const router = createBrowserRouter([ { path: '/feedback/slide/:projectId', element: ( - } center={} /> + } center={}> + + ), - children: [{ index: true, element: }], - }, - // { - // path: '/video/:videoId', - // element: , - // }, - { - path: '/:projectId/video/record', - element: , }, { path: '/feedback/video/:projectId', element: ( - } center={} /> + } center={}> + + ), - children: [{ index: true, element: }], + }, + { + path: '/:projectId/video/record', + element: , }, ]); diff --git a/src/types/slide.ts b/src/types/slide.ts index 213bacc6..d8156640 100644 --- a/src/types/slide.ts +++ b/src/types/slide.ts @@ -7,6 +7,7 @@ import type { Reaction } from './script'; * API 응답 타입: 슬라이드 목록 조회 */ export interface SlideListItem { + script: string; slideId: string; projectId: string; title: string; @@ -22,8 +23,6 @@ export interface SlideListItem { * 프론트엔드 확장 타입: 슬라이드 상세/편집 화면용 */ export interface SlideDetail extends SlideListItem { - /** 대본 */ - script?: string; /** 댓글 목록 */ comments?: Comment[]; /** 수정 기록 */ From 636405c6a45f95650954d4d2c474a9037496037c Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 21:00:17 +0900 Subject: [PATCH 69/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=9E=AC=EC=88=98=EC=A0=95(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 5863 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 5863 insertions(+) create mode 100644 pnpm-lock.yaml diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..29c42774 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5863 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + '@tanstack/react-query': + specifier: ^5.90.16 + version: 5.90.20(react@19.2.4) + '@tanstack/react-query-devtools': + specifier: ^5.91.2 + version: 5.91.2(@tanstack/react-query@5.90.20(react@19.2.4))(react@19.2.4) + axios: + specifier: ^1.13.2 + version: 1.13.3 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dayjs: + specifier: ^1.11.19 + version: 1.11.19 + hls.js: + specifier: ^1.6.15 + version: 1.6.15 + react: + specifier: ^19.2.0 + version: 19.2.4 + react-dom: + specifier: ^19.2.0 + version: 19.2.4(react@19.2.4) + react-router-dom: + specifier: ^7.12.0 + version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + recharts: + specifier: ^3.7.0 + version: 3.7.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1) + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + superjson: + specifier: ^2.2.6 + version: 2.2.6 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + zustand: + specifier: ^5.0.9 + version: 5.0.10(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + devDependencies: + '@commitlint/cli': + specifier: ^20.2.0 + version: 20.3.1(@types/node@24.10.9)(typescript@5.9.3) + '@commitlint/config-conventional': + specifier: ^20.2.0 + version: 20.3.1 + '@trivago/prettier-plugin-sort-imports': + specifier: ^6.0.0 + version: 6.0.2(prettier@3.7.4) + '@types/node': + specifier: ^24.10.1 + version: 24.10.9 + '@types/react': + specifier: ^19.2.5 + version: 19.2.10 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.10) + '@typescript-eslint/eslint-plugin': + specifier: ^8.50.1 + version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.50.1 + version: 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@vitejs/plugin-react-swc': + specifier: ^4.2.2 + version: 4.2.2(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + cross-env: + specifier: ^10.1.0 + version: 10.1.0 + eslint: + specifier: ^8 + version: 8.57.1 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@8.57.1) + eslint-plugin-check-file: + specifier: ^2.8.0 + version: 2.8.0(eslint@8.57.1) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.0.1(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: ^0.4.26 + version: 0.4.26(eslint@8.57.1) + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.2.7 + version: 16.2.7 + msw: + specifier: ^2.12.7 + version: 2.12.7(@types/node@24.10.9)(typescript@5.9.3) + prettier: + specifier: 3.7.4 + version: 3.7.4 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.4 + version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite-plugin-svgr: + specifier: ^4.5.0 + version: 4.5.0(rollup@4.57.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + +packages: + + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.6': + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.6': + resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.6': + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.6': + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@commitlint/cli@20.3.1': + resolution: {integrity: sha512-NtInjSlyev/+SLPvx/ulz8hRE25Wf5S9dLNDcIwazq0JyB4/w1ROF/5nV0ObPTX8YpRaKYeKtXDYWqumBNHWsw==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@20.3.1': + resolution: {integrity: sha512-NCzwvxepstBZbmVXsvg49s+shCxlJDJPWxXqONVcAtJH9wWrOlkMQw/zyl+dJmt8lyVopt5mwQ3mR5M2N2rUWg==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@20.3.1': + resolution: {integrity: sha512-ErVLC/IsHhcvxCyh+FXo7jy12/nkQySjWXYgCoQbZLkFp4hysov8KS6CdxBB0cWjbZWjvNOKBMNoUVqkmGmahw==} + engines: {node: '>=v18'} + + '@commitlint/ensure@20.3.1': + resolution: {integrity: sha512-h664FngOEd7bHAm0j8MEKq+qm2mH+V+hwJiIE2bWcw3pzJMlO0TPKtk0ATyRAtV6jQw+xviRYiIjjSjfajiB5w==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@20.0.0': + resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} + engines: {node: '>=v18'} + + '@commitlint/format@20.3.1': + resolution: {integrity: sha512-jfsjGPFTd2Yti2YHwUH4SPRPbWKAJAwrfa3eNa9bXEdrXBb9mCwbIrgYX38LdEJK9zLJ3AsLBP4/FLEtxyu2AA==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@20.3.1': + resolution: {integrity: sha512-tWwAoh93QvAhxgp99CzCuHD86MgxE4NBtloKX+XxQxhfhSwHo7eloiar/yzx53YW9eqSLP95zgW2KDDk4/WX+A==} + engines: {node: '>=v18'} + + '@commitlint/lint@20.3.1': + resolution: {integrity: sha512-LaOtrQ24+6SfUaWg8A+a+Wc77bvLbO5RIr6iy9F7CI3/0iq1uPEWgGRCwqWTuLGHkZDAcwaq0gZ01zpwZ1jCGw==} + engines: {node: '>=v18'} + + '@commitlint/load@20.3.1': + resolution: {integrity: sha512-YDD9XA2XhgYgbjju8itZ/weIvOOobApDqwlPYCX5NLO/cPtw2UMO5Cmn44Ks8RQULUVI5fUT6roKvyxcoLbNmw==} + engines: {node: '>=v18'} + + '@commitlint/message@20.0.0': + resolution: {integrity: sha512-gLX4YmKnZqSwkmSB9OckQUrI5VyXEYiv3J5JKZRxIp8jOQsWjZgHSG/OgEfMQBK9ibdclEdAyIPYggwXoFGXjQ==} + engines: {node: '>=v18'} + + '@commitlint/parse@20.3.1': + resolution: {integrity: sha512-TuUTdbLpyUNLgDzLDYlI2BeTE6V/COZbf3f8WwsV0K6eq/2nSpNTMw7wHtXb+YxeY9wwxBp/Ldad4P+YIxHJoA==} + engines: {node: '>=v18'} + + '@commitlint/read@20.3.1': + resolution: {integrity: sha512-nCmJAdIg3OdNVUpQW0Idk/eF/vfOo2W2xzmvRmNeptLrzFK7qhwwl/kIwy1Q1LZrKHUFNj7PGNpIT5INbgZWzA==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@20.3.1': + resolution: {integrity: sha512-iGTGeyaoDyHDEZNjD8rKeosjSNs8zYanmuowY4ful7kFI0dnY4b5QilVYaFQJ6IM27S57LAeH5sKSsOHy4bw5w==} + engines: {node: '>=v18'} + + '@commitlint/rules@20.3.1': + resolution: {integrity: sha512-/uic4P+4jVNpqQxz02+Y6vvIC0A2J899DBztA1j6q3f3MOKwydlNrojSh0dQmGDxxT1bXByiRtDhgFnOFnM6Pg==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@20.0.0': + resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} + engines: {node: '>=v18'} + + '@commitlint/top-level@20.0.0': + resolution: {integrity: sha512-drXaPSP2EcopukrUXvUXmsQMu3Ey/FuJDc/5oiW4heoCfoE5BdLQyuc7veGeE3aoQaTVqZnh4D5WTWe2vefYKg==} + engines: {node: '>=v18'} + + '@commitlint/types@20.3.1': + resolution: {integrity: sha512-VmIFV/JkBRhDRRv7N5B7zEUkNZIx9Mp+8Pe65erz0rKycXLsi8Epcw0XJ+btSeRXgTzE7DyOyA9bkJ9mn/yqVQ==} + engines: {node: '>=v18'} + + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mswjs/interceptors@0.40.0': + resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + engines: {node: '>=18'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@rolldown/pluginutils@1.0.0-beta.47': + resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.57.0': + resolution: {integrity: sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.0': + resolution: {integrity: sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.0': + resolution: {integrity: sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.0': + resolution: {integrity: sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.0': + resolution: {integrity: sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.0': + resolution: {integrity: sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.0': + resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.57.0': + resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.57.0': + resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.57.0': + resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.57.0': + resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.57.0': + resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.57.0': + resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.57.0': + resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.57.0': + resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.57.0': + resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.57.0': + resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.57.0': + resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.57.0': + resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.57.0': + resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.0': + resolution: {integrity: sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.0': + resolution: {integrity: sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.0': + resolution: {integrity: sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.0': + resolution: {integrity: sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.0': + resolution: {integrity: sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==} + cpu: [x64] + os: [win32] + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0': + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0': + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0': + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0': + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-preset@8.1.0': + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/core@8.1.0': + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + + '@svgr/hast-util-to-babel-ast@8.0.0': + resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} + engines: {node: '>=14'} + + '@svgr/plugin-jsx@8.1.0': + resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + + '@swc/core-darwin-arm64@1.15.10': + resolution: {integrity: sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.10': + resolution: {integrity: sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.10': + resolution: {integrity: sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.10': + resolution: {integrity: sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.10': + resolution: {integrity: sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-x64-gnu@1.15.10': + resolution: {integrity: sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.10': + resolution: {integrity: sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.10': + resolution: {integrity: sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.10': + resolution: {integrity: sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.10': + resolution: {integrity: sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.10': + resolution: {integrity: sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} + + '@tanstack/query-devtools@5.92.0': + resolution: {integrity: sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==} + + '@tanstack/react-query-devtools@5.91.2': + resolution: {integrity: sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==} + peerDependencies: + '@tanstack/react-query': ^5.90.14 + react: ^18 || ^19 + + '@tanstack/react-query@5.90.20': + resolution: {integrity: sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==} + peerDependencies: + react: ^18 || ^19 + + '@trivago/prettier-plugin-sort-imports@6.0.2': + resolution: {integrity: sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==} + engines: {node: '>= 20'} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + prettier-plugin-ember-template-tag: '>= 2.0.0' + prettier-plugin-svelte: 3.x + svelte: 4.x || 5.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + prettier-plugin-ember-template-tag: + optional: true + prettier-plugin-svelte: + optional: true + svelte: + optional: true + + '@types/conventional-commits-parser@5.0.2': + resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@24.10.9': + resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.10': + resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@typescript-eslint/eslint-plugin@8.54.0': + resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.54.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.54.0': + resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.54.0': + resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.54.0': + resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.54.0': + resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.54.0': + resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.54.0': + resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.54.0': + resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.54.0': + resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.54.0': + resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-react-swc@4.2.2': + resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@7.2.0: + resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.13.3: + resolution: {integrity: sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.18: + resolution: {integrity: sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==} + hasBin: true + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.1.1: + resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} + engines: {node: '>=20'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + cosmiconfig-typescript-loader@6.2.0: + resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.279: + resolution: {integrity: sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es-toolkit@1.44.0: + resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-check-file@2.8.0: + resolution: {integrity: sha512-FvvafMTam2WJYH9uj+FuMxQ1y+7jY3Z6P9T4j2214cH0FBxNzTcmeCiGTj1Lxp3mI6kbbgsXvmgewvf+llKYyw==} + engines: {node: '>=18'} + peerDependencies: + eslint: '>=7.28.0' + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hls.js@1.6.15: + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.3: + resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@16.2.7: + resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.12.7: + resolution: {integrity: sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nano-spawn@2.0.0: + resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} + engines: {node: '>=20.17'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-router-dom@7.13.0: + resolution: {integrity: sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.13.0: + resolution: {integrity: sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + recharts@3.7.0: + resolution: {integrity: sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rettime@0.7.0: + resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.57.0: + resolution: {integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} + engines: {node: '>=10.0.0'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.1.0: + resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} + engines: {node: '>=20'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@5.4.1: + resolution: {integrity: sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==} + engines: {node: '>=20'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + + vite-plugin-svgr@4.5.0: + resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==} + peerDependencies: + vite: '>=2.6.0' + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + 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 + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zustand@5.0.10: + resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.6': {} + + '@babel/core@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.6': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/traverse@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@commitlint/cli@20.3.1(@types/node@24.10.9)(typescript@5.9.3)': + dependencies: + '@commitlint/format': 20.3.1 + '@commitlint/lint': 20.3.1 + '@commitlint/load': 20.3.1(@types/node@24.10.9)(typescript@5.9.3) + '@commitlint/read': 20.3.1 + '@commitlint/types': 20.3.1 + tinyexec: 1.0.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@20.3.1': + dependencies: + '@commitlint/types': 20.3.1 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@20.3.1': + dependencies: + '@commitlint/types': 20.3.1 + ajv: 8.17.1 + + '@commitlint/ensure@20.3.1': + dependencies: + '@commitlint/types': 20.3.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@20.0.0': {} + + '@commitlint/format@20.3.1': + dependencies: + '@commitlint/types': 20.3.1 + chalk: 5.6.2 + + '@commitlint/is-ignored@20.3.1': + dependencies: + '@commitlint/types': 20.3.1 + semver: 7.7.3 + + '@commitlint/lint@20.3.1': + dependencies: + '@commitlint/is-ignored': 20.3.1 + '@commitlint/parse': 20.3.1 + '@commitlint/rules': 20.3.1 + '@commitlint/types': 20.3.1 + + '@commitlint/load@20.3.1(@types/node@24.10.9)(typescript@5.9.3)': + dependencies: + '@commitlint/config-validator': 20.3.1 + '@commitlint/execute-rule': 20.0.0 + '@commitlint/resolve-extends': 20.3.1 + '@commitlint/types': 20.3.1 + chalk: 5.6.2 + cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.2.0(@types/node@24.10.9)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@20.0.0': {} + + '@commitlint/parse@20.3.1': + dependencies: + '@commitlint/types': 20.3.1 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@20.3.1': + dependencies: + '@commitlint/top-level': 20.0.0 + '@commitlint/types': 20.3.1 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 1.0.2 + + '@commitlint/resolve-extends@20.3.1': + dependencies: + '@commitlint/config-validator': 20.3.1 + '@commitlint/types': 20.3.1 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@20.3.1': + dependencies: + '@commitlint/ensure': 20.3.1 + '@commitlint/message': 20.0.0 + '@commitlint/to-lines': 20.0.0 + '@commitlint/types': 20.3.1 + + '@commitlint/to-lines@20.0.0': {} + + '@commitlint/top-level@20.0.0': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@20.3.1': + dependencies: + '@types/conventional-commits-parser': 5.0.2 + chalk: 5.6.2 + + '@epic-web/invariant@1.0.0': {} + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/core@10.3.2(@types/node@24.10.9)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.9) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@24.10.9)': + optionalDependencies: + '@types/node': 24.10.9 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mswjs/interceptors@0.40.0': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.3 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1) + + '@rolldown/pluginutils@1.0.0-beta.47': {} + + '@rollup/pluginutils@5.3.0(rollup@4.57.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.57.0 + + '@rollup/rollup-android-arm-eabi@4.57.0': + optional: true + + '@rollup/rollup-android-arm64@4.57.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.0': + optional: true + + '@rollup/rollup-darwin-x64@4.57.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.0': + optional: true + + '@socket.io/component-emitter@3.1.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@svgr/babel-preset@8.1.0(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.6) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.6) + + '@svgr/core@8.1.0(typescript@5.9.3)': + dependencies: + '@babel/core': 7.28.6 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.6) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@5.9.3) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@svgr/hast-util-to-babel-ast@8.0.0': + dependencies: + '@babel/types': 7.28.6 + entities: 4.5.0 + + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': + dependencies: + '@babel/core': 7.28.6 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.6) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/hast-util-to-babel-ast': 8.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + + '@swc/core-darwin-arm64@1.15.10': + optional: true + + '@swc/core-darwin-x64@1.15.10': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.10': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.10': + optional: true + + '@swc/core-linux-arm64-musl@1.15.10': + optional: true + + '@swc/core-linux-x64-gnu@1.15.10': + optional: true + + '@swc/core-linux-x64-musl@1.15.10': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.10': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.10': + optional: true + + '@swc/core-win32-x64-msvc@1.15.10': + optional: true + + '@swc/core@1.15.10': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.10 + '@swc/core-darwin-x64': 1.15.10 + '@swc/core-linux-arm-gnueabihf': 1.15.10 + '@swc/core-linux-arm64-gnu': 1.15.10 + '@swc/core-linux-arm64-musl': 1.15.10 + '@swc/core-linux-x64-gnu': 1.15.10 + '@swc/core-linux-x64-musl': 1.15.10 + '@swc/core-win32-arm64-msvc': 1.15.10 + '@swc/core-win32-ia32-msvc': 1.15.10 + '@swc/core-win32-x64-msvc': 1.15.10 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + + '@tanstack/query-core@5.90.20': {} + + '@tanstack/query-devtools@5.92.0': {} + + '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.20(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/query-devtools': 5.92.0 + '@tanstack/react-query': 5.90.20(react@19.2.4) + react: 19.2.4 + + '@tanstack/react-query@5.90.20(react@19.2.4)': + dependencies: + '@tanstack/query-core': 5.90.20 + react: 19.2.4 + + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.7.4)': + dependencies: + '@babel/generator': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + javascript-natural-sort: 0.7.1 + lodash-es: 4.17.23 + minimatch: 9.0.5 + parse-imports-exports: 0.2.4 + prettier: 3.7.4 + transitivePeerDependencies: + - supports-color + + '@types/conventional-commits-parser@5.0.2': + dependencies: + '@types/node': 24.10.9 + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@24.10.9': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.10)': + dependencies: + '@types/react': 19.2.10 + + '@types/react@19.2.10': + dependencies: + csstype: 3.2.3 + + '@types/statuses@2.0.6': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/type-utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + eslint: 8.57.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + + '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.54.0': {} + + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react-swc@4.2.2(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.47 + '@swc/core': 1.15.10 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + transitivePeerDependencies: + - '@swc/helpers' + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@7.2.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-ify@1.0.0: {} + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios@1.13.3: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.18: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.18 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.279 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001766: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.1.1: + dependencies: + slice-ansi: 7.1.2 + string-width: 8.1.0 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@14.0.2: {} + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + concat-map@0.0.1: {} + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cosmiconfig-typescript-loader@6.2.0(@types/node@24.10.9)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): + dependencies: + '@types/node': 24.10.9 + cosmiconfig: 9.0.0(typescript@5.9.3) + jiti: 2.6.1 + typescript: 5.9.3 + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + dargs@8.1.0: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + dayjs@1.11.19: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.279: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + engine.io-client@6.6.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.18.3 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es-toolkit@1.44.0: {} + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-check-file@2.8.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + is-glob: 4.0.3 + micromatch: 4.0.8 + + eslint-plugin-react-hooks@7.0.1(eslint@8.57.1): + dependencies: + '@babel/core': 7.28.6 + '@babel/parser': 7.28.6 + eslint: 8.57.1 + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.26(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react@7.37.5(eslint@8.57.1): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + eventemitter3@5.0.4: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.4.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + graphql@16.12.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + headers-polyfill@4.0.3: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hls.js@1.6.15: {} + + husky@9.1.7: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immer@10.2.0: {} + + immer@11.1.3: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.2.0: {} + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@4.1.1: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + internmap@2.0.3: {} + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.4.0 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-node-process@1.2.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-path-inside@3.0.3: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-what@5.5.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + javascript-natural-sort@0.7.1: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonparse@1.3.1: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lines-and-columns@1.2.4: {} + + lint-staged@16.2.7: + dependencies: + commander: 14.0.2 + listr2: 9.0.5 + micromatch: 4.0.8 + nano-spawn: 2.0.0 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.2 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.1.1 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.23: {} + + lodash.camelcase@4.3.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.2.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + meow@12.1.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-function@5.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + msw@2.12.7(@types/node@24.10.9)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@24.10.9) + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.1 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + + nano-spawn@2.0.0: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-releases@2.0.27: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + outvariant@1.4.3: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.28.6 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-statements@1.0.11: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@6.3.0: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pidtree@0.6.0: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.7.4: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-redux@9.2.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.10 + redux: 5.0.1 + + react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + + react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + cookie: 1.1.1 + react: 19.2.4 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + + react@19.2.4: {} + + recharts@3.7.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.44.0 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + reselect@5.1.1: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rettime@0.7.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.57.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.0 + '@rollup/rollup-android-arm64': 4.57.0 + '@rollup/rollup-darwin-arm64': 4.57.0 + '@rollup/rollup-darwin-x64': 4.57.0 + '@rollup/rollup-freebsd-arm64': 4.57.0 + '@rollup/rollup-freebsd-x64': 4.57.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.0 + '@rollup/rollup-linux-arm-musleabihf': 4.57.0 + '@rollup/rollup-linux-arm64-gnu': 4.57.0 + '@rollup/rollup-linux-arm64-musl': 4.57.0 + '@rollup/rollup-linux-loong64-gnu': 4.57.0 + '@rollup/rollup-linux-loong64-musl': 4.57.0 + '@rollup/rollup-linux-ppc64-gnu': 4.57.0 + '@rollup/rollup-linux-ppc64-musl': 4.57.0 + '@rollup/rollup-linux-riscv64-gnu': 4.57.0 + '@rollup/rollup-linux-riscv64-musl': 4.57.0 + '@rollup/rollup-linux-s390x-gnu': 4.57.0 + '@rollup/rollup-linux-x64-gnu': 4.57.0 + '@rollup/rollup-linux-x64-musl': 4.57.0 + '@rollup/rollup-openbsd-x64': 4.57.0 + '@rollup/rollup-openharmony-arm64': 4.57.0 + '@rollup/rollup-win32-arm64-msvc': 4.57.0 + '@rollup/rollup-win32-ia32-msvc': 4.57.0 + '@rollup/rollup-win32-x64-gnu': 4.57.0 + '@rollup/rollup-win32-x64-msvc': 4.57.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + set-cookie-parser@2.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + socket.io-client@4.8.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-client: 6.6.4 + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.5: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + statuses@2.0.2: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + strict-event-emitter@0.5.1: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string-width@8.1.0: + dependencies: + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@3.1.1: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-parser@2.0.4: {} + + tagged-tag@1.0.0: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + text-extensions@2.4.0: {} + + text-table@0.2.0: {} + + through@2.3.8: {} + + tiny-invariant@1.3.3: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tldts-core@7.0.19: {} + + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@5.4.1: + dependencies: + tagged-tag: 1.0.0 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@7.16.0: {} + + unicorn-magic@0.1.0: {} + + until-async@3.0.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite-plugin-svgr@4.5.0(rollup@4.57.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.57.0) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + + vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.9 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + yaml: 2.8.2 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + xmlhttprequest-ssl@2.1.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@2.8.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} + + yoctocolors-cjs@2.1.3: {} + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} + + zustand@5.0.10(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + optionalDependencies: + '@types/react': 19.2.10 + immer: 11.1.3 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) From 199c37a7939e94e9d4df2f9bec0b63abcac30ef8 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 21:03:46 +0900 Subject: [PATCH 70/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=9E=AC=EC=88=98=EC=A0=95(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4363ab31..fd29f30a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ bun.lock *.njsproj *.sln *.sw? -pnpm-lock.yaml +!pnpm-lock.yaml From f35d7f5a33d778a4268185c7b7407cfb834853f1 Mon Sep 17 00:00:00 2001 From: kimyw1018 Date: Mon, 9 Feb 2026 21:18:43 +0900 Subject: [PATCH 71/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=9E=AC=EC=88=98=EC=A0=95(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 50 ++++++++++- public/mockServiceWorker.js | 161 +++++++++++++++++++----------------- 2 files changed, 135 insertions(+), 76 deletions(-) diff --git a/package-lock.json b/package-lock.json index c00cab9d..920f04f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@typescript-eslint/eslint-plugin": "^8.50.1", "@typescript-eslint/parser": "^8.50.1", "@vitejs/plugin-react-swc": "^4.2.2", + "cross-env": "^10.1.0", "eslint": "^8", "eslint-config-prettier": "^10.1.8", "eslint-plugin-check-file": "^2.8.0", @@ -81,6 +82,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -722,6 +724,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -2761,6 +2770,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz", "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.16" }, @@ -2915,6 +2925,7 @@ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2925,6 +2936,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2987,6 +2999,7 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -3310,6 +3323,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3641,6 +3655,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4018,6 +4033,7 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -4057,6 +4073,24 @@ "typescript": ">=5" } }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4757,6 +4791,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7618,6 +7653,7 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7682,6 +7718,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7691,6 +7728,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7702,13 +7740,15 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -7799,7 +7839,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8719,6 +8760,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8902,6 +8944,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9036,6 +9079,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9142,6 +9186,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9504,6 +9549,7 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 6c62cc22..461e2600 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -7,42 +7,42 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.12.7'; -const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'; -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse'); -const activeClientIds = new Set(); +const PACKAGE_VERSION = '2.12.7' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() addEventListener('install', function () { - self.skipWaiting(); -}); + self.skipWaiting() +}) addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()); -}); + event.waitUntil(self.clients.claim()) +}) addEventListener('message', async function (event) { - const clientId = Reflect.get(event.source || {}, 'id'); + const clientId = Reflect.get(event.source || {}, 'id') if (!clientId || !self.clients) { - return; + return } - const client = await self.clients.get(clientId); + const client = await self.clients.get(clientId) if (!client) { - return; + return } const allClients = await self.clients.matchAll({ type: 'window', - }); + }) switch (event.data) { case 'KEEPALIVE_REQUEST': { sendToClient(client, { type: 'KEEPALIVE_RESPONSE', - }); - break; + }) + break } case 'INTEGRITY_CHECK_REQUEST': { @@ -52,12 +52,12 @@ addEventListener('message', async function (event) { packageVersion: PACKAGE_VERSION, checksum: INTEGRITY_CHECKSUM, }, - }); - break; + }) + break } case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId); + activeClientIds.add(clientId) sendToClient(client, { type: 'MOCKING_ENABLED', @@ -67,51 +67,54 @@ addEventListener('message', async function (event) { frameType: client.frameType, }, }, - }); - break; + }) + break } case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId); + activeClientIds.delete(clientId) const remainingClients = allClients.filter((client) => { - return client.id !== clientId; - }); + return client.id !== clientId + }) // Unregister itself when there are no more clients if (remainingClients.length === 0) { - self.registration.unregister(); + self.registration.unregister() } - break; + break } } -}); +}) addEventListener('fetch', function (event) { - const requestInterceptedAt = Date.now(); + const requestInterceptedAt = Date.now() // Bypass navigation requests. if (event.request.mode === 'navigate') { - return; + return } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. - if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { - return; + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been terminated (still remains active until the next reload). if (activeClientIds.size === 0) { - return; + return } - const requestId = crypto.randomUUID(); - event.respondWith(handleRequest(event, requestId, requestInterceptedAt)); -}); + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) /** * @param {FetchEvent} event @@ -119,18 +122,23 @@ addEventListener('fetch', function (event) { * @param {number} requestInterceptedAt */ async function handleRequest(event, requestId, requestInterceptedAt) { - const client = await resolveMainClient(event); - const requestCloneForEvents = event.request.clone(); - const response = await getResponse(event, client, requestId, requestInterceptedAt); + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - const serializedRequest = await serializeRequest(requestCloneForEvents); + const serializedRequest = await serializeRequest(requestCloneForEvents) // Clone the response so both the client and the library could consume it. - const responseClone = response.clone(); + const responseClone = response.clone() sendToClient( client, @@ -152,10 +160,10 @@ async function handleRequest(event, requestId, requestInterceptedAt) { }, }, responseClone.body ? [serializedRequest.body, responseClone.body] : [], - ); + ) } - return response; + return response } /** @@ -167,30 +175,30 @@ async function handleRequest(event, requestId, requestInterceptedAt) { * @returns {Promise} */ async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId); + const client = await self.clients.get(event.clientId) if (activeClientIds.has(event.clientId)) { - return client; + return client } if (client?.frameType === 'top-level') { - return client; + return client } const allClients = await self.clients.matchAll({ type: 'window', - }); + }) return allClients .filter((client) => { // Get only those clients that are currently visible. - return client.visibilityState === 'visible'; + return client.visibilityState === 'visible' }) .find((client) => { // Find the client ID that's recorded in the // set of clients that have registered the worker. - return activeClientIds.has(client.id); - }); + return activeClientIds.has(client.id) + }) } /** @@ -203,34 +211,36 @@ async function resolveMainClient(event) { async function getResponse(event, client, requestId, requestInterceptedAt) { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const requestClone = event.request.clone(); + const requestClone = event.request.clone() function passthrough() { // Cast the request headers to a new Headers instance // so the headers can be manipulated with. - const headers = new Headers(requestClone.headers); + const headers = new Headers(requestClone.headers) // Remove the "accept" header value that marked this request as passthrough. // This prevents request alteration and also keeps it compliant with the // user-defined CORS policies. - const acceptHeader = headers.get('accept'); + const acceptHeader = headers.get('accept') if (acceptHeader) { - const values = acceptHeader.split(',').map((value) => value.trim()); - const filteredValues = values.filter((value) => value !== 'msw/passthrough'); + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) if (filteredValues.length > 0) { - headers.set('accept', filteredValues.join(', ')); + headers.set('accept', filteredValues.join(', ')) } else { - headers.delete('accept'); + headers.delete('accept') } } - return fetch(requestClone, { headers }); + return fetch(requestClone, { headers }) } // Bypass mocking when the client is not active. if (!client) { - return passthrough(); + return passthrough() } // Bypass initial page load requests (i.e. static assets). @@ -238,11 +248,11 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { - return passthrough(); + return passthrough() } // Notify the client that a request has been intercepted. - const serializedRequest = await serializeRequest(event.request); + const serializedRequest = await serializeRequest(event.request) const clientMessage = await sendToClient( client, { @@ -254,19 +264,19 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { }, }, [serializedRequest.body], - ); + ) switch (clientMessage.type) { case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data); + return respondWithMock(clientMessage.data) } case 'PASSTHROUGH': { - return passthrough(); + return passthrough() } } - return passthrough(); + return passthrough() } /** @@ -277,18 +287,21 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { */ function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { - const channel = new MessageChannel(); + const channel = new MessageChannel() channel.port1.onmessage = (event) => { if (event.data && event.data.error) { - return reject(event.data.error); + return reject(event.data.error) } - resolve(event.data); - }; + resolve(event.data) + } - client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]); - }); + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) } /** @@ -301,17 +314,17 @@ function respondWithMock(response) { // instance will have status code set to 0. Since it's not possible to create // a Response instance with status code 0, handle that use-case separately. if (response.status === 0) { - return Response.error(); + return Response.error() } - const mockedResponse = new Response(response.body, response); + const mockedResponse = new Response(response.body, response) Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { value: true, enumerable: true, - }); + }) - return mockedResponse; + return mockedResponse } /** @@ -332,5 +345,5 @@ async function serializeRequest(request) { referrerPolicy: request.referrerPolicy, body: await request.arrayBuffer(), keepalive: request.keepalive, - }; + } } From c7d315f1b9291e0b05a57921012ca630b2efea50 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 21:50:41 +0900 Subject: [PATCH 72/81] =?UTF-8?q?fix:=20=EB=AA=A8=EB=8B=AC=20=EB=B2=97?= =?UTF-8?q?=EC=96=B4=EB=82=98=EB=A9=B4=20=EC=84=B8=EC=85=98=20=ED=92=80?= =?UTF-8?q?=EB=A6=AC=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 34 +++++++++++++++++++++++++++++++++ src/api/index.ts | 1 - src/pages/OAuthCallbackPage.tsx | 8 ++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index cdf5beee..b4c506b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,46 @@ +import { useEffect } from 'react'; import { RouterProvider } from 'react-router-dom'; +import type { JwtPayloadDto } from '@/api/dto'; import { DevFab } from '@/components/common/DevFab'; import { router } from '@/router'; +import { useAuthStore } from '@/stores/authStore'; import { useThemeListener } from '@/stores/themeStore'; +import { parseJwtPayload } from '@/utils/jwt'; function App() { useThemeListener(); + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.origin !== window.location.origin) return; + const data = event.data; + if (!data || data.type !== 'oauth:callback') return; + + const accessToken = data.accessToken as string | undefined; + if (!accessToken) return; + + const payload = parseJwtPayload(accessToken); + const userId = payload?.id ?? ''; + const userEmail = payload?.email ?? ''; + const sessionId = data.sessionId ?? payload?.sessionId ?? ''; + + useAuthStore.getState().login( + { + id: userId, + email: userEmail, + name: userEmail.split('@')[0] || userEmail, + sessionId, + }, + accessToken, + ); + useAuthStore.getState().closeLoginModal(); + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + return ( <> diff --git a/src/api/index.ts b/src/api/index.ts index f13d2732..6844e209 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,6 +2,5 @@ * API 모듈 배럴 파일 */ export { queryClient, queryKeys } from './queryClient'; -export * from './endpoints/auth'; export * from './endpoints/videos'; export * from './dto'; diff --git a/src/pages/OAuthCallbackPage.tsx b/src/pages/OAuthCallbackPage.tsx index 3809cad8..84ecf330 100644 --- a/src/pages/OAuthCallbackPage.tsx +++ b/src/pages/OAuthCallbackPage.tsx @@ -49,6 +49,14 @@ export default function OAuthCallbackPage() { // 팝업인 경우 닫기, 아니면 홈으로 이동 if (window.opener) { + window.opener.postMessage( + { + type: 'oauth:callback', + accessToken, + sessionId, + }, + window.location.origin, + ); window.close(); } else { navigate('/', { replace: true }); From 365bf999279c3ff9d6909ddf0208e7c948cb75e7 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 22:13:34 +0900 Subject: [PATCH 73/81] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useSlideCommentsActions.ts | 24 ++++++++++++++++--- .../dev-test/sections/AuthTokenSection.tsx | 19 ++++----------- src/utils/jwt.ts | 10 ++++---- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/hooks/useSlideCommentsActions.ts b/src/hooks/useSlideCommentsActions.ts index 91f87e35..cc2369b2 100644 --- a/src/hooks/useSlideCommentsActions.ts +++ b/src/hooks/useSlideCommentsActions.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { CreateCommentRequestDto } from '@/api'; import { @@ -10,6 +10,7 @@ import { deleteComment as deleteCommentApi, updateComment as updateCommentApi, } from '@/api/endpoints/comments'; +import { queryKeys } from '@/api/queryClient'; import { useSlideStore } from '@/stores/slideStore'; import type { Comment } from '@/types/comment'; import { flatToTree } from '@/utils/comment'; @@ -75,6 +76,7 @@ const EMPTY_COMMENTS: Comment[] = []; export function useSlideCommentsActions() { const { projectId = '' } = useParams<{ projectId: string }>(); const slideId = useSlideStore((state) => state.slide?.slideId); + const queryClient = useQueryClient(); const flatComments = useSlideStore((state) => state.slide?.comments); const addCommentStore = useSlideStore((state) => state.addComment); const addReplyStore = useSlideStore((state) => state.addReply); @@ -104,8 +106,9 @@ export function useSlideCommentsActions() { { slideId, projectId, data: { content } }, { onSuccess: () => { - // 서버가 웹소켓을 보내지 않으므로 수동으로 쿼리 무효화 - // TODO: 서버에서 broadcastNewComment 호출 후 제거 + queryClient.invalidateQueries({ + queryKey: queryKeys.comments.list(slideId), + }); }, onError: () => { setComments(previousComments); @@ -128,6 +131,11 @@ export function useSlideCommentsActions() { createReplyMutate( { commentId: targetServerId, slideId: targetSlideId, projectId, data: { content } }, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.comments.list(targetSlideId), + }); + }, onError: () => { setComments(previousComments); showToast.error('답글 등록에 실패했습니다.', '잠시 후 다시 시도해주세요.'); @@ -158,6 +166,11 @@ export function useSlideCommentsActions() { deleteCommentMutate( { commentId: targetServerId, slideId: targetSlideId, projectId }, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.comments.list(targetSlideId), + }); + }, onError: () => { setComments(previousComments); showToast.error('댓글 삭제에 실패했습니다.', '잠시 후 다시 시도해주세요.'); @@ -188,6 +201,11 @@ export function useSlideCommentsActions() { updateCommentMutate( { commentId: targetServerId, slideId: targetSlideId, projectId, data: { content } }, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.comments.list(targetSlideId), + }); + }, onError: () => { setComments(previousComments); showToast.error('댓글 수정에 실패했습니다.', '잠시 후 다시 시도해주세요.'); diff --git a/src/pages/dev-test/sections/AuthTokenSection.tsx b/src/pages/dev-test/sections/AuthTokenSection.tsx index f49eb65a..89036952 100644 --- a/src/pages/dev-test/sections/AuthTokenSection.tsx +++ b/src/pages/dev-test/sections/AuthTokenSection.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { TextField } from '@/components/common'; import { useAuthStore } from '@/stores/authStore'; +import { parseJwtPayload } from '@/utils/jwt'; import { showToast } from '@/utils/toast'; type JwtDecodeResult = { ok: true; payload: unknown } | { ok: false; reason: string }; @@ -11,22 +12,12 @@ const decodeJwtPayload = (token: string | null): JwtDecodeResult => { return { ok: false, reason: '토큰 없음' }; } - const parts = token.split('.'); - if (parts.length < 2) { - return { ok: false, reason: 'JWT 형식이 아닙니다.' }; - } - - try { - const base64Url = parts[1]; - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '='); - const binary = window.atob(padded); - const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); - const json = new TextDecoder().decode(bytes); - return { ok: true, payload: JSON.parse(json) }; - } catch { + const payload = parseJwtPayload(token); + if (!payload) { return { ok: false, reason: 'payload 파싱 실패' }; } + + return { ok: true, payload }; }; export function AuthTokenSection() { diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index e85c9886..3df5e682 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -9,12 +9,10 @@ export function parseJwtPayload(token: string): T | null { if (!base64Url) return null; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const json = decodeURIComponent( - atob(base64) - .split('') - .map((c) => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')) - .join(''), - ); + const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '='); + const binary = atob(padded); + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); + const json = new TextDecoder().decode(bytes); return JSON.parse(json) as T; } catch { From ed42b05bc29ecbbbeef4384f4bd336ebaf3ff7fe Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 16:26:23 +0900 Subject: [PATCH 74/81] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20optimistic?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=84=A3=EC=9D=80=EA=B1=B0=20=EC=82=AC?= =?UTF-8?q?=EB=9D=BC=EC=A7=80=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/queries/useSlideCommentsQuery.ts | 25 ++++----------- src/hooks/useSlideCommentsActions.ts | 36 +--------------------- src/hooks/useSlideCommentsLoader.ts | 35 ++++++++++----------- 3 files changed, 24 insertions(+), 72 deletions(-) diff --git a/src/hooks/queries/useSlideCommentsQuery.ts b/src/hooks/queries/useSlideCommentsQuery.ts index 0282c8fb..528c1985 100644 --- a/src/hooks/queries/useSlideCommentsQuery.ts +++ b/src/hooks/queries/useSlideCommentsQuery.ts @@ -1,6 +1,6 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; -import type { CommentWithUserDto, GetSlideCommentsResponseDto } from '@/api/dto'; +import type { CommentWithUserDto } from '@/api/dto'; import { getSlideComments } from '@/api/endpoints/comments'; import { queryKeys } from '@/api/queryClient'; import { useAuthStore } from '@/stores/authStore'; @@ -18,32 +18,19 @@ function mapDtoToComment(dto: CommentWithUserDto, currentUserId?: string): Comme } /** - * 슬라이드 댓글 목록 조회 (무한 스크롤) + * 슬라이드 댓글 목록 조회 * * DTO를 Comment 타입으로 변환하고, 현재 사용자의 댓글을 isMine으로 표시합니다. - * pages를 flat한 Comment[]로 변환하여 제공합니다. * * @param slideId - 슬라이드 ID */ export function useSlideCommentsQuery(slideId?: string) { const userId = useAuthStore((state) => state.user?.id); - return useInfiniteQuery< - GetSlideCommentsResponseDto, - Error, - Comment[], - ReturnType, - number - >({ + return useQuery({ queryKey: queryKeys.comments.list(slideId ?? ''), - queryFn: ({ pageParam }) => getSlideComments(slideId!, pageParam), - initialPageParam: 1, - getNextPageParam: (lastPage) => { - const { page, totalPages } = lastPage.pagination; - return page < totalPages ? page + 1 : undefined; - }, - select: (data) => - data.pages.flatMap((p) => p.comments.map((dto) => mapDtoToComment(dto, userId))), + queryFn: () => getSlideComments(slideId!, 1, 100), + select: (data) => data.comments.map((dto) => mapDtoToComment(dto, userId)), enabled: !!slideId, }); } diff --git a/src/hooks/useSlideCommentsActions.ts b/src/hooks/useSlideCommentsActions.ts index 11b42c40..91f87e35 100644 --- a/src/hooks/useSlideCommentsActions.ts +++ b/src/hooks/useSlideCommentsActions.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import type { CreateCommentRequestDto } from '@/api'; import { @@ -10,7 +10,6 @@ import { deleteComment as deleteCommentApi, updateComment as updateCommentApi, } from '@/api/endpoints/comments'; -import { queryKeys } from '@/api/queryClient'; import { useSlideStore } from '@/stores/slideStore'; import type { Comment } from '@/types/comment'; import { flatToTree } from '@/utils/comment'; @@ -19,26 +18,16 @@ import { showToast } from '@/utils/toast'; // ── 내부 전용 TanStack Query 훅 ───────────────────────────── function useCreateCommentMutation() { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: (variables: { slideId: string; projectId: string; data: CreateCommentRequestDto; }) => createSlideComment(variables.slideId, variables.data), - - onSuccess: (_, { slideId, projectId }) => { - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - }, }); } function useCreateReplyMutation() { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: (variables: { commentId: string; @@ -46,19 +35,10 @@ function useCreateReplyMutation() { projectId: string; data: { content: string }; }) => createReply(variables.commentId, variables.data), - - onSuccess: (_, { commentId, slideId, projectId }) => { - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.replies(commentId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - }, }); } function useUpdateCommentMutation() { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: (variables: { commentId: string; @@ -66,27 +46,13 @@ function useUpdateCommentMutation() { projectId: string; data: { content: string }; }) => updateCommentApi(variables.commentId, variables.data), - - onSuccess: (_, { slideId, projectId }) => { - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - }, }); } function useDeleteCommentMutation() { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: (variables: { commentId: string; slideId: string; projectId: string }) => deleteCommentApi({ commentId: variables.commentId }), - - onSuccess: (_, { slideId, projectId }) => { - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - }, }); } diff --git a/src/hooks/useSlideCommentsLoader.ts b/src/hooks/useSlideCommentsLoader.ts index 6ea1b34d..d161f772 100644 --- a/src/hooks/useSlideCommentsLoader.ts +++ b/src/hooks/useSlideCommentsLoader.ts @@ -1,9 +1,8 @@ /** * 슬라이드 댓글 로딩 공통 훅 * - * - slideId 변경 시 댓글 초기화 - * - getSlideComments 호출 결과를 store에 주입 - * - 무한 스크롤 핸들러(fetchNextPage, hasNextPage, isFetchingNextPage) 반환 + * - slideId 변경 시 서버에서 댓글 로드 + * - 초기 로드 후에는 Zustand store가 댓글 상태를 관리 */ import { useEffect, useRef } from 'react'; @@ -18,27 +17,27 @@ type UseSlideCommentsLoaderOptions = { export function useSlideCommentsLoader(slideId?: string, options?: UseSlideCommentsLoaderOptions) { const { setComments } = useSlideActions(); - const { - data: fetchedComments, - isLoading, - dataUpdatedAt, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useSlideCommentsQuery(slideId); - const lastAppliedRef = useRef(0); + const { data: fetchedComments, isLoading } = useSlideCommentsQuery(slideId); + const prevSlideIdRef = useRef(undefined); + const initialLoadDoneRef = useRef(false); + // slideId가 변경되면 초기 로드 플래그 리셋 useEffect(() => { - setComments([]); + if (prevSlideIdRef.current !== slideId) { + prevSlideIdRef.current = slideId; + initialLoadDoneRef.current = false; + setComments([]); + } }, [slideId, setComments]); + // 초기 로드 시에만 서버 데이터를 store에 적용 useEffect(() => { - if (!fetchedComments) return; - if (dataUpdatedAt && lastAppliedRef.current === dataUpdatedAt) return; - if (dataUpdatedAt) lastAppliedRef.current = dataUpdatedAt; + if (!fetchedComments || initialLoadDoneRef.current) return; + + initialLoadDoneRef.current = true; const mapped = options?.mapComments ? options.mapComments(fetchedComments) : fetchedComments; setComments(mapped); - }, [dataUpdatedAt, fetchedComments, options?.mapComments, setComments]); + }, [fetchedComments, options?.mapComments, setComments]); - return { isLoading, fetchNextPage, hasNextPage, isFetchingNextPage }; + return { isLoading }; } From b56d53b9fcd34fd6e293630baf85d1543043ae5e Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 16:29:14 +0900 Subject: [PATCH 75/81] =?UTF-8?q?Revert=20"fix:=20=EB=8C=93=EA=B8=80=20opt?= =?UTF-8?q?imistic=ED=95=98=EA=B2=8C=20=EB=84=A3=EC=9D=80=EA=B1=B0=20?= =?UTF-8?q?=EC=82=AC=EB=9D=BC=EC=A7=80=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#130)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 88844232583d40bc4f6934351aa9b5412e04f96c. --- src/hooks/queries/useSlideCommentsQuery.ts | 25 +++++++++++---- src/hooks/useSlideCommentsActions.ts | 36 +++++++++++++++++++++- src/hooks/useSlideCommentsLoader.ts | 35 +++++++++++---------- 3 files changed, 72 insertions(+), 24 deletions(-) diff --git a/src/hooks/queries/useSlideCommentsQuery.ts b/src/hooks/queries/useSlideCommentsQuery.ts index 528c1985..0282c8fb 100644 --- a/src/hooks/queries/useSlideCommentsQuery.ts +++ b/src/hooks/queries/useSlideCommentsQuery.ts @@ -1,6 +1,6 @@ -import { useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; -import type { CommentWithUserDto } from '@/api/dto'; +import type { CommentWithUserDto, GetSlideCommentsResponseDto } from '@/api/dto'; import { getSlideComments } from '@/api/endpoints/comments'; import { queryKeys } from '@/api/queryClient'; import { useAuthStore } from '@/stores/authStore'; @@ -18,19 +18,32 @@ function mapDtoToComment(dto: CommentWithUserDto, currentUserId?: string): Comme } /** - * 슬라이드 댓글 목록 조회 + * 슬라이드 댓글 목록 조회 (무한 스크롤) * * DTO를 Comment 타입으로 변환하고, 현재 사용자의 댓글을 isMine으로 표시합니다. + * pages를 flat한 Comment[]로 변환하여 제공합니다. * * @param slideId - 슬라이드 ID */ export function useSlideCommentsQuery(slideId?: string) { const userId = useAuthStore((state) => state.user?.id); - return useQuery({ + return useInfiniteQuery< + GetSlideCommentsResponseDto, + Error, + Comment[], + ReturnType, + number + >({ queryKey: queryKeys.comments.list(slideId ?? ''), - queryFn: () => getSlideComments(slideId!, 1, 100), - select: (data) => data.comments.map((dto) => mapDtoToComment(dto, userId)), + queryFn: ({ pageParam }) => getSlideComments(slideId!, pageParam), + initialPageParam: 1, + getNextPageParam: (lastPage) => { + const { page, totalPages } = lastPage.pagination; + return page < totalPages ? page + 1 : undefined; + }, + select: (data) => + data.pages.flatMap((p) => p.comments.map((dto) => mapDtoToComment(dto, userId))), enabled: !!slideId, }); } diff --git a/src/hooks/useSlideCommentsActions.ts b/src/hooks/useSlideCommentsActions.ts index 91f87e35..11b42c40 100644 --- a/src/hooks/useSlideCommentsActions.ts +++ b/src/hooks/useSlideCommentsActions.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { CreateCommentRequestDto } from '@/api'; import { @@ -10,6 +10,7 @@ import { deleteComment as deleteCommentApi, updateComment as updateCommentApi, } from '@/api/endpoints/comments'; +import { queryKeys } from '@/api/queryClient'; import { useSlideStore } from '@/stores/slideStore'; import type { Comment } from '@/types/comment'; import { flatToTree } from '@/utils/comment'; @@ -18,16 +19,26 @@ import { showToast } from '@/utils/toast'; // ── 내부 전용 TanStack Query 훅 ───────────────────────────── function useCreateCommentMutation() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (variables: { slideId: string; projectId: string; data: CreateCommentRequestDto; }) => createSlideComment(variables.slideId, variables.data), + + onSuccess: (_, { slideId, projectId }) => { + void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); + }, }); } function useCreateReplyMutation() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (variables: { commentId: string; @@ -35,10 +46,19 @@ function useCreateReplyMutation() { projectId: string; data: { content: string }; }) => createReply(variables.commentId, variables.data), + + onSuccess: (_, { commentId, slideId, projectId }) => { + void queryClient.invalidateQueries({ queryKey: queryKeys.comments.replies(commentId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); + }, }); } function useUpdateCommentMutation() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (variables: { commentId: string; @@ -46,13 +66,27 @@ function useUpdateCommentMutation() { projectId: string; data: { content: string }; }) => updateCommentApi(variables.commentId, variables.data), + + onSuccess: (_, { slideId, projectId }) => { + void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); + }, }); } function useDeleteCommentMutation() { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (variables: { commentId: string; slideId: string; projectId: string }) => deleteCommentApi({ commentId: variables.commentId }), + + onSuccess: (_, { slideId, projectId }) => { + void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); + }, }); } diff --git a/src/hooks/useSlideCommentsLoader.ts b/src/hooks/useSlideCommentsLoader.ts index d161f772..6ea1b34d 100644 --- a/src/hooks/useSlideCommentsLoader.ts +++ b/src/hooks/useSlideCommentsLoader.ts @@ -1,8 +1,9 @@ /** * 슬라이드 댓글 로딩 공통 훅 * - * - slideId 변경 시 서버에서 댓글 로드 - * - 초기 로드 후에는 Zustand store가 댓글 상태를 관리 + * - slideId 변경 시 댓글 초기화 + * - getSlideComments 호출 결과를 store에 주입 + * - 무한 스크롤 핸들러(fetchNextPage, hasNextPage, isFetchingNextPage) 반환 */ import { useEffect, useRef } from 'react'; @@ -17,27 +18,27 @@ type UseSlideCommentsLoaderOptions = { export function useSlideCommentsLoader(slideId?: string, options?: UseSlideCommentsLoaderOptions) { const { setComments } = useSlideActions(); - const { data: fetchedComments, isLoading } = useSlideCommentsQuery(slideId); - const prevSlideIdRef = useRef(undefined); - const initialLoadDoneRef = useRef(false); + const { + data: fetchedComments, + isLoading, + dataUpdatedAt, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useSlideCommentsQuery(slideId); + const lastAppliedRef = useRef(0); - // slideId가 변경되면 초기 로드 플래그 리셋 useEffect(() => { - if (prevSlideIdRef.current !== slideId) { - prevSlideIdRef.current = slideId; - initialLoadDoneRef.current = false; - setComments([]); - } + setComments([]); }, [slideId, setComments]); - // 초기 로드 시에만 서버 데이터를 store에 적용 useEffect(() => { - if (!fetchedComments || initialLoadDoneRef.current) return; - - initialLoadDoneRef.current = true; + if (!fetchedComments) return; + if (dataUpdatedAt && lastAppliedRef.current === dataUpdatedAt) return; + if (dataUpdatedAt) lastAppliedRef.current = dataUpdatedAt; const mapped = options?.mapComments ? options.mapComments(fetchedComments) : fetchedComments; setComments(mapped); - }, [fetchedComments, options?.mapComments, setComments]); + }, [dataUpdatedAt, fetchedComments, options?.mapComments, setComments]); - return { isLoading }; + return { isLoading, fetchNextPage, hasNextPage, isFetchingNextPage }; } From df7f70d37dc23e388b3983940ecd5c4c3265e4e7 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 16:29:45 +0900 Subject: [PATCH 76/81] =?UTF-8?q?Reapply=20"fix:=20=EB=8C=93=EA=B8=80=20op?= =?UTF-8?q?timistic=ED=95=98=EA=B2=8C=20=EB=84=A3=EC=9D=80=EA=B1=B0=20?= =?UTF-8?q?=EC=82=AC=EB=9D=BC=EC=A7=80=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#130)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 516e925b45b8d1eda314bd2d8e4d870f34061437. --- src/hooks/queries/useSlideCommentsQuery.ts | 25 ++++----------- src/hooks/useSlideCommentsActions.ts | 36 +--------------------- src/hooks/useSlideCommentsLoader.ts | 35 ++++++++++----------- 3 files changed, 24 insertions(+), 72 deletions(-) diff --git a/src/hooks/queries/useSlideCommentsQuery.ts b/src/hooks/queries/useSlideCommentsQuery.ts index 0282c8fb..528c1985 100644 --- a/src/hooks/queries/useSlideCommentsQuery.ts +++ b/src/hooks/queries/useSlideCommentsQuery.ts @@ -1,6 +1,6 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; -import type { CommentWithUserDto, GetSlideCommentsResponseDto } from '@/api/dto'; +import type { CommentWithUserDto } from '@/api/dto'; import { getSlideComments } from '@/api/endpoints/comments'; import { queryKeys } from '@/api/queryClient'; import { useAuthStore } from '@/stores/authStore'; @@ -18,32 +18,19 @@ function mapDtoToComment(dto: CommentWithUserDto, currentUserId?: string): Comme } /** - * 슬라이드 댓글 목록 조회 (무한 스크롤) + * 슬라이드 댓글 목록 조회 * * DTO를 Comment 타입으로 변환하고, 현재 사용자의 댓글을 isMine으로 표시합니다. - * pages를 flat한 Comment[]로 변환하여 제공합니다. * * @param slideId - 슬라이드 ID */ export function useSlideCommentsQuery(slideId?: string) { const userId = useAuthStore((state) => state.user?.id); - return useInfiniteQuery< - GetSlideCommentsResponseDto, - Error, - Comment[], - ReturnType, - number - >({ + return useQuery({ queryKey: queryKeys.comments.list(slideId ?? ''), - queryFn: ({ pageParam }) => getSlideComments(slideId!, pageParam), - initialPageParam: 1, - getNextPageParam: (lastPage) => { - const { page, totalPages } = lastPage.pagination; - return page < totalPages ? page + 1 : undefined; - }, - select: (data) => - data.pages.flatMap((p) => p.comments.map((dto) => mapDtoToComment(dto, userId))), + queryFn: () => getSlideComments(slideId!, 1, 100), + select: (data) => data.comments.map((dto) => mapDtoToComment(dto, userId)), enabled: !!slideId, }); } diff --git a/src/hooks/useSlideCommentsActions.ts b/src/hooks/useSlideCommentsActions.ts index 11b42c40..91f87e35 100644 --- a/src/hooks/useSlideCommentsActions.ts +++ b/src/hooks/useSlideCommentsActions.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import type { CreateCommentRequestDto } from '@/api'; import { @@ -10,7 +10,6 @@ import { deleteComment as deleteCommentApi, updateComment as updateCommentApi, } from '@/api/endpoints/comments'; -import { queryKeys } from '@/api/queryClient'; import { useSlideStore } from '@/stores/slideStore'; import type { Comment } from '@/types/comment'; import { flatToTree } from '@/utils/comment'; @@ -19,26 +18,16 @@ import { showToast } from '@/utils/toast'; // ── 내부 전용 TanStack Query 훅 ───────────────────────────── function useCreateCommentMutation() { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: (variables: { slideId: string; projectId: string; data: CreateCommentRequestDto; }) => createSlideComment(variables.slideId, variables.data), - - onSuccess: (_, { slideId, projectId }) => { - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - }, }); } function useCreateReplyMutation() { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: (variables: { commentId: string; @@ -46,19 +35,10 @@ function useCreateReplyMutation() { projectId: string; data: { content: string }; }) => createReply(variables.commentId, variables.data), - - onSuccess: (_, { commentId, slideId, projectId }) => { - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.replies(commentId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - }, }); } function useUpdateCommentMutation() { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: (variables: { commentId: string; @@ -66,27 +46,13 @@ function useUpdateCommentMutation() { projectId: string; data: { content: string }; }) => updateCommentApi(variables.commentId, variables.data), - - onSuccess: (_, { slideId, projectId }) => { - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - }, }); } function useDeleteCommentMutation() { - const queryClient = useQueryClient(); - return useMutation({ mutationFn: (variables: { commentId: string; slideId: string; projectId: string }) => deleteCommentApi({ commentId: variables.commentId }), - - onSuccess: (_, { slideId, projectId }) => { - void queryClient.invalidateQueries({ queryKey: queryKeys.comments.list(slideId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); - void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - }, }); } diff --git a/src/hooks/useSlideCommentsLoader.ts b/src/hooks/useSlideCommentsLoader.ts index 6ea1b34d..d161f772 100644 --- a/src/hooks/useSlideCommentsLoader.ts +++ b/src/hooks/useSlideCommentsLoader.ts @@ -1,9 +1,8 @@ /** * 슬라이드 댓글 로딩 공통 훅 * - * - slideId 변경 시 댓글 초기화 - * - getSlideComments 호출 결과를 store에 주입 - * - 무한 스크롤 핸들러(fetchNextPage, hasNextPage, isFetchingNextPage) 반환 + * - slideId 변경 시 서버에서 댓글 로드 + * - 초기 로드 후에는 Zustand store가 댓글 상태를 관리 */ import { useEffect, useRef } from 'react'; @@ -18,27 +17,27 @@ type UseSlideCommentsLoaderOptions = { export function useSlideCommentsLoader(slideId?: string, options?: UseSlideCommentsLoaderOptions) { const { setComments } = useSlideActions(); - const { - data: fetchedComments, - isLoading, - dataUpdatedAt, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useSlideCommentsQuery(slideId); - const lastAppliedRef = useRef(0); + const { data: fetchedComments, isLoading } = useSlideCommentsQuery(slideId); + const prevSlideIdRef = useRef(undefined); + const initialLoadDoneRef = useRef(false); + // slideId가 변경되면 초기 로드 플래그 리셋 useEffect(() => { - setComments([]); + if (prevSlideIdRef.current !== slideId) { + prevSlideIdRef.current = slideId; + initialLoadDoneRef.current = false; + setComments([]); + } }, [slideId, setComments]); + // 초기 로드 시에만 서버 데이터를 store에 적용 useEffect(() => { - if (!fetchedComments) return; - if (dataUpdatedAt && lastAppliedRef.current === dataUpdatedAt) return; - if (dataUpdatedAt) lastAppliedRef.current = dataUpdatedAt; + if (!fetchedComments || initialLoadDoneRef.current) return; + + initialLoadDoneRef.current = true; const mapped = options?.mapComments ? options.mapComments(fetchedComments) : fetchedComments; setComments(mapped); - }, [dataUpdatedAt, fetchedComments, options?.mapComments, setComments]); + }, [fetchedComments, options?.mapComments, setComments]); - return { isLoading, fetchNextPage, hasNextPage, isFetchingNextPage }; + return { isLoading }; } From 77b13b21bd0b03a0140eae5da903d6d7ac77716e Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 20:23:16 +0900 Subject: [PATCH 77/81] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.ts | 2 +- src/api/dto/auth.dto.ts | 11 ++++- src/api/dto/index.ts | 1 + src/api/endpoints/auth.ts | 71 +-------------------------------- src/mocks/handlers.ts | 36 ++++++++++++----- src/pages/OAuthCallbackPage.tsx | 30 +++++++------- src/stores/authStore.ts | 6 +-- src/types/auth.ts | 2 +- src/utils/jwt.ts | 23 +++++++++++ 9 files changed, 80 insertions(+), 102 deletions(-) create mode 100644 src/utils/jwt.ts diff --git a/src/api/client.ts b/src/api/client.ts index 2fdbd8ef..30390485 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -40,7 +40,7 @@ export const apiClient = axios.create({ timeout: API_TIMEOUT_MS, // 쿠키 설정 - withCredentials: false, + withCredentials: true, headers: { 'Content-Type': 'application/json', diff --git a/src/api/dto/auth.dto.ts b/src/api/dto/auth.dto.ts index abfe776c..e5e34b29 100644 --- a/src/api/dto/auth.dto.ts +++ b/src/api/dto/auth.dto.ts @@ -14,10 +14,10 @@ export interface SocialLoginUserResponseDto { /** * 소셜 로그인 성공 응답의 토큰 정보 + * refreshToken은 HttpOnly 쿠키로 전달되므로 응답 body에 포함되지 않음 */ export interface SocialLoginTokensResponseDto { accessToken: string; - refreshToken: string; } /** @@ -29,4 +29,13 @@ export interface SocialLoginSuccessResponseDto { tokens: SocialLoginTokensResponseDto; } +/** + * JWT payload 타입 (서버 JWT에서 디코딩) + */ +export interface JwtPayloadDto { + id: string; + email: string; + sessionId: string; +} + //위 형식대로 response, request dto 작성 부탁드립니다. diff --git a/src/api/dto/index.ts b/src/api/dto/index.ts index 87d2f37b..d34e342d 100644 --- a/src/api/dto/index.ts +++ b/src/api/dto/index.ts @@ -4,6 +4,7 @@ */ export type { + JwtPayloadDto, SocialLoginSuccessResponseDto, SocialLoginTokensResponseDto, SocialLoginUserResponseDto, diff --git a/src/api/endpoints/auth.ts b/src/api/endpoints/auth.ts index bad0cfce..3375e10a 100644 --- a/src/api/endpoints/auth.ts +++ b/src/api/endpoints/auth.ts @@ -1,74 +1,7 @@ /** * @file auth.ts * @description 인증 관련 API 엔드포인트 - */ -import { apiClient } from '@/api/client'; -import type { SocialLoginSuccessResponseDto } from '@/api/dto'; -import type { ApiResponse } from '@/types/api'; - -/** - * Google OAuth 콜백 처리 - * - * @param code - OAuth 인증 코드 - * @returns 로그인 성공 정보 (user, tokens) - * - * @example - * const result = await getGoogleCallback('authorization-code'); - */ -export async function getGoogleCallback(code: string): Promise { - const response = await apiClient.get>( - `/auth/google/callback`, - { params: { code } }, - ); - - if (response.data.resultType === 'SUCCESS') { - return response.data.success; - } - throw new Error(response.data.error.reason); -} - -/** - * Kakao OAuth 콜백 처리 - * - * @param code - OAuth 인증 코드 - * @returns 로그인 성공 정보 (user, tokens) - * - * @example - * const result = await getKakaoCallback('authorization-code'); - */ -export async function getKakaoCallback(code: string): Promise { - const response = await apiClient.get>( - `/auth/kakao/callback`, - { - params: { code }, - }, - ); - - if (response.data.resultType === 'SUCCESS') { - return response.data.success; - } - throw new Error(response.data.error.reason); -} - -/** - * Naver OAuth 콜백 처리 - * - * @param code - OAuth 인증 코드 - * @returns 로그인 성공 정보 (user, tokens) * - * @example - * const result = await getNaverCallback('authorization-code'); + * OAuth 콜백은 서버가 리다이렉트로 처리하므로 클라이언트 API 호출 불필요. + * OAuthCallbackPage에서 URL 파라미터 + JWT 디코딩으로 처리합니다. */ -export async function getNaverCallback(code: string): Promise { - const response = await apiClient.get>( - `/auth/naver/callback`, - { - params: { code }, - }, - ); - - if (response.data.resultType === 'SUCCESS') { - return response.data.success; - } - throw new Error(response.data.error.reason); -} diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 22636eb2..fa7e46de 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -148,6 +148,15 @@ slides.forEach((s) => { // AUTH — /auth/:provider/callback, /auth/logout, /users/:userId // ═══════════════════════════════════════════════════════════════ +/** + * Mock JWT 생성 (payload만 유효, 서명은 더미) + */ +function createMockJwt(payload: Record): string { + const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); + const body = btoa(JSON.stringify(payload)); + return `${header}.${body}.mock-signature`; +} + const authCallbackHandler = async ({ request }: { request: Request }) => { await delay(200); const url = new URL(request.url); @@ -155,17 +164,22 @@ const authCallbackHandler = async ({ request }: { request: Request }) => { if (!code) return fail(400, 'A001', '인증 코드가 없습니다.'); const user = MOCK_CURRENT_USER; - return ok({ - message: '소셜 로그인 성공!', - user: { - id: user.id, - email: user.email, - name: user.name, - sessionId: user.sessionId, - }, - tokens: { - accessToken: `mock-access-token-${user.id}`, - refreshToken: `mock-refresh-token-${user.id}`, + const accessToken = createMockJwt({ + id: user.id, + email: user.email, + sessionId: user.sessionId, + }); + + // 새 플로우: accessToken + sessionId를 URL 파라미터로, refreshToken은 HttpOnly 쿠키로 전달 + const callbackUrl = new URL('/auth/callback', url.origin); + callbackUrl.searchParams.set('accessToken', accessToken); + callbackUrl.searchParams.set('sessionId', user.sessionId); + + return new HttpResponse(null, { + status: 302, + headers: { + Location: callbackUrl.toString(), + 'Set-Cookie': `refreshToken=mock-refresh-token-${user.id}; HttpOnly; Path=/; SameSite=Lax`, }, }); }; diff --git a/src/pages/OAuthCallbackPage.tsx b/src/pages/OAuthCallbackPage.tsx index 1cf1efce..3809cad8 100644 --- a/src/pages/OAuthCallbackPage.tsx +++ b/src/pages/OAuthCallbackPage.tsx @@ -1,9 +1,10 @@ import { useEffect, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import type { JwtPayloadDto } from '@/api/dto'; import { Spinner } from '@/components/common'; import { useAuthStore } from '@/stores/authStore'; -import type { AuthProvider } from '@/types/auth'; +import { parseJwtPayload } from '@/utils/jwt'; export default function OAuthCallbackPage() { const [searchParams] = useSearchParams(); @@ -12,17 +13,11 @@ export default function OAuthCallbackPage() { const isProcessing = useRef(false); useEffect(() => { - // 백엔드에서 리다이렉트 시 전달받은 토큰들 const accessToken = searchParams.get('accessToken'); - const refreshToken = searchParams.get('refreshToken'); - const userId = searchParams.get('userId'); - const userName = searchParams.get('userName'); - const userEmail = searchParams.get('userEmail'); - const sessionId = searchParams.get('sessionId'); - const provider = searchParams.get('provider') as AuthProvider | null; + const sessionIdParam = searchParams.get('sessionId'); - // 필수 파라미터가 없으면 홈으로 - if (!accessToken || !refreshToken || !userId) { + // 필수 파라미터가 없으면 팝업 닫기 또는 홈으로 + if (!accessToken) { if (window.opener) { window.close(); } else { @@ -34,17 +29,20 @@ export default function OAuthCallbackPage() { if (isProcessing.current) return; isProcessing.current = true; - // 로그인 처리 + // JWT 디코딩으로 유저 정보 추출 + const payload = parseJwtPayload(accessToken); + const userId = payload?.id ?? ''; + const userEmail = payload?.email ?? ''; + const sessionId = sessionIdParam ?? payload?.sessionId ?? ''; + login( { id: userId, - email: userEmail || '', - name: userName || '', - sessionId: sessionId || '', - provider: provider || 'google', + email: userEmail, + name: userEmail.split('@')[0] || userEmail, + sessionId, }, accessToken, - refreshToken, ); closeLoginModal(); diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 2d6067ab..edee548c 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -15,7 +15,7 @@ interface AuthState { refreshToken: string | null; isLoginModalOpen: boolean; - login: (user: User, accessToken: string, refreshToken: string) => void; + login: (user: User, accessToken: string) => void; anonymous: (accessToken: string, refreshToken: string) => void; logout: () => void; updateUser: (user: Partial) => void; @@ -32,12 +32,12 @@ export const useAuthStore = create()( refreshToken: null, isLoginModalOpen: false, - login: (user, accessToken, refreshToken) => { + login: (user, accessToken) => { set( { user, accessToken, - refreshToken, + refreshToken: null, }, false, 'auth/login', diff --git a/src/types/auth.ts b/src/types/auth.ts index b293ba81..b504f1ca 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -2,7 +2,7 @@ export type AuthProvider = 'google' | 'kakao' | 'naver'; export interface User { id: string; - name: string; + name?: string; email: string; sessionId: string; provider?: AuthProvider; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 00000000..e85c9886 --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,23 @@ +/** + * JWT payload base64 디코딩 유틸리티 + * + * 서명 검증 불필요 (서버가 검증함), payload만 추출합니다. + */ +export function parseJwtPayload(token: string): T | null { + try { + const base64Url = token.split('.')[1]; + if (!base64Url) return null; + + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const json = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')) + .join(''), + ); + + return JSON.parse(json) as T; + } catch { + return null; + } +} From 8e675749064fddac54fe167cafba1da2739564c6 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 20:30:44 +0900 Subject: [PATCH 78/81] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/endpoints/auth.ts | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/api/endpoints/auth.ts diff --git a/src/api/endpoints/auth.ts b/src/api/endpoints/auth.ts deleted file mode 100644 index 3375e10a..00000000 --- a/src/api/endpoints/auth.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @file auth.ts - * @description 인증 관련 API 엔드포인트 - * - * OAuth 콜백은 서버가 리다이렉트로 처리하므로 클라이언트 API 호출 불필요. - * OAuthCallbackPage에서 URL 파라미터 + JWT 디코딩으로 처리합니다. - */ From c31a9df8c9929a09135cc1c62d0c133ce7f699e0 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 21:50:41 +0900 Subject: [PATCH 79/81] =?UTF-8?q?fix:=20=EB=AA=A8=EB=8B=AC=20=EB=B2=97?= =?UTF-8?q?=EC=96=B4=EB=82=98=EB=A9=B4=20=EC=84=B8=EC=85=98=20=ED=92=80?= =?UTF-8?q?=EB=A6=AC=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 34 +++++++++++++++++++++++++++++++++ src/api/index.ts | 1 - src/pages/OAuthCallbackPage.tsx | 8 ++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index cdf5beee..b4c506b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,46 @@ +import { useEffect } from 'react'; import { RouterProvider } from 'react-router-dom'; +import type { JwtPayloadDto } from '@/api/dto'; import { DevFab } from '@/components/common/DevFab'; import { router } from '@/router'; +import { useAuthStore } from '@/stores/authStore'; import { useThemeListener } from '@/stores/themeStore'; +import { parseJwtPayload } from '@/utils/jwt'; function App() { useThemeListener(); + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.origin !== window.location.origin) return; + const data = event.data; + if (!data || data.type !== 'oauth:callback') return; + + const accessToken = data.accessToken as string | undefined; + if (!accessToken) return; + + const payload = parseJwtPayload(accessToken); + const userId = payload?.id ?? ''; + const userEmail = payload?.email ?? ''; + const sessionId = data.sessionId ?? payload?.sessionId ?? ''; + + useAuthStore.getState().login( + { + id: userId, + email: userEmail, + name: userEmail.split('@')[0] || userEmail, + sessionId, + }, + accessToken, + ); + useAuthStore.getState().closeLoginModal(); + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + return ( <> diff --git a/src/api/index.ts b/src/api/index.ts index f13d2732..6844e209 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,6 +2,5 @@ * API 모듈 배럴 파일 */ export { queryClient, queryKeys } from './queryClient'; -export * from './endpoints/auth'; export * from './endpoints/videos'; export * from './dto'; diff --git a/src/pages/OAuthCallbackPage.tsx b/src/pages/OAuthCallbackPage.tsx index 3809cad8..84ecf330 100644 --- a/src/pages/OAuthCallbackPage.tsx +++ b/src/pages/OAuthCallbackPage.tsx @@ -49,6 +49,14 @@ export default function OAuthCallbackPage() { // 팝업인 경우 닫기, 아니면 홈으로 이동 if (window.opener) { + window.opener.postMessage( + { + type: 'oauth:callback', + accessToken, + sessionId, + }, + window.location.origin, + ); window.close(); } else { navigate('/', { replace: true }); From 61be7cf3d2020bb6f97e974d31ffd85a5a093c86 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 22:13:34 +0900 Subject: [PATCH 80/81] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useSlideCommentsActions.ts | 24 ++++++++++++++++--- .../dev-test/sections/AuthTokenSection.tsx | 19 ++++----------- src/utils/jwt.ts | 10 ++++---- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/hooks/useSlideCommentsActions.ts b/src/hooks/useSlideCommentsActions.ts index 91f87e35..cc2369b2 100644 --- a/src/hooks/useSlideCommentsActions.ts +++ b/src/hooks/useSlideCommentsActions.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { CreateCommentRequestDto } from '@/api'; import { @@ -10,6 +10,7 @@ import { deleteComment as deleteCommentApi, updateComment as updateCommentApi, } from '@/api/endpoints/comments'; +import { queryKeys } from '@/api/queryClient'; import { useSlideStore } from '@/stores/slideStore'; import type { Comment } from '@/types/comment'; import { flatToTree } from '@/utils/comment'; @@ -75,6 +76,7 @@ const EMPTY_COMMENTS: Comment[] = []; export function useSlideCommentsActions() { const { projectId = '' } = useParams<{ projectId: string }>(); const slideId = useSlideStore((state) => state.slide?.slideId); + const queryClient = useQueryClient(); const flatComments = useSlideStore((state) => state.slide?.comments); const addCommentStore = useSlideStore((state) => state.addComment); const addReplyStore = useSlideStore((state) => state.addReply); @@ -104,8 +106,9 @@ export function useSlideCommentsActions() { { slideId, projectId, data: { content } }, { onSuccess: () => { - // 서버가 웹소켓을 보내지 않으므로 수동으로 쿼리 무효화 - // TODO: 서버에서 broadcastNewComment 호출 후 제거 + queryClient.invalidateQueries({ + queryKey: queryKeys.comments.list(slideId), + }); }, onError: () => { setComments(previousComments); @@ -128,6 +131,11 @@ export function useSlideCommentsActions() { createReplyMutate( { commentId: targetServerId, slideId: targetSlideId, projectId, data: { content } }, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.comments.list(targetSlideId), + }); + }, onError: () => { setComments(previousComments); showToast.error('답글 등록에 실패했습니다.', '잠시 후 다시 시도해주세요.'); @@ -158,6 +166,11 @@ export function useSlideCommentsActions() { deleteCommentMutate( { commentId: targetServerId, slideId: targetSlideId, projectId }, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.comments.list(targetSlideId), + }); + }, onError: () => { setComments(previousComments); showToast.error('댓글 삭제에 실패했습니다.', '잠시 후 다시 시도해주세요.'); @@ -188,6 +201,11 @@ export function useSlideCommentsActions() { updateCommentMutate( { commentId: targetServerId, slideId: targetSlideId, projectId, data: { content } }, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.comments.list(targetSlideId), + }); + }, onError: () => { setComments(previousComments); showToast.error('댓글 수정에 실패했습니다.', '잠시 후 다시 시도해주세요.'); diff --git a/src/pages/dev-test/sections/AuthTokenSection.tsx b/src/pages/dev-test/sections/AuthTokenSection.tsx index b3bb1a96..b031c862 100644 --- a/src/pages/dev-test/sections/AuthTokenSection.tsx +++ b/src/pages/dev-test/sections/AuthTokenSection.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { sessionApi } from '@/api/endpoints/session'; import { TextField } from '@/components/common'; import { useAuthStore } from '@/stores/authStore'; +import { parseJwtPayload } from '@/utils/jwt'; import { showToast } from '@/utils/toast'; type JwtDecodeResult = { ok: true; payload: unknown } | { ok: false; reason: string }; @@ -12,22 +13,12 @@ const decodeJwtPayload = (token: string | null): JwtDecodeResult => { return { ok: false, reason: '토큰 없음' }; } - const parts = token.split('.'); - if (parts.length < 2) { - return { ok: false, reason: 'JWT 형식이 아닙니다.' }; - } - - try { - const base64Url = parts[1]; - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '='); - const binary = window.atob(padded); - const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); - const json = new TextDecoder().decode(bytes); - return { ok: true, payload: JSON.parse(json) }; - } catch { + const payload = parseJwtPayload(token); + if (!payload) { return { ok: false, reason: 'payload 파싱 실패' }; } + + return { ok: true, payload }; }; export function AuthTokenSection() { diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index e85c9886..3df5e682 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -9,12 +9,10 @@ export function parseJwtPayload(token: string): T | null { if (!base64Url) return null; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const json = decodeURIComponent( - atob(base64) - .split('') - .map((c) => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')) - .join(''), - ); + const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '='); + const binary = atob(padded); + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); + const json = new TextDecoder().decode(bytes); return JSON.parse(json) as T; } catch { From d2a040b8e2a74374dd38d969e12b9fdc420a6791 Mon Sep 17 00:00:00 2001 From: Andy Hong Date: Mon, 9 Feb 2026 22:37:57 +0900 Subject: [PATCH 81/81] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mocks/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index fa7e46de..5b566b8e 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -748,7 +748,7 @@ const commentHandlers = [ content: body.content, timestampMs: body.timestampMs, userId: MOCK_CURRENT_USER.id, - userName: MOCK_CURRENT_USER.name, + userName: MOCK_CURRENT_USER.name ?? '사용자', createdAt: now, }; const arr = videoComments.get(videoId) ?? [];