'use client'; import { createContext, useContext, useEffect, useState, useRef } from 'react'; import * as signalR from '@microsoft/signalr'; import { fetchApi, throwError } from '@/lib/utils/client'; const SignalRContext = createContext<{ cryptoConnection: signalR.HubConnection | null; chatConnection: signalR.HubConnection | null; cryptoConnected: boolean; chatConnected: boolean; stopConnections: () => Promise; reconnectChat: (accessToken?: string | null) => Promise; }>({ cryptoConnection: null, chatConnection: null, cryptoConnected: false, chatConnected: false, stopConnections: async () => {}, reconnectChat: async () => {} }); type Props = { children: React.ReactNode; accessToken: string|null; signalRCryptoUrl: string; signalRChatUrl: string; } export function SignalRProvider({ children, accessToken, signalRCryptoUrl, signalRChatUrl }: Props) { const cryptoConnectionRef = useRef(null); const chatConnectionRef = useRef(null); const [chatConnection, setChatConnection] = useState(null); const [cryptoConnected, setCryptoConnected] = useState(false); const [chatConnected, setChatConnected] = useState(false); // 초기 렌더 시에만 전달됨. 토큰 갱신 시에는 reconnectChat()을 통해 수동으로 재연결 처리 useEffect(() => { initCryptoConnection(); initChatConnection(accessToken); return () => { stopConnections(); }; }, []); useEffect(() => { if (cryptoConnected) { console.info('SignalR Crypto Connected'); } }, [cryptoConnected]); useEffect(() => { if (chatConnected) { console.info('SignalR Chat Connected'); } }, [chatConnected]); const initCryptoConnection = async () => { try { if (cryptoConnectionRef.current && cryptoConnectionRef.current.state !== signalR.HubConnectionState.Disconnected) { return; } const conn = new signalR.HubConnectionBuilder().withUrl(signalRCryptoUrl).withAutomaticReconnect().build(); await conn.start(); setCryptoConnected(true); cryptoConnectionRef.current = conn; } catch (error) { console.error('SignalR Crypto Connect Failed:', error); } }; 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('Kick', async () => { fetchApi('/api/auth/logout', { method: 'POST' }).then((res) => { throwError(res); 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); } } if (cryptoConnectionRef.current && cryptoConnectionRef.current.state === signalR.HubConnectionState.Connected) { try { await cryptoConnectionRef.current.stop(); setCryptoConnected(false); } catch (error) { console.error('SignalR Crypto Disconnect Failed:', error); } } }; const reconnectChat = async (token?: string | null) => { await initChatConnection(token); }; return ( {children} ) } export function useSignalRContext() { return useContext(SignalRContext); }