11'use client' ;
22
3+ import { LinkIcon } from '@/feature/meeting/components/icons' ;
34import { MeetingControlBar } from '@/feature/meeting/components/MeetingControlBar' ;
45import { RemoteAudioPlayer } from '@/feature/meeting/components/MeetingMedia' ;
56import { VideoStage } from '@/feature/meeting/components/VideoStage' ;
67import type { UseMediasoupViewModel } from '@/feature/meeting/hooks/useMediasoupViewModel' ;
78import { useMeetingElapsedViewModel } from '@/feature/meeting/hooks/useMeetingElapsedViewModel' ;
89import type { MeetingLayoutVariant } from '@/feature/meeting/hooks/useMeetingLayoutViewModel' ;
10+ import {
11+ type CopyLinkStatus ,
12+ useMeetingLinkViewModel ,
13+ } from '@/feature/meeting/hooks/useMeetingLinkViewModel' ;
914import type {
1015 MeetingConnectionStatus ,
1116 UseMeetingViewModel ,
@@ -22,6 +27,46 @@ const STATUS_TONE_DEFAULT = { text: 'text-muted', dot: 'bg-pending' } as const;
2227const statusLabel = ( status : MeetingConnectionStatus ) : string =>
2328 status === 'joined' ? '연결됨' : status === 'reconnecting' ? '재접속 중…' : status ;
2429
30+ const COPY_FEEDBACK : Record < Exclude < CopyLinkStatus , 'idle' > , string > = {
31+ copied : '링크 복사됨' ,
32+ error : '복사하지 못했습니다' ,
33+ } ;
34+
35+ function MeetingTitle ( { code, title } : { readonly code : string ; readonly title : string | null } ) {
36+ const { url, status, copy } = useMeetingLinkViewModel ( code ) ;
37+ const label = title ?? `회의 ${ code } ` ;
38+ return (
39+ < h1 className = "text-title flex min-w-0 items-center gap-2.5 font-bold tracking-[-0.025em]" >
40+ < button
41+ type = "button"
42+ onClick = { copy }
43+ title = { url }
44+ aria-label = { `${ label } — 회의 링크 복사` }
45+ className = "text-text hover:text-accent-on group flex min-w-0 items-center gap-2 transition-colors"
46+ >
47+ < span className = "truncate" >
48+ { title ?? (
49+ < >
50+ 회의 < span className = "text-code font-mono font-medium" > { code } </ span >
51+ </ >
52+ ) }
53+ </ span >
54+ < LinkIcon className = "text-muted group-hover:text-accent-on h-4 w-4 shrink-0 transition-colors" />
55+ </ button >
56+ { status !== 'idle' && (
57+ < span
58+ role = "status"
59+ className = { `text-meta shrink-0 font-semibold ${
60+ status === 'copied' ? 'text-positive' : 'text-danger-on'
61+ } `}
62+ >
63+ { COPY_FEEDBACK [ status ] }
64+ </ span >
65+ ) }
66+ </ h1 >
67+ ) ;
68+ }
69+
2570/**
2671 * 경과 시간은 1초마다 바뀐다. 헤더에 인라인으로 두면 그 틱이 회의 화면 전체를 —
2772 * 참가자 수만큼 깔린 비디오 타일까지 — 다시 그린다. 상태를 이 경계 안에 가둔다.
@@ -92,13 +137,10 @@ export function MeetingScreen({
92137 < section className = "flex h-full min-w-0 flex-1 flex-col" >
93138 < header className = "border-border px-gutter py-gutter-sm flex items-center justify-between gap-3 border-b" >
94139 < div className = "min-w-0" >
95- < h1 className = "text-text text-title truncate font-bold tracking-[-0.025em]" >
96- { title ?? (
97- < >
98- 회의 < span className = "text-code font-mono font-medium" > { code } </ span >
99- </ >
100- ) }
101- </ h1 >
140+ < MeetingTitle
141+ code = { code }
142+ title = { title }
143+ />
102144 < p className = "text-muted text-meta mt-1 truncate font-medium" >
103145 { title !== null && < span className = "font-mono" > { code } · </ span > } 내 닉네임:{ ' ' }
104146 { nickname ?? '(미인증)' } · 참가자 { participantCount } 명
0 commit comments