From dc97af181639e0601a54d1415add80aa512f2d17 Mon Sep 17 00:00:00 2001 From: HeHelee Date: Mon, 24 Nov 2025 20:03:38 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../serverDashboard/hooks/useAllEquipmentBackgroundSSE.ts | 1 - .../serverView/serverDashboard/hooks/useEquipmentSSE.ts | 6 ------ 2 files changed, 7 deletions(-) diff --git a/src/domains/serverView/serverDashboard/hooks/useAllEquipmentBackgroundSSE.ts b/src/domains/serverView/serverDashboard/hooks/useAllEquipmentBackgroundSSE.ts index 7df8d76..6903748 100644 --- a/src/domains/serverView/serverDashboard/hooks/useAllEquipmentBackgroundSSE.ts +++ b/src/domains/serverView/serverDashboard/hooks/useAllEquipmentBackgroundSSE.ts @@ -49,7 +49,6 @@ export const useAllEquipmentBackgroundSSE = ( equipmentIds: number[], callbacks: BackgroundSSECallbacks ) => { - // ✅ useRef로 콜백 참조 유지 (매번 새로 생성되어도 dependency에 영향 없음) const callbacksRef = useRef(callbacks); useEffect(() => { diff --git a/src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts b/src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts index f1c9098..25edeb8 100644 --- a/src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts +++ b/src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts @@ -1,7 +1,3 @@ -// ============================================================================ -// useEquipmentSSE.ts - 콜백 패턴 (안정적 버전) -// ============================================================================ - import { useEffect, useRef } from "react"; import { EventSourcePolyfill } from "event-source-polyfill"; import { getAccessToken, BASE_URL } from "@/api/client"; @@ -23,7 +19,6 @@ export const useEquipmentSSE = ( callbacks: SSECallbacks, enabled: boolean = true ) => { - // ✅ useRef로 콜백 참조 유지 (매번 새로 생성되어도 dependency에 영향 없음) const callbacksRef = useRef(callbacks); useEffect(() => { @@ -96,6 +91,5 @@ export const useEquipmentSSE = ( error ); } - // ✅ equipmentId와 enabled만 dependency에 포함 (callbacks는 제외) }, [equipmentId, enabled]); }; From 1aef166fdde58e48a6dabc565c12c0b21d0b827d Mon Sep 17 00:00:00 2001 From: HeHelee Date: Tue, 25 Nov 2025 16:22:07 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../serverView/rack/components/RackView.tsx | 11 +- .../serverDashboard/hooks/useEquipmentSSE.ts | 175 ++++++++++++------ 2 files changed, 122 insertions(+), 64 deletions(-) diff --git a/src/domains/serverView/rack/components/RackView.tsx b/src/domains/serverView/rack/components/RackView.tsx index 2faf01f..a98949d 100644 --- a/src/domains/serverView/rack/components/RackView.tsx +++ b/src/domains/serverView/rack/components/RackView.tsx @@ -66,14 +66,11 @@ function RackView({ rackName, serverRoomId, onClose }: RackViewProps) { ); const backgroundSSEEquipmentIds = useMemo(() => { - const shouldStart = - !rackManager.isLoading && selectableEquipmentIds.length > 0; - - if (shouldStart) { - return selectableEquipmentIds; + if (rackManager.isLoading || selectableEquipmentIds.length === 0) { + return []; } - return []; - }, [rackManager.isLoading, selectableEquipmentIds]); + return selectableEquipmentIds.filter((id) => id !== selectedDevice?.id); + }, [rackManager.isLoading, selectableEquipmentIds, selectedDevice?.id]); const backgroundCallbacks = useCallback( () => ({ diff --git a/src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts b/src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts index 25edeb8..73d39da 100644 --- a/src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts +++ b/src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts @@ -1,3 +1,4 @@ +// useEquipmentSSE.ts - 타입 수정 import { useEffect, useRef } from "react"; import { EventSourcePolyfill } from "event-source-polyfill"; import { getAccessToken, BASE_URL } from "@/api/client"; @@ -20,76 +21,136 @@ export const useEquipmentSSE = ( enabled: boolean = true ) => { const callbacksRef = useRef(callbacks); + const reconnectAttemptsRef = useRef(0); + const MAX_RECONNECT_ATTEMPTS = 5; + const RECONNECT_DELAY = 3000; useEffect(() => { callbacksRef.current = callbacks; }, [callbacks]); useEffect(() => { - if (!enabled || !equipmentId) return; + if (!enabled || !equipmentId) { + reconnectAttemptsRef.current = 0; + return; + } const token = getAccessToken(); if (!token) return; const url = `${BASE_URL}/monitoring/subscribe/equipment/${equipmentId}`; + let eventSource: EventSource | null = null; + let reconnectTimeout: number | null = null; // 🔥 여기 수정 - try { - const eventSource = new EventSourcePolyfill(url, { - headers: { - Authorization: `Bearer ${token}`, - }, - withCredentials: true, - }); - - eventSource.addEventListener("system", (event) => { - try { - const data: SystemMonitoringData = JSON.parse(event.data); - callbacksRef.current.onSystemData?.(data); - } catch (error) { - console.error( - `[Equipment ${equipmentId}] System data parse error:`, - error - ); - } - }); - - eventSource.addEventListener("disk", (event) => { - try { - const data: DiskMonitoringData = JSON.parse(event.data); - callbacksRef.current.onDiskData?.(data); - } catch (error) { - console.error( - `[Equipment ${equipmentId}] Disk data parse error:`, - error - ); - } - }); - - eventSource.addEventListener("network", (event) => { - try { - const data: NetworkMonitoringData[] = JSON.parse(event.data); - callbacksRef.current.onNetworkData?.(data); - } catch (error) { - console.error( - `[Equipment ${equipmentId}] Network data parse error:`, - error - ); - } - }); - - eventSource.onerror = (error) => { - console.error(`[Equipment ${equipmentId}] SSE Error:`, error); - eventSource.close(); - }; + const connect = () => { + try { + console.log( + `[Equipment ${equipmentId}] Establishing SSE connection...` + ); + + eventSource = new EventSourcePolyfill(url, { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + heartbeatTimeout: 120000, + }) as EventSource; + + eventSource.addEventListener("system", (event) => { + try { + const data: SystemMonitoringData = JSON.parse(event.data); + reconnectAttemptsRef.current = 0; + callbacksRef.current.onSystemData?.(data); + } catch (error) { + console.error( + `[Equipment ${equipmentId}] System data parse error:`, + error + ); + } + }); + + eventSource.addEventListener("disk", (event) => { + try { + const data: DiskMonitoringData = JSON.parse(event.data); + callbacksRef.current.onDiskData?.(data); + } catch (error) { + console.error( + `[Equipment ${equipmentId}] Disk data parse error:`, + error + ); + } + }); + + eventSource.addEventListener("network", (event) => { + try { + const data: NetworkMonitoringData[] = JSON.parse(event.data); + callbacksRef.current.onNetworkData?.(data); + } catch (error) { + console.error( + `[Equipment ${equipmentId}] Network data parse error:`, + error + ); + } + }); + + eventSource.onerror = (error) => { + console.error(`[Equipment ${equipmentId}] SSE Error:`, error); + + if (eventSource) { + eventSource.close(); + eventSource = null; + } + + callbacksRef.current.onError?.(error); - return () => { + if (reconnectAttemptsRef.current < MAX_RECONNECT_ATTEMPTS) { + reconnectAttemptsRef.current++; + const delay = + RECONNECT_DELAY * Math.pow(2, reconnectAttemptsRef.current - 1); + + console.log( + `[Equipment ${equipmentId}] Reconnecting in ${delay}ms... (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})` + ); + + reconnectTimeout = window.setTimeout(() => { + // 🔥 window.setTimeout으로 명시 + if (enabled) { + connect(); + } + }, delay); + } else { + console.error( + `[Equipment ${equipmentId}] Max reconnection attempts reached` + ); + } + }; + + eventSource.onopen = () => { + console.log(`[Equipment ${equipmentId}] SSE connection established`); + reconnectAttemptsRef.current = 0; + }; + } catch (error) { + console.error( + `[Equipment ${equipmentId}] Failed to create SSE connection:`, + error + ); + } + }; + + connect(); + + return () => { + console.log(`[Equipment ${equipmentId}] Closing SSE connection`); + + if (reconnectTimeout !== null) { + window.clearTimeout(reconnectTimeout); // 🔥 window.clearTimeout으로 명시 + } + + if (eventSource) { eventSource.close(); - }; - } catch (error) { - console.error( - `[Equipment ${equipmentId}] Failed to create SSE connection:`, - error - ); - } + } + + reconnectAttemptsRef.current = 0; + }; }, [equipmentId, enabled]); };