useDonationAlert.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. 'use client';
  2. import { useEffect, useRef, useState, useCallback } from 'react';
  3. import * as signalR from '@microsoft/signalr';
  4. import { DonationAlertData, DonationRemoteState } from '@/types/donation';
  5. type AlertQueueItem = DonationAlertData & { status: 'queued'|'playing'|'done' };
  6. export function useDonationAlert(
  7. widgetToken: string,
  8. hubUrl: string,
  9. getDurationSec?: (alert: DonationAlertData) => number
  10. ) {
  11. const connectionRef = useRef<signalR.HubConnection|null>(null);
  12. const [connected, setConnected] = useState(false);
  13. const [queue, setQueue] = useState<AlertQueueItem[]>([]);
  14. const [current, setCurrent] = useState<AlertQueueItem|null>(null);
  15. const [remoteState, setRemoteState] = useState<DonationRemoteState>({
  16. isPaused: false,
  17. isAccepting: true,
  18. isAudioOnly: false,
  19. isVideoOnly: false
  20. });
  21. const [skipSignal, setSkipSignal] = useState(0);
  22. // SignalR 연결
  23. useEffect(() => {
  24. const conn = new signalR.HubConnectionBuilder()
  25. .withUrl(hubUrl)
  26. .withAutomaticReconnect()
  27. .build();
  28. conn.on('ReceiveAlert', (data: DonationAlertData) => {
  29. setQueue(prev => {
  30. // 같은 alertID가 이미 큐에 있으면 무시 (중복 broadcast 방어)
  31. if (prev.some(q => q.alertID === data.alertID)) {
  32. return prev;
  33. }
  34. return [...prev, { ...data, status: 'queued' }];
  35. });
  36. });
  37. conn.on('ReceiveSkip', () => {
  38. setSkipSignal(prev => prev + 1);
  39. });
  40. conn.on('ReceivePause', (isPaused: boolean) => {
  41. setRemoteState(prev => ({ ...prev, isPaused }));
  42. });
  43. conn.on('ReceiveState', (state: DonationRemoteState) => {
  44. setRemoteState(state);
  45. });
  46. // 리모콘에서 큐 순서 변경 — 두 알림 위치 swap
  47. conn.on('ReceiveQueueReorder', (data: { swap?: number[] }) => {
  48. if (!data?.swap || data.swap.length !== 2) {
  49. return;
  50. }
  51. const [a, b] = data.swap;
  52. setQueue(prev => {
  53. const idxA = prev.findIndex(q => q.alertID === a);
  54. const idxB = prev.findIndex(q => q.alertID === b);
  55. if (idxA < 0 || idxB < 0) {
  56. return prev;
  57. }
  58. const next = [...prev];
  59. [next[idxA], next[idxB]] = [next[idxB], next[idxA]];
  60. return next;
  61. });
  62. });
  63. conn.start().then(() => {
  64. conn.invoke('JoinChannel', widgetToken);
  65. setConnected(true);
  66. }).catch(err => {
  67. console.error('[DonationHub] Connect failed:', err);
  68. });
  69. conn.onreconnected(() => {
  70. conn.invoke('JoinChannel', widgetToken);
  71. setConnected(true);
  72. });
  73. conn.onclose(() => setConnected(false));
  74. connectionRef.current = conn;
  75. return () => {
  76. conn.stop();
  77. };
  78. }, [widgetToken, hubUrl]);
  79. // 큐 처리 — 일시정지가 아닐 때 다음 알림 꺼내기
  80. useEffect(() => {
  81. if (current || remoteState.isPaused || queue.length === 0) {
  82. return;
  83. }
  84. const next = queue[0];
  85. setQueue(prev => prev.slice(1));
  86. setCurrent({ ...next, status: 'playing' });
  87. // 알림 재생 시작 보고 — duration 동봉 시 리모콘 타이머 ring 표시 가능
  88. const durationSec = getDurationSec?.(next);
  89. connectionRef.current?.invoke('AlertDelivered', next.alertID, durationSec ?? null).catch(() => {});
  90. }, [queue, current, remoteState.isPaused, getDurationSec]);
  91. // 스킵 시그널 처리
  92. useEffect(() => {
  93. if (skipSignal > 0 && current) {
  94. setCurrent(null);
  95. }
  96. }, [skipSignal]);
  97. // 알림 완료 콜백 — backend에 보고 + state 클리어
  98. const onAlertComplete = useCallback(() => {
  99. setCurrent(prev => {
  100. if (prev) {
  101. connectionRef.current?.invoke('AlertCompleted', prev.alertID).catch(() => {});
  102. }
  103. return null;
  104. });
  105. }, []);
  106. return {
  107. connected,
  108. current,
  109. queue,
  110. remoteState,
  111. skipSignal,
  112. onAlertComplete
  113. };
  114. }