signalrProvider.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. 'use client';
  2. import { createContext, useContext, useEffect, useState, useRef } from 'react';
  3. import * as signalR from '@microsoft/signalr';
  4. import { fetchApi } from '@/lib/utils/client';
  5. export type CrewEvent = {
  6. type: 'invitation'|'consentUpdate'|'started'|'ended'|'toast';
  7. data: any;
  8. };
  9. const SignalRContext = createContext<{
  10. chatConnection: signalR.HubConnection | null;
  11. chatConnected: boolean;
  12. stopConnections: () => Promise<void>;
  13. reconnectChat: (accessToken?: string | null) => Promise<void>;
  14. lastCrewEvent: CrewEvent|null;
  15. }>({
  16. chatConnection: null,
  17. chatConnected: false,
  18. stopConnections: async () => {},
  19. reconnectChat: async () => {},
  20. lastCrewEvent: null
  21. });
  22. type Props = {
  23. children: React.ReactNode;
  24. accessToken: string|null;
  25. signalRChatUrl: string;
  26. }
  27. export function SignalRProvider({ children, accessToken, signalRChatUrl }: Props) {
  28. const chatConnectionRef = useRef<signalR.HubConnection|null>(null);
  29. const [chatConnection, setChatConnection] = useState<signalR.HubConnection|null>(null);
  30. const [chatConnected, setChatConnected] = useState<boolean>(false);
  31. const [lastCrewEvent, setLastCrewEvent] = useState<CrewEvent|null>(null);
  32. // 초기 렌더 시에만 전달됨. 토큰 갱신 시에는 reconnectChat()을 통해 수동으로 재연결 처리
  33. useEffect(() => {
  34. initChatConnection(accessToken);
  35. return () => {
  36. stopConnections();
  37. };
  38. }, []);
  39. useEffect(() => {
  40. if (chatConnected) {
  41. console.info('SignalR Chat Connected');
  42. }
  43. }, [chatConnected]);
  44. const initChatConnection = async (accessToken?: string|null) => {
  45. try {
  46. if (chatConnectionRef.current && chatConnectionRef.current.state !== signalR.HubConnectionState.Disconnected) {
  47. await chatConnectionRef.current.stop();
  48. }
  49. const connectionOptions = accessToken ? { accessTokenFactory: async () => accessToken, withCredentials: true } : {};
  50. const conn = new signalR.HubConnectionBuilder().withUrl(signalRChatUrl, connectionOptions).withAutomaticReconnect().build();
  51. await conn.start();
  52. conn.on('Connected', (message) => {
  53. console.info(message);
  54. });
  55. conn.on('Logout', (message) => {
  56. console.info(message);
  57. });
  58. // ── 크루 이벤트 ──
  59. conn.on('ReceiveCrewInvitation', (data) => {
  60. setLastCrewEvent({ type: 'invitation', data });
  61. });
  62. conn.on('ReceiveCrewConsentUpdate', (data) => {
  63. setLastCrewEvent({ type: 'consentUpdate', data });
  64. });
  65. conn.on('ReceiveCrewStarted', (data) => {
  66. setLastCrewEvent({ type: 'started', data });
  67. });
  68. conn.on('ReceiveCrewEnded', (data) => {
  69. setLastCrewEvent({ type: 'ended', data });
  70. });
  71. conn.on('ReceiveCrewToast', (data) => {
  72. setLastCrewEvent({ type: 'toast', data });
  73. });
  74. conn.on('Kick', async () => {
  75. fetchApi('/api/auth/logout', {
  76. method: 'POST'
  77. }).then(() => {
  78. alert('관리자에 의해 강제 종료되었습니다.');
  79. localStorage.setItem('rememberMe', "false");
  80. localStorage.removeItem('member');
  81. location.replace('/');
  82. });
  83. });
  84. chatConnectionRef.current = conn;
  85. setChatConnection(conn);
  86. setChatConnected(true);
  87. } catch (error) {
  88. console.error('SignalR Chat Connect Failed:', error);
  89. }
  90. };
  91. const stopConnections = async () => {
  92. if (chatConnectionRef.current && chatConnectionRef.current.state === signalR.HubConnectionState.Connected) {
  93. try {
  94. await chatConnectionRef.current.invoke('Logout');
  95. setChatConnected(false);
  96. } catch (error) {
  97. console.error('SignalR Chat Disconnect Failed:', error);
  98. }
  99. }
  100. };
  101. const reconnectChat = async (token?: string | null) => {
  102. await initChatConnection(token);
  103. };
  104. return (
  105. <SignalRContext.Provider value={{
  106. chatConnection,
  107. chatConnected,
  108. stopConnections,
  109. reconnectChat,
  110. lastCrewEvent
  111. }}>
  112. {children}
  113. </SignalRContext.Provider>
  114. )
  115. }
  116. export function useSignalRContext() {
  117. return useContext(SignalRContext);
  118. }