signalrProvider.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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. cryptoConnection: signalR.HubConnection | null;
  7. chatConnection: signalR.HubConnection | null;
  8. cryptoConnected: boolean;
  9. chatConnected: boolean;
  10. stopConnections: () => Promise<void>;
  11. reconnectChat: (accessToken?: string | null) => Promise<void>;
  12. }>({
  13. cryptoConnection: null,
  14. chatConnection: null,
  15. cryptoConnected: false,
  16. chatConnected: false,
  17. stopConnections: async () => {},
  18. reconnectChat: async () => {}
  19. });
  20. type Props = {
  21. children: React.ReactNode;
  22. accessToken: string|null;
  23. signalRCryptoUrl: string;
  24. signalRChatUrl: string;
  25. }
  26. export function SignalRProvider({ children, accessToken, signalRCryptoUrl, signalRChatUrl }: Props) {
  27. const cryptoConnectionRef = useRef<signalR.HubConnection|null>(null);
  28. const chatConnectionRef = useRef<signalR.HubConnection|null>(null);
  29. const [chatConnection, setChatConnection] = useState<signalR.HubConnection|null>(null);
  30. const [cryptoConnected, setCryptoConnected] = useState<boolean>(false);
  31. const [chatConnected, setChatConnected] = useState<boolean>(false);
  32. // 초기 렌더 시에만 전달됨. 토큰 갱신 시에는 reconnectChat()을 통해 수동으로 재연결 처리
  33. useEffect(() => {
  34. initCryptoConnection();
  35. initChatConnection(accessToken);
  36. return () => {
  37. stopConnections();
  38. };
  39. }, []);
  40. useEffect(() => {
  41. if (cryptoConnected) {
  42. console.info('SignalR Crypto Connected');
  43. }
  44. }, [cryptoConnected]);
  45. useEffect(() => {
  46. if (chatConnected) {
  47. console.info('SignalR Chat Connected');
  48. }
  49. }, [chatConnected]);
  50. const initCryptoConnection = async () => {
  51. if (!signalRCryptoUrl) {
  52. console.warn('SIGNALR_CRYPTO_URL not configured — crypto hub disabled');
  53. return;
  54. }
  55. try {
  56. if (cryptoConnectionRef.current && cryptoConnectionRef.current.state !== signalR.HubConnectionState.Disconnected) {
  57. return;
  58. }
  59. const conn = new signalR.HubConnectionBuilder().withUrl(signalRCryptoUrl).withAutomaticReconnect().build();
  60. await conn.start();
  61. setCryptoConnected(true);
  62. cryptoConnectionRef.current = conn;
  63. } catch (error) {
  64. console.error('SignalR Crypto Connect Failed:', error);
  65. }
  66. };
  67. const initChatConnection = async (accessToken?: string|null) => {
  68. if (!signalRChatUrl) {
  69. console.warn('SIGNALR_CHAT_URL not configured — chat hub disabled');
  70. return;
  71. }
  72. try {
  73. if (chatConnectionRef.current && chatConnectionRef.current.state !== signalR.HubConnectionState.Disconnected) {
  74. await chatConnectionRef.current.stop();
  75. }
  76. const connectionOptions = accessToken ? { accessTokenFactory: async () => accessToken, withCredentials: true } : {};
  77. const conn = new signalR.HubConnectionBuilder().withUrl(signalRChatUrl, connectionOptions).withAutomaticReconnect().build();
  78. await conn.start();
  79. conn.on('Connected', (message) => {
  80. console.info(message);
  81. });
  82. conn.on('Logout', (message) => {
  83. console.info(message);
  84. });
  85. conn.on('Kick', async () => {
  86. fetchApi('/api/auth/logout', {
  87. method: 'POST'
  88. }).then((res) => {
  89. throwError(res);
  90. alert('관리자에 의해 강제 종료되었습니다.');
  91. localStorage.setItem('rememberMe', "false");
  92. localStorage.removeItem('member');
  93. location.replace('/');
  94. });
  95. });
  96. chatConnectionRef.current = conn;
  97. setChatConnection(conn);
  98. setChatConnected(true);
  99. } catch (error) {
  100. console.error('SignalR Chat Connect Failed:', error);
  101. }
  102. };
  103. const stopConnections = async () => {
  104. if (chatConnectionRef.current && chatConnectionRef.current.state === signalR.HubConnectionState.Connected) {
  105. try {
  106. await chatConnectionRef.current.invoke('Logout');
  107. setChatConnected(false);
  108. } catch (error) {
  109. console.error('SignalR Chat Disconnect Failed:', error);
  110. }
  111. }
  112. if (cryptoConnectionRef.current && cryptoConnectionRef.current.state === signalR.HubConnectionState.Connected) {
  113. try {
  114. await cryptoConnectionRef.current.stop();
  115. setCryptoConnected(false);
  116. } catch (error) {
  117. console.error('SignalR Crypto Disconnect Failed:', error);
  118. }
  119. }
  120. };
  121. const reconnectChat = async (token?: string | null) => {
  122. await initChatConnection(token);
  123. };
  124. return (
  125. <SignalRContext.Provider value={{
  126. cryptoConnection: cryptoConnectionRef.current,
  127. chatConnection,
  128. cryptoConnected,
  129. chatConnected,
  130. stopConnections,
  131. reconnectChat
  132. }}>
  133. {children}
  134. </SignalRContext.Provider>
  135. )
  136. }
  137. export function useSignalRContext() {
  138. return useContext(SignalRContext);
  139. }