'use client'; import { useEffect, useRef, useState, useCallback } from 'react'; import * as signalR from '@microsoft/signalr'; import { DonationAlertData, DonationRemoteState } from '@/types/donation'; type AlertQueueItem = DonationAlertData & { status: 'queued'|'playing'|'done' }; export function useDonationAlert( widgetToken: string, hubUrl: string, getDurationSec?: (alert: DonationAlertData) => number ) { const connectionRef = useRef(null); const [connected, setConnected] = useState(false); const [queue, setQueue] = useState([]); const [current, setCurrent] = useState(null); const [remoteState, setRemoteState] = useState({ isPaused: false, isAccepting: true, isAudioOnly: false, isVideoOnly: false }); const [skipSignal, setSkipSignal] = useState(0); // SignalR 연결 useEffect(() => { const conn = new signalR.HubConnectionBuilder() .withUrl(hubUrl) .withAutomaticReconnect() .build(); conn.on('ReceiveAlert', (data: DonationAlertData) => { setQueue(prev => { // 같은 alertID가 이미 큐에 있으면 무시 (중복 broadcast 방어) if (prev.some(q => q.alertID === data.alertID)) { return prev; } return [...prev, { ...data, status: 'queued' }]; }); }); conn.on('ReceiveSkip', () => { setSkipSignal(prev => prev + 1); }); conn.on('ReceivePause', (isPaused: boolean) => { setRemoteState(prev => ({ ...prev, isPaused })); }); conn.on('ReceiveState', (state: DonationRemoteState) => { setRemoteState(state); }); // 리모콘에서 큐 순서 변경 — 두 알림 위치 swap conn.on('ReceiveQueueReorder', (data: { swap?: number[] }) => { if (!data?.swap || data.swap.length !== 2) { return; } const [a, b] = data.swap; setQueue(prev => { const idxA = prev.findIndex(q => q.alertID === a); const idxB = prev.findIndex(q => q.alertID === b); if (idxA < 0 || idxB < 0) { return prev; } const next = [...prev]; [next[idxA], next[idxB]] = [next[idxB], next[idxA]]; return next; }); }); conn.start().then(() => { conn.invoke('JoinChannel', widgetToken); setConnected(true); }).catch(err => { console.error('[DonationHub] Connect failed:', err); }); conn.onreconnected(() => { conn.invoke('JoinChannel', widgetToken); setConnected(true); }); conn.onclose(() => setConnected(false)); connectionRef.current = conn; return () => { conn.stop(); }; }, [widgetToken, hubUrl]); // 큐 처리 — 일시정지가 아닐 때 다음 알림 꺼내기 useEffect(() => { if (current || remoteState.isPaused || queue.length === 0) { return; } const next = queue[0]; setQueue(prev => prev.slice(1)); setCurrent({ ...next, status: 'playing' }); // 알림 재생 시작 보고 — duration 동봉 시 리모콘 타이머 ring 표시 가능 const durationSec = getDurationSec?.(next); connectionRef.current?.invoke('AlertDelivered', next.alertID, durationSec ?? null).catch(() => {}); }, [queue, current, remoteState.isPaused, getDurationSec]); // 스킵 시그널 처리 useEffect(() => { if (skipSignal > 0 && current) { setCurrent(null); } }, [skipSignal]); // 알림 완료 콜백 — backend에 보고 + state 클리어 const onAlertComplete = useCallback(() => { setCurrent(prev => { if (prev) { connectionRef.current?.invoke('AlertCompleted', prev.alertID).catch(() => {}); } return null; }); }, []); return { connected, current, queue, remoteState, skipSignal, onAlertComplete }; }