import { cancelScheduledDisconnectRoomChat, scheduleDisconnectRoomChat, } from '@/lib/chzzk/chat-session'; import { assertChannelRoomAccess } from '@/lib/auth/room-access'; import { addRoomViewer, getRoom, removeRoomViewer, subscribeRoom, toPublicRoom, type PublicRoomState, } from '@/lib/rooms/store'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; type Params = { params: Promise<{ roomId: string }> }; export async function GET(request: Request, { params }: Params) { const { roomId } = await params; const room = getRoom(roomId); if (!room) { return new Response(JSON.stringify({ error: '방을 찾을 수 없습니다.' }), { status: 404, headers: { 'Content-Type': 'application/json' }, }); } const access = await assertChannelRoomAccess(room, request); if (!access.ok) return access.response; const clientId = new URL(request.url).searchParams.get('clientId')?.trim() || crypto.randomUUID(); const encoder = new TextEncoder(); let unsubscribe: (() => void) | null = null; let heartbeat: ReturnType | null = null; let presenceHeld = false; const release = () => { if (!presenceHeld) return; presenceHeld = false; const { emptied } = removeRoomViewer(roomId, clientId); if (emptied) { scheduleDisconnectRoomChat(roomId); } }; const stream = new ReadableStream({ start(controller) { cancelScheduledDisconnectRoomChat(roomId); addRoomViewer(roomId, clientId); presenceHeld = true; const send = (event: string, data: unknown) => { controller.enqueue( encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`), ); }; send('room', toPublicRoom(room)); unsubscribe = subscribeRoom(roomId, (state: PublicRoomState) => { send('room', state); }); heartbeat = setInterval(() => { controller.enqueue(encoder.encode(`: ping\n\n`)); }, 15000); }, cancel() { if (unsubscribe) unsubscribe(); if (heartbeat) clearInterval(heartbeat); release(); }, }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache, no-transform', Connection: 'keep-alive', }, }); }