Skip to content

Commit d65d972

Browse files
yuchem2claude
andcommitted
fix(frontend): 동적 라우트 코드를 URL 에서 읽기 (정적 export placeholder 대응)
CloudFront 가 동적 경로에 placeholder HTML 을 서빙하면 useParams() 가 빌드 타임 'placeholder' 를 반환 → meeting:join/getRtpCapabilities 에 code='placeholder' 가 실려 validation 실패(8자 초과)·입장 불가. URL pathname 에서 직접 segment 를 읽는 useRouteSegment 훅 도입(window 없으면 useParams 폴백) → meetings/[code], reports/[id] 적용. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6002ce3 commit d65d972

3 files changed

Lines changed: 37 additions & 9 deletions

File tree

‎apps/frontend/src/app/meetings/[code]/MeetingPageClient.tsx‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
'use client';
22

3-
import { useParams } from 'next/navigation';
4-
53
import { ChatPanel } from '@/feature/meeting/components/ChatPanel';
64
import { MeetingScreen } from '@/feature/meeting/components/MeetingScreen';
75
import { NicknameGate } from '@/feature/meeting/components/NicknameGate';
@@ -10,11 +8,12 @@ import { useMediasoupViewModel } from '@/feature/meeting/hooks/useMediasoupViewM
108
import { useMeetingLayoutViewModel } from '@/feature/meeting/hooks/useMeetingLayoutViewModel';
119
import { useMeetingViewModel } from '@/feature/meeting/hooks/useMeetingViewModel';
1210
import { useNicknameGateViewModel } from '@/feature/meeting/hooks/useNicknameGateViewModel';
11+
import { useRouteSegment } from '@/shared/hooks/useRouteSegment';
1312

1413
/**
1514
* `/meetings/[code]` 의 client wrapper.
1615
*
17-
* `useParams` 로 동적 segment 를 읽고 세 ViewModel hook 을 합성한다:
16+
* URL 에서 회의 코드를 읽고(`useRouteSegment` — 정적 export placeholder 대응) 세 ViewModel hook 을 합성한다:
1817
* - `useMeetingViewModel` 이 socket 을 만들고 join/참가자 목록 담당
1918
* - `useMediasoupViewModel` 이 그 socket 위에서 RTP capabilities/Transport 준비
2019
* - `useChatViewModel` 이 그 socket 을 받아 채팅 emit/수신 담당
@@ -23,8 +22,7 @@ import { useNicknameGateViewModel } from '@/feature/meeting/hooks/useNicknameGat
2322
* 와 client hook 사용이 동일 파일에서 공존할 수 없기 때문이다.
2423
*/
2524
export function MeetingPageClient() {
26-
const params = useParams<{ code: string }>();
27-
const code = typeof params?.code === 'string' ? params.code : '';
25+
const code = useRouteSegment('meetings', 'code');
2826
const meetingVm = useMeetingViewModel(code);
2927
const mediasoupVm = useMediasoupViewModel(meetingVm.socket, code);
3028
const chatVm = useChatViewModel(meetingVm.socket, code);

‎apps/frontend/src/app/reports/[id]/ReportDetailPageClient.tsx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
'use client';
22

33
import Link from 'next/link';
4-
import { useParams } from 'next/navigation';
54

65
import { ReportDetail } from '@/feature/reports/components/ReportDetail';
76
import { useReportDetailViewModel } from '@/feature/reports/hooks/useReportDetailViewModel';
7+
import { useRouteSegment } from '@/shared/hooks/useRouteSegment';
88

99
/**
1010
* `/reports/[id]` 의 client wrapper.
11-
* useParams 로 dynamic segment 를 읽고 ViewModel + View 를 합성한다.
11+
* URL 에서 회의록 id 를 읽고(`useRouteSegment` — 정적 export placeholder 대응) ViewModel + View 를 합성한다.
1212
*/
1313
export function ReportDetailPageClient() {
14-
const params = useParams<{ id: string }>();
15-
const id = typeof params?.id === 'string' ? params.id : '';
14+
const id = useRouteSegment('reports', 'id');
1615
const vm = useReportDetailViewModel(id);
1716
return (
1817
<main className="min-h-screen bg-bg px-4 py-10">
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client';
2+
3+
import { useParams } from 'next/navigation';
4+
5+
/**
6+
* 동적 라우트 segment 값을 **브라우저 URL 에서 직접** 읽는다.
7+
*
8+
* 정적 export(`output: 'export'`)의 동적 라우트는 `generateStaticParams` 의
9+
* placeholder HTML 한 장만 빌드되고, 운영에선 CloudFront 가 그 placeholder HTML 을
10+
* 실제 코드 경로(`/meetings/{code}`)에 서빙한다. 이때 `useParams()` 는 URL 이 아니라
11+
* **빌드 타임 값(`'placeholder'`)** 을 돌려주므로 그대로 쓰면 안 된다.
12+
* 따라서 클라이언트에서는 `window.location.pathname` 에서 실제 값을 읽고,
13+
* window 가 없는 빌드/SSR/테스트 환경에서는 `useParams()` 로 폴백한다.
14+
*
15+
* @param parentSegment 동적 segment 바로 앞의 고정 경로 (예: `'meetings'`, `'reports'`)
16+
* @param paramKey `useParams` 폴백 시 쓸 키 (예: `'code'`, `'id'`)
17+
*/
18+
export function useRouteSegment(parentSegment: string, paramKey: string): string {
19+
const params = useParams<Record<string, string>>();
20+
21+
if (typeof window !== 'undefined') {
22+
const segments = window.location.pathname.split('/').filter(Boolean);
23+
const idx = segments.indexOf(parentSegment);
24+
if (idx >= 0 && segments[idx + 1]) {
25+
return decodeURIComponent(segments[idx + 1]);
26+
}
27+
}
28+
29+
const fallback = params?.[paramKey];
30+
return typeof fallback === 'string' ? fallback : '';
31+
}

0 commit comments

Comments
 (0)