Skip to content

Commit b5e0b06

Browse files
committed
refactor: SSE 에러 개선
1 parent e6b1a04 commit b5e0b06

3 files changed

Lines changed: 64 additions & 7 deletions

File tree

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ ENV NODE_ENV=$NODE_ENV
2222

2323
# Vite로 빌드
2424
RUN echo "VITE_API_URL=$VITE_API_URL"
25+
RUN echo "NODE_ENV=$NODE_ENV"
2526
RUN npm run build
2627

2728
# 2단계: 프로덕션 스테이지

src/components/sections/TodayQuizSection.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useRef, useEffect } from 'react';
22
import { useSearchParams } from 'react-router-dom';
33
import { useQuery } from '@tanstack/react-query';
44
import { quizAPI } from '../../utils/api';
@@ -76,10 +76,21 @@ const TodayQuizSection: React.FC = () => {
7676
const [resultChars, setResultChars] = useState<string[]>([]);
7777
const [feedbackChars, setFeedbackChars] = useState<string[]>([]);
7878
const { openModal } = useModal();
79+
const sseConnectionRef = useRef<{ close: () => void } | null>(null);
7980

8081
const subscriptionId = searchParams.get('subscriptionId');
8182
const quizId = searchParams.get('quizId');
8283

84+
// SSE 연결 정리 - 컴포넌트 언마운트 시
85+
useEffect(() => {
86+
return () => {
87+
if (sseConnectionRef.current) {
88+
sseConnectionRef.current.close();
89+
sseConnectionRef.current = null;
90+
}
91+
};
92+
}, []);
93+
8394
// 결과 텍스트를 글자별로 분리하고 애니메이션 적용
8495
React.useEffect(() => {
8596
if (!feedbackResult) {
@@ -286,7 +297,13 @@ const TodayQuizSection: React.FC = () => {
286297
setResultChars([]);
287298
setFeedbackChars([]);
288299

289-
quizAPI.streamAiFeedback(
300+
// 기존 연결이 있다면 먼저 정리
301+
if (sseConnectionRef.current) {
302+
sseConnectionRef.current.close();
303+
sseConnectionRef.current = null;
304+
}
305+
306+
const sseConnection = quizAPI.streamAiFeedback(
290307
answerId,
291308
// onData: 스트리밍 데이터 수신
292309
(data: string) => {
@@ -363,7 +380,8 @@ const TodayQuizSection: React.FC = () => {
363380
}
364381
);
365382

366-
// SSE 연결은 자동으로 완료되거나 에러 시 닫힘
383+
// SSE 연결을 ref에 저장하여 나중에 정리할 수 있도록 함
384+
sseConnectionRef.current = sseConnection;
367385
} catch (feedbackError) {
368386
console.error('AI 피드백 요청 실패:', feedbackError);
369387

src/utils/api.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,23 +156,61 @@ export const quizAPI = {
156156

157157
// AI 피드백 SSE 스트리밍 (주관식)
158158
streamAiFeedback: (answerId: string, onData: (data: string) => void, onComplete: () => void, onError: (error: Event) => void) => {
159-
const eventSource = new EventSource(`${apiUrl}/quizzes/answers/${answerId}/feedback-sentence`, { withCredentials: true });
159+
let isCompleted = false;
160+
161+
const eventSource = new EventSource(`${apiUrl}/quizzes/${answerId}/feedback`, {
162+
withCredentials: true
163+
// EventSource는 자동으로 Accept: text/event-stream 헤더 설정
164+
});
160165

161166
eventSource.onmessage = (event) => {
167+
if (isCompleted) return;
168+
169+
// [종료] 메시지를 받으면 정상 종료
170+
if (event.data === '[종료]' || event.data === '[완료]' || event.data === 'END') {
171+
isCompleted = true;
172+
eventSource.close();
173+
onComplete();
174+
return;
175+
}
176+
162177
onData(event.data);
163178
};
164179

165180
eventSource.onerror = (error) => {
181+
if (isCompleted) return; // 이미 정상 완료된 경우 에러 무시
182+
183+
console.log('SSE 연결 에러:', error);
166184
eventSource.close();
167185
onError(error);
168186
};
169187

170188
eventSource.addEventListener('complete', () => {
171-
eventSource.close();
172-
onComplete();
189+
if (!isCompleted) {
190+
isCompleted = true;
191+
eventSource.close();
192+
onComplete();
193+
}
173194
});
174195

175-
return eventSource;
196+
// 타임아웃 설정 (선택사항 - 너무 오래 걸리면 자동 종료)
197+
const timeout = setTimeout(() => {
198+
if (!isCompleted) {
199+
console.log('SSE 스트리밍 타임아웃');
200+
isCompleted = true;
201+
eventSource.close();
202+
onError(new Event('timeout'));
203+
}
204+
}, 60000); // 60초 타임아웃
205+
206+
// 정리 함수 반환
207+
return {
208+
close: () => {
209+
isCompleted = true;
210+
clearTimeout(timeout);
211+
eventSource.close();
212+
}
213+
};
176214
},
177215
};
178216

0 commit comments

Comments
 (0)