'use client'; import { createContext, useContext, useEffect, useState, useRef } from 'react'; import * as signalR from '@microsoft/signalr'; import { fetchApi } from '@/lib/utils/client'; type FeedPostEvent = { postID: number; authorSID: string; authorName: string|null; authorThumb: string|null; subject: string; thumbnail: string|null; createdAt: string; }; // Backend hub 매핑 // chatConnection → /hubs/chat (ChatHub) — 채팅, 향후 1:1 실시간 문의 // appConnection → /hubs/app (AppHub) — 알림, 쪽지, 피드 토스트 const SignalRContext = createContext<{ chatConnection: signalR.HubConnection|null; chatConnected: boolean; appConnection: signalR.HubConnection|null; appConnected: boolean; stopConnections: () => Promise; reconnectChat: (accessToken?: string|null) => Promise; reconnectApp: (accessToken?: string|null) => Promise; lastFeedPost: FeedPostEvent|null; clearLastFeedPost: () => void; }>({ chatConnection: null, chatConnected: false, appConnection: null, appConnected: false, stopConnections: async () => {}, reconnectChat: async () => {}, reconnectApp: async () => {}, lastFeedPost: null, clearLastFeedPost: () => {} }); type Props = { children: React.ReactNode; accessToken: string|null; signalRChatUrl: string; signalRAppUrl: string; } export function SignalRProvider({ children, accessToken, signalRChatUrl, signalRAppUrl }: Props) { const chatConnectionRef = useRef(null); const appConnectionRef = useRef(null); const [chatConnection, setChatConnection] = useState(null); const [chatConnected, setChatConnected] = useState(false); const [appConnection, setAppConnection] = useState(null); const [appConnected, setAppConnected] = useState(false); const [lastFeedPost, setLastFeedPost] = useState(null); // 초기 1회만 시작 — 토큰 갱신 시에는 reconnectChat / reconnectApp 으로 수동 재연결 useEffect(() => { initChatConnection(accessToken); initAppConnection(accessToken); return () => { stopConnections(); }; }, []); useEffect(() => { if (chatConnected) { console.info('SignalR Chat Connected'); } }, [chatConnected]); useEffect(() => { if (appConnected) { console.info('SignalR App Connected'); } }, [appConnected]); const buildConn = (url: string, token: string|null|undefined) => { const options = token ? { accessTokenFactory: async () => token, withCredentials: true } : {}; return new signalR.HubConnectionBuilder().withUrl(url, options).withAutomaticReconnect().build(); }; const initChatConnection = async (token?: string|null) => { if (!signalRChatUrl) { console.warn('SIGNALR_CHAT_URL not configured — chat hub disabled'); return; } try { if (chatConnectionRef.current && chatConnectionRef.current.state !== signalR.HubConnectionState.Disconnected) { await chatConnectionRef.current.stop(); } const conn = buildConn(signalRChatUrl, token); await conn.start(); conn.on('Connected', (message) => { console.info('[chat]', message); }); conn.on('Logout', (message) => { console.info('[chat]', message); }); chatConnectionRef.current = conn; setChatConnection(conn); setChatConnected(true); } catch (error) { console.error('SignalR Chat Connect Failed:', error); } }; const initAppConnection = async (token?: string|null) => { if (!signalRAppUrl) { console.warn('SIGNALR_APP_URL not configured — app hub disabled'); return; } try { if (appConnectionRef.current && appConnectionRef.current.state !== signalR.HubConnectionState.Disconnected) { await appConnectionRef.current.stop(); } const conn = buildConn(signalRAppUrl, token); await conn.start(); conn.on('Connected', (message) => { console.info('[app]', message); }); conn.on('Logout', (message) => { console.info('[app]', message); }); // ── 피드 이벤트 ── conn.on('ReceiveFeedPost', (data: FeedPostEvent) => { setLastFeedPost(data); }); // ── 강제 종료 ── conn.on('Kick', async () => { fetchApi('/api/auth/logout', { method: 'POST' }).then(() => { alert('관리자에 의해 강제 종료되었습니다.'); localStorage.setItem('rememberMe', "false"); localStorage.removeItem('member'); location.replace('/'); }); }); appConnectionRef.current = conn; setAppConnection(conn); setAppConnected(true); } catch (error) { console.error('SignalR App 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 (appConnectionRef.current && appConnectionRef.current.state === signalR.HubConnectionState.Connected) { try { await appConnectionRef.current.invoke('Logout'); setAppConnected(false); } catch (error) { console.error('SignalR App Disconnect Failed:', error); } } }; const reconnectChat = async (token?: string|null) => { await initChatConnection(token); }; const reconnectApp = async (token?: string|null) => { await initAppConnection(token); }; return ( setLastFeedPost(null) }}> {children} ) } export function useSignalRContext() { return useContext(SignalRContext); }