실시간 마을 채팅 티커
- 오른쪽 상단에 한 줄씩 뜨는 반투명 채팅, 20초 후 페이드아웃 (최대 6줄) - 💬 버튼으로 입력창 토글, Enter 전송 / ESC 닫기 - SSE 스트림(/api/chat/stream): 접속 즉시 연결, 최근 20개 백로그는 비동기 전송, 25초 하트비트로 프록시 유휴 타임아웃 방지 - 전송(/api/chat/send): 닉네임+토큰 검증(사칭 방지), 1.5초 도배 제한, 120자 제한 - chat_messages 테이블 자동 생성, 인프로세스 EventEmitter 브로드캐스트 (단일 컨테이너) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
@@ -0,0 +1,61 @@
|
||||
import { onChat, recentChat, type ChatMessage } from "@/lib/chat";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/** SSE: 접속 시 최근 20개 → 이후 새 메시지 실시간 푸시 */
|
||||
export async function GET(req: Request) {
|
||||
const encoder = new TextEncoder();
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const push = (msg: ChatMessage) => {
|
||||
try {
|
||||
controller.enqueue(
|
||||
encoder.encode(`id: ${msg.id}\ndata: ${JSON.stringify(msg)}\n\n`),
|
||||
);
|
||||
} catch {
|
||||
/* 이미 닫힘 */
|
||||
}
|
||||
};
|
||||
// 실시간 구독을 먼저 연결 (백로그 DB 조회가 느려도 스트림은 즉시 열림)
|
||||
unsubscribe = onChat(push);
|
||||
controller.enqueue(encoder.encode(": connected\n\n"));
|
||||
void recentChat(20)
|
||||
.then((backlog) => backlog.forEach(push))
|
||||
.catch(() => {
|
||||
/* DB 불가 시 백로그 생략, 실시간만 */
|
||||
});
|
||||
// Traefik/프록시 유휴 타임아웃 방지
|
||||
heartbeat = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(encoder.encode(": ping\n\n"));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, 25000);
|
||||
req.signal.addEventListener("abort", () => {
|
||||
unsubscribe?.();
|
||||
if (heartbeat) clearInterval(heartbeat);
|
||||
try {
|
||||
controller.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
unsubscribe?.();
|
||||
if (heartbeat) clearInterval(heartbeat);
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
새 이슈에서 참조
사용자 차단