import { cancelScheduledDisconnectRoomChat, scheduleDisconnectRoomChat, } from '@/lib/chzzk/chat-session'; 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 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) { // 재입장 시 예약된 disconnect 취소 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', }, }); }