치지직 채팅창 인식 추가 및 주사위 횟수 증가 추가.

This commit is contained in:
kimyu
2026-09-16 02:21:39 +09:00
parent 171a4e19f6
commit 7c402dfd8f
22 changed files with 2133 additions and 90 deletions

140
lib/rooms/client.ts Normal file
View File

@@ -0,0 +1,140 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
export type PublicRoomState = {
id: string;
settings: {
chzzkEnabled: boolean;
liveUrl: string;
channelId: string | null;
cheesePerDice: number;
multiplyByCheese: boolean;
maxMultiplier: number;
};
diceQueue: number;
channelName: string | null;
chatChannelId: string | null;
chatStatus: 'idle' | 'connecting' | 'connected' | 'error' | 'disconnected';
chatError: string | null;
lastDonations: Array<{
id: string;
at: number;
payAmount: number;
diceAdded: number;
donatorNickname: string;
donationText?: string;
simulated?: boolean;
}>;
chatFeed: Array<{
id: string;
at: number;
nickname: string;
message: string;
kind: 'chat' | 'donation';
payAmount?: number;
}>;
};
export function useRoom(roomId: string | null) {
const [room, setRoom] = useState<PublicRoomState | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!roomId) return;
const clientId =
typeof crypto !== 'undefined' && 'randomUUID' in crypto
? crypto.randomUUID()
: `c-${Date.now()}-${Math.random().toString(16).slice(2)}`;
void fetch(`/api/rooms/${roomId}`)
.then((r) => r.json())
.then((j) => {
if (j.room) setRoom(j.room as PublicRoomState);
else setError(j.error || '방을 불러오지 못했습니다.');
})
.catch(() => setError('방을 불러오지 못했습니다.'));
const es = new EventSource(
`/api/rooms/${roomId}/events?clientId=${encodeURIComponent(clientId)}`,
);
es.addEventListener('room', (ev) => {
try {
setRoom(JSON.parse((ev as MessageEvent).data) as PublicRoomState);
setError(null);
} catch {
// ignore
}
});
es.onerror = () => {
// keep last state; browser will retry SSE
};
const leave = () => {
try {
const payload = JSON.stringify({ clientId });
if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
navigator.sendBeacon(
`/api/rooms/${roomId}/leave`,
new Blob([payload], { type: 'application/json' }),
);
} else {
void fetch(`/api/rooms/${roomId}/leave`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
keepalive: true,
});
}
} catch {
// ignore
}
};
window.addEventListener('pagehide', leave);
return () => {
window.removeEventListener('pagehide', leave);
es.close();
leave();
};
}, [roomId]);
const consumeDice = useCallback(
async (count = 1) => {
if (!roomId) return { ok: true as const };
const res = await fetch(`/api/rooms/${roomId}/consume-dice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ count }),
});
const json = await res.json();
if (!res.ok) {
return {
ok: false as const,
error: (json.error as string) || '주사위를 소모할 수 없습니다.',
};
}
setRoom(json.room as PublicRoomState);
return { ok: true as const };
},
[roomId],
);
const simulateDonation = useCallback(
async (payAmount: number) => {
if (!roomId) return;
const res = await fetch(`/api/rooms/${roomId}/simulate-donation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payAmount }),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error || '시뮬레이션 실패');
setRoom(json.room as PublicRoomState);
},
[roomId],
);
return { room, error, consumeDice, simulateDonation };
}