Skip to content
Merged

Dev #192

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
11 changes: 4 additions & 7 deletions src/domains/serverView/rack/components/RackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,11 @@
);

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(
() => ({
Expand Down Expand Up @@ -130,7 +127,7 @@
if (view && editMode) {
setEditMode(false);
}
}, [view]);

Check warning on line 130 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
Expand Up @@ -49,7 +49,6 @@ export const useAllEquipmentBackgroundSSE = (
equipmentIds: number[],
callbacks: BackgroundSSECallbacks
) => {
// ✅ useRef로 콜백 참조 유지 (매번 새로 생성되어도 dependency에 영향 없음)
const callbacksRef = useRef(callbacks);

useEffect(() => {
Expand Down Expand Up @@ -185,6 +184,5 @@ export const useAllEquipmentBackgroundSSE = (
});
eventSourceMap.clear();
};
// ✅ equipmentIds만 dependency에 포함 (callbacks은 제외)
}, [equipmentIds]);
};
181 changes: 118 additions & 63 deletions src/domains/serverView/serverDashboard/hooks/useEquipmentSSE.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// ============================================================================
// useEquipmentSSE.ts - 콜백 패턴 (안정적 버전)
// ============================================================================

// useEquipmentSSE.ts - 타입 수정
import { useEffect, useRef } from "react";
import { EventSourcePolyfill } from "event-source-polyfill";
import { getAccessToken, BASE_URL } from "@/api/client";
Expand All @@ -23,79 +20,137 @@ export const useEquipmentSSE = (
callbacks: SSECallbacks,
enabled: boolean = true
) => {
// ✅ useRef로 콜백 참조 유지 (매번 새로 생성되어도 dependency에 영향 없음)
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
);
}
// ✅ equipmentId와 enabled만 dependency에 포함 (callbacks는 제외)
}

reconnectAttemptsRef.current = 0;
};
}, [equipmentId, enabled]);
};
Loading