| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- '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<signalR.HubConnection|null>(null);
- const [connected, setConnected] = useState(false);
- const [queue, setQueue] = useState<AlertQueueItem[]>([]);
- const [current, setCurrent] = useState<AlertQueueItem|null>(null);
- const [remoteState, setRemoteState] = useState<DonationRemoteState>({
- 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
- };
- }
|