Skip to content
Merged

Dev #183

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions src/domains/serverView/rack/components/RackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Sidebar from "./Sidebar";
import RackHeader from "./RackHeader";
import Button from "./Button";
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useCallback } from "react";
import ServerDashboard from "@domains/serverView/serverDashboard/components/ServerDashboard";
import { useMonitoringStore } from "../../serverDashboard/stores/monitoringStore";
import { useEquipmentSSE } from "../../serverDashboard/hooks/useEquipmentSSE";
Expand All @@ -25,7 +25,14 @@
name: string;
} | null>(null);

const { deviceMetricsMap, setSelectedDeviceId } = useMonitoringStore();
const {
deviceMetricsMap,
setSelectedDeviceId,
setSystemData,
setDiskData,
setNetworkData,
setDeviceMetrics,
} = useMonitoringStore();
const { user } = useAuthStore();
const view = user?.role === "VIEWER";

Expand Down Expand Up @@ -68,10 +75,44 @@
return [];
}, [rackManager.isLoading, selectableEquipmentIds]);

useAllEquipmentBackgroundSSE(backgroundSSEEquipmentIds);
const backgroundCallbacks = useCallback(
() => ({
onMetricsUpdate: setDeviceMetrics,
onConnectionError: (equipmentId: number, error: Event) => {
console.error(
`[Rack ${rackId}] Background SSE error for equipment ${equipmentId}:`,
error
);
},
}),
[rackId, setDeviceMetrics]
);

const equipmentCallbacks = useCallback(
() => ({
onSystemData: setSystemData,
onDiskData: setDiskData,
onNetworkData: setNetworkData,
onError: (error: Event) => {
console.error(
`[Rack ${rackId}] Equipment SSE error for device ${selectedDevice?.id}:`,
error
);
},
}),
[rackId, selectedDevice?.id, setSystemData, setDiskData, setNetworkData]
);

// 백그라운드 메트릭 수집 (모든 장비)
useAllEquipmentBackgroundSSE(
backgroundSSEEquipmentIds,
backgroundCallbacks()
);

// 선택된 장비 상세 데이터 수집
useEquipmentSSE(
selectedDevice?.id || 0,
selectedDevice?.id || null,
equipmentCallbacks(),
dashboardOpen && selectedDevice?.id !== 0
);

Expand All @@ -89,7 +130,7 @@
if (view && editMode) {
setEditMode(false);
}
}, [view]);

Check warning on line 133 in src/domains/serverView/rack/components/RackView.tsx

View workflow job for this annotation

GitHub Actions / test

React Hook useEffect has a missing dependency: 'editMode'. Either include it or remove the dependency array

const displayRackName = rackManager.rack?.rackName || rackName || "N/A";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { EventSourcePolyfill } from "event-source-polyfill";
import { getAccessToken, BASE_URL } from "@/api/client";
import type { DiskMonitoringData, SystemMonitoringData } from "../types";
import {
useMonitoringStore,
type SimpleMetrics,
} from "../stores/monitoringStore";

interface SSEEvent extends Event {
data: string;
}

export interface SimpleMetrics {
cpu: number;
memory: number;
disk: number;
}

interface BackgroundSSECallbacks {
onMetricsUpdate: (equipmentId: number, metrics: SimpleMetrics) => void;
onConnectionError?: (equipmentId: number, error: Event) => void;
}

const isSystemMonitoringData = (
data: unknown
): data is SystemMonitoringData => {
Expand All @@ -21,7 +28,6 @@ const isDiskMonitoringData = (data: unknown): data is DiskMonitoringData => {
return typeof data === "object" && data !== null && "usedPercentage" in data;
};

// 메트릭 추출 함수
const extractMetricsFromSystemData = (
systemData: SystemMonitoringData | null,
diskData: DiskMonitoringData | null
Expand All @@ -39,15 +45,18 @@ const extractMetricsFromSystemData = (
};
};

// 여러 장비의 백그라운드 메트릭을 동시에 수집합니다
export const useAllEquipmentBackgroundSSE = (
equipmentIds: number[],
callbacks: BackgroundSSECallbacks
) => {
// ✅ useRef로 콜백 참조 유지 (매번 새로 생성되어도 dependency에 영향 없음)
const callbacksRef = useRef(callbacks);

export const useAllEquipmentBackgroundSSE = (equipmentIds: number[]) => {
const setDeviceMetrics = useMonitoringStore(
(state) => state.setDeviceMetrics
);
useEffect(() => {
callbacksRef.current = callbacks;
}, [callbacks]);

useEffect(() => {
// equipmentIds가 없으면 리턴
if (!equipmentIds || equipmentIds.length === 0) {
return;
}
Expand All @@ -57,14 +66,16 @@ export const useAllEquipmentBackgroundSSE = (equipmentIds: number[]) => {
return;
}

// 각 장비별 SSE 연결 저장
const eventSourceMap = new Map<number, EventSource>();

const connectToEquipment = (equipmentId: number) => {
try {
const url = `${BASE_URL}/monitoring/subscribe/equipment/${equipmentId}`;

let latestSystemData: SystemMonitoringData | null = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAY = 3000;

const eventSource = new EventSourcePolyfill(url, {
headers: {
Expand All @@ -81,9 +92,17 @@ export const useAllEquipmentBackgroundSSE = (equipmentIds: number[]) => {

if (isSystemMonitoringData(parsedData)) {
latestSystemData = parsedData;
} else {
console.warn(
`[Equipment ${equipmentId}] Invalid system data:`,
parsedData
);
}
} catch (error) {
console.error(error);
console.error(
`[Equipment ${equipmentId}] System data parse error:`,
error
);
}
});

Expand All @@ -99,7 +118,7 @@ export const useAllEquipmentBackgroundSSE = (equipmentIds: number[]) => {
parsedData
);
if (metrics) {
setDeviceMetrics(equipmentId, metrics);
callbacksRef.current.onMetricsUpdate(equipmentId, metrics);
}
} else {
console.warn(
Expand All @@ -115,19 +134,33 @@ export const useAllEquipmentBackgroundSSE = (equipmentIds: number[]) => {
}
});

eventSource.onerror = (error) => {
eventSource.onerror = (error: Event) => {
console.error(
`[Equipment ${equipmentId}] SSE connection error:`,
error
);
eventSource.close();
eventSourceMap.delete(equipmentId);

setTimeout(() => {
if (equipmentIds.includes(equipmentId)) {
connectToEquipment(equipmentId);
}
}, 3000);
callbacksRef.current.onConnectionError?.(equipmentId, error);

// 지수 백오프로 재연결
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
const delay = RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1);

setTimeout(() => {
if (equipmentIds.includes(equipmentId)) {
console.log(
`[Equipment ${equipmentId}] Reconnecting... (attempt ${reconnectAttempts})`
);
connectToEquipment(equipmentId);
}
}, delay);
} else {
console.error(
`[Equipment ${equipmentId}] Max reconnection attempts reached`
);
}
};

eventSourceMap.set(equipmentId, eventSource);
Expand All @@ -152,5 +185,6 @@ export const useAllEquipmentBackgroundSSE = (equipmentIds: number[]) => {
});
eventSourceMap.clear();
};
}, [equipmentIds, setDeviceMetrics]);
// ✅ equipmentIds만 dependency에 포함 (callbacks은 제외)
}, [equipmentIds]);
};
117 changes: 75 additions & 42 deletions src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { useEffect } from "react";
// ============================================================================
// useEquipmentSSE.ts - 콜백 패턴 (안정적 버전)
// ============================================================================

import { useEffect, useRef } from "react";
import { EventSourcePolyfill } from "event-source-polyfill";
import { getAccessToken, BASE_URL } from "@/api/client";
import { useMonitoringStore } from "../stores/monitoringStore";
import type {
DiskMonitoringData,
NetworkMonitoringData,
SystemMonitoringData,
} from "../types";

interface SSECallbacks {
onSystemData?: (data: SystemMonitoringData) => void;
onDiskData?: (data: DiskMonitoringData) => void;
onNetworkData?: (data: NetworkMonitoringData[]) => void;
onError?: (error: Event) => void;
}

export const useEquipmentSSE = (
equipmentId: number,
equipmentId: number | null,
callbacks: SSECallbacks,
enabled: boolean = true
) => {
const { setSystemData, setDiskData, setNetworkData } = useMonitoringStore();
// ✅ useRef로 콜백 참조 유지 (매번 새로 생성되어도 dependency에 영향 없음)
const callbacksRef = useRef(callbacks);

useEffect(() => {
callbacksRef.current = callbacks;
}, [callbacks]);

useEffect(() => {
if (!enabled || !equipmentId) return;
Expand All @@ -22,47 +38,64 @@ export const useEquipmentSSE = (

const url = `${BASE_URL}/monitoring/subscribe/equipment/${equipmentId}`;

const eventSource = new EventSourcePolyfill(url, {
headers: {
Authorization: `Bearer ${token}`,
},
withCredentials: true,
});
try {
const eventSource = new EventSourcePolyfill(url, {
headers: {
Authorization: `Bearer ${token}`,
},
withCredentials: true,
});

eventSource.addEventListener("system", (event) => {
try {
const data: SystemMonitoringData = JSON.parse(event.data);
setSystemData(data);
} catch (error) {
console.error("System data parse error:", error);
}
});
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);
setDiskData(data);
} catch (error) {
console.error("Disk 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);
setNetworkData(data);
} catch (error) {
console.error("Network 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("SSE Error:", error);
eventSource.close();
};
eventSource.onerror = (error) => {
console.error(`[Equipment ${equipmentId}] SSE Error:`, error);
eventSource.close();
};

return () => {
eventSource.close();
};
}, [equipmentId, enabled, setSystemData, setDiskData, setNetworkData]);
return () => {
eventSource.close();
};
} catch (error) {
console.error(
`[Equipment ${equipmentId}] Failed to create SSE connection:`,
error
);
}
// ✅ equipmentId와 enabled만 dependency에 포함 (callbacks는 제외)
}, [equipmentId, enabled]);
};
Loading