1- // ============================================================================
2- // useEquipmentSSE.ts - 콜백 패턴 (안정적 버전)
3- // ============================================================================
4-
1+ // useEquipmentSSE.ts - 타입 수정
52import { useEffect , useRef } from "react" ;
63import { EventSourcePolyfill } from "event-source-polyfill" ;
74import { getAccessToken , BASE_URL } from "@/api/client" ;
@@ -23,79 +20,137 @@ export const useEquipmentSSE = (
2320 callbacks : SSECallbacks ,
2421 enabled : boolean = true
2522) => {
26- // ✅ useRef로 콜백 참조 유지 (매번 새로 생성되어도 dependency에 영향 없음)
2723 const callbacksRef = useRef ( callbacks ) ;
24+ const reconnectAttemptsRef = useRef ( 0 ) ;
25+ const MAX_RECONNECT_ATTEMPTS = 5 ;
26+ const RECONNECT_DELAY = 3000 ;
2827
2928 useEffect ( ( ) => {
3029 callbacksRef . current = callbacks ;
3130 } , [ callbacks ] ) ;
3231
3332 useEffect ( ( ) => {
34- if ( ! enabled || ! equipmentId ) return ;
33+ if ( ! enabled || ! equipmentId ) {
34+ reconnectAttemptsRef . current = 0 ;
35+ return ;
36+ }
3537
3638 const token = getAccessToken ( ) ;
3739 if ( ! token ) return ;
3840
3941 const url = `${ BASE_URL } /monitoring/subscribe/equipment/${ equipmentId } ` ;
42+ let eventSource : EventSource | null = null ;
43+ let reconnectTimeout : number | null = null ; // 🔥 여기 수정
4044
41- try {
42- const eventSource = new EventSourcePolyfill ( url , {
43- headers : {
44- Authorization : `Bearer ${ token } ` ,
45- } ,
46- withCredentials : true ,
47- } ) ;
48-
49- eventSource . addEventListener ( "system" , ( event ) => {
50- try {
51- const data : SystemMonitoringData = JSON . parse ( event . data ) ;
52- callbacksRef . current . onSystemData ?.( data ) ;
53- } catch ( error ) {
54- console . error (
55- `[Equipment ${ equipmentId } ] System data parse error:` ,
56- error
57- ) ;
58- }
59- } ) ;
60-
61- eventSource . addEventListener ( "disk" , ( event ) => {
62- try {
63- const data : DiskMonitoringData = JSON . parse ( event . data ) ;
64- callbacksRef . current . onDiskData ?.( data ) ;
65- } catch ( error ) {
66- console . error (
67- `[Equipment ${ equipmentId } ] Disk data parse error:` ,
68- error
69- ) ;
70- }
71- } ) ;
72-
73- eventSource . addEventListener ( "network" , ( event ) => {
74- try {
75- const data : NetworkMonitoringData [ ] = JSON . parse ( event . data ) ;
76- callbacksRef . current . onNetworkData ?.( data ) ;
77- } catch ( error ) {
78- console . error (
79- `[Equipment ${ equipmentId } ] Network data parse error:` ,
80- error
81- ) ;
82- }
83- } ) ;
84-
85- eventSource . onerror = ( error ) => {
86- console . error ( `[Equipment ${ equipmentId } ] SSE Error:` , error ) ;
87- eventSource . close ( ) ;
88- } ;
45+ const connect = ( ) => {
46+ try {
47+ console . log (
48+ `[Equipment ${ equipmentId } ] Establishing SSE connection...`
49+ ) ;
50+
51+ eventSource = new EventSourcePolyfill ( url , {
52+ headers : {
53+ Authorization : `Bearer ${ token } ` ,
54+ } ,
55+ withCredentials : true ,
56+ heartbeatTimeout : 120000 ,
57+ } ) as EventSource ;
58+
59+ eventSource . addEventListener ( "system" , ( event ) => {
60+ try {
61+ const data : SystemMonitoringData = JSON . parse ( event . data ) ;
62+ reconnectAttemptsRef . current = 0 ;
63+ callbacksRef . current . onSystemData ?.( data ) ;
64+ } catch ( error ) {
65+ console . error (
66+ `[Equipment ${ equipmentId } ] System data parse error:` ,
67+ error
68+ ) ;
69+ }
70+ } ) ;
71+
72+ eventSource . addEventListener ( "disk" , ( event ) => {
73+ try {
74+ const data : DiskMonitoringData = JSON . parse ( event . data ) ;
75+ callbacksRef . current . onDiskData ?.( data ) ;
76+ } catch ( error ) {
77+ console . error (
78+ `[Equipment ${ equipmentId } ] Disk data parse error:` ,
79+ error
80+ ) ;
81+ }
82+ } ) ;
83+
84+ eventSource . addEventListener ( "network" , ( event ) => {
85+ try {
86+ const data : NetworkMonitoringData [ ] = JSON . parse ( event . data ) ;
87+ callbacksRef . current . onNetworkData ?.( data ) ;
88+ } catch ( error ) {
89+ console . error (
90+ `[Equipment ${ equipmentId } ] Network data parse error:` ,
91+ error
92+ ) ;
93+ }
94+ } ) ;
95+
96+ eventSource . onerror = ( error ) => {
97+ console . error ( `[Equipment ${ equipmentId } ] SSE Error:` , error ) ;
98+
99+ if ( eventSource ) {
100+ eventSource . close ( ) ;
101+ eventSource = null ;
102+ }
103+
104+ callbacksRef . current . onError ?.( error ) ;
89105
90- return ( ) => {
106+ if ( reconnectAttemptsRef . current < MAX_RECONNECT_ATTEMPTS ) {
107+ reconnectAttemptsRef . current ++ ;
108+ const delay =
109+ RECONNECT_DELAY * Math . pow ( 2 , reconnectAttemptsRef . current - 1 ) ;
110+
111+ console . log (
112+ `[Equipment ${ equipmentId } ] Reconnecting in ${ delay } ms... (attempt ${ reconnectAttemptsRef . current } /${ MAX_RECONNECT_ATTEMPTS } )`
113+ ) ;
114+
115+ reconnectTimeout = window . setTimeout ( ( ) => {
116+ // 🔥 window.setTimeout으로 명시
117+ if ( enabled ) {
118+ connect ( ) ;
119+ }
120+ } , delay ) ;
121+ } else {
122+ console . error (
123+ `[Equipment ${ equipmentId } ] Max reconnection attempts reached`
124+ ) ;
125+ }
126+ } ;
127+
128+ eventSource . onopen = ( ) => {
129+ console . log ( `[Equipment ${ equipmentId } ] SSE connection established` ) ;
130+ reconnectAttemptsRef . current = 0 ;
131+ } ;
132+ } catch ( error ) {
133+ console . error (
134+ `[Equipment ${ equipmentId } ] Failed to create SSE connection:` ,
135+ error
136+ ) ;
137+ }
138+ } ;
139+
140+ connect ( ) ;
141+
142+ return ( ) => {
143+ console . log ( `[Equipment ${ equipmentId } ] Closing SSE connection` ) ;
144+
145+ if ( reconnectTimeout !== null ) {
146+ window . clearTimeout ( reconnectTimeout ) ; // 🔥 window.clearTimeout으로 명시
147+ }
148+
149+ if ( eventSource ) {
91150 eventSource . close ( ) ;
92- } ;
93- } catch ( error ) {
94- console . error (
95- `[Equipment ${ equipmentId } ] Failed to create SSE connection:` ,
96- error
97- ) ;
98- }
99- // ✅ equipmentId와 enabled만 dependency에 포함 (callbacks는 제외)
151+ }
152+
153+ reconnectAttemptsRef . current = 0 ;
154+ } ;
100155 } , [ equipmentId , enabled ] ) ;
101156} ;
0 commit comments