signalrProvider.tsx 3.1 KB

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