'use client'; import { createContext, useContext, useEffect, useState, useRef } from 'react'; import * as signalR from '@microsoft/signalr'; import { fetchApi } from '@/lib/utils/client'; export type CrewEvent = { type: 'invitation'|'consentUpdate'|'started'|'ended'|'toast'; data: any; }; const SignalRContext = createContext<{ chatConnection: signalR.HubConnection | null; chatConnected: boolean; stopConnections: () => Promise; reconnectChat: (accessToken?: string | null) => Promise; lastCrewEvent: CrewEvent|null; }>({ chatConnection: null, chatConnected: false, stopConnections: async () => {}, reconnectChat: async () => {}, lastCrewEvent: null }); type Props = { children: React.ReactNode; accessToken: string|null; signalRChatUrl: string; } export function SignalRProvider({ children, accessToken, signalRChatUrl }: Props) { const chatConnectionRef = useRef(null); const [chatConnection, setChatConnection] = useState(null); const [chatConnected, setChatConnected] = useState(false); const [lastCrewEvent, setLastCrewEvent] = useState(null); // 초기 렌더 시에만 전달됨. 토큰 갱신 시에는 reconnectChat()을 통해 수동으로 재연결 처리 useEffect(() => { initChatConnection(accessToken); return () => { stopConnections(); }; }, []); useEffect(() => { if (chatConnected) { console.info('SignalR Chat Connected'); } }, [chatConnected]); const initChatConnection = async (accessToken?: string|null) => { try { if (chatConnectionRef.current && chatConnectionRef.current.state !== signalR.HubConnectionState.Disconnected) { await chatConnectionRef.current.stop(); } const connectionOptions = accessToken ? { accessTokenFactory: async () => accessToken, withCredentials: true } : {}; const conn = new signalR.HubConnectionBuilder().withUrl(signalRChatUrl, connectionOptions).withAutomaticReconnect().build(); await conn.start(); conn.on('Connected', (message) => { console.info(message); }); conn.on('Logout', (message) => { console.info(message); }); // ── 크루 이벤트 ── conn.on('ReceiveCrewInvitation', (data) => { setLastCrewEvent({ type: 'invitation', data }); }); conn.on('ReceiveCrewConsentUpdate', (data) => { setLastCrewEvent({ type: 'consentUpdate', data }); }); conn.on('ReceiveCrewStarted', (data) => { setLastCrewEvent({ type: 'started', data }); }); conn.on('ReceiveCrewEnded', (data) => { setLastCrewEvent({ type: 'ended', data }); }); conn.on('ReceiveCrewToast', (data) => { setLastCrewEvent({ type: 'toast', data }); }); conn.on('Kick', async () => { fetchApi('/api/auth/logout', { method: 'POST' }).then(() => { alert('관리자에 의해 강제 종료되었습니다.'); localStorage.setItem('rememberMe', "false"); localStorage.removeItem('member'); location.replace('/'); }); }); chatConnectionRef.current = conn; setChatConnection(conn); setChatConnected(true); } catch (error) { console.error('SignalR Chat Connect Failed:', error); } }; const stopConnections = async () => { if (chatConnectionRef.current && chatConnectionRef.current.state === signalR.HubConnectionState.Connected) { try { await chatConnectionRef.current.invoke('Logout'); setChatConnected(false); } catch (error) { console.error('SignalR Chat Disconnect Failed:', error); } } }; const reconnectChat = async (token?: string | null) => { await initChatConnection(token); }; return ( {children} ) } export function useSignalRContext() { return useContext(SignalRContext); }