diff --git a/frontend/nginx-locations.inc b/frontend/nginx-locations.inc index 06d8912..780f5c5 100644 --- a/frontend/nginx-locations.inc +++ b/frontend/nginx-locations.inc @@ -23,6 +23,21 @@ chunked_transfer_encoding on; } + # ── LLM / AI 검증 (Ta4j 재평가 + LLM 최대 120s — 일반 /api/ 60s 초과) ─── + location ~ ^/api/strategy/evaluation/(ai-verify|llm-test)$ { + proxy_pass http://backend:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_buffering off; + client_max_body_size 16m; + } + # ── GoldenChart Spring Boot Backend 프록시 ──────────────────────── location /api/ { proxy_pass http://backend:8080; diff --git a/frontend/src/components/strategyEvaluation/StrategyEvaluationAiVerifyModal.tsx b/frontend/src/components/strategyEvaluation/StrategyEvaluationAiVerifyModal.tsx index d71b9f2..f7116d3 100644 --- a/frontend/src/components/strategyEvaluation/StrategyEvaluationAiVerifyModal.tsx +++ b/frontend/src/components/strategyEvaluation/StrategyEvaluationAiVerifyModal.tsx @@ -299,7 +299,7 @@ const StrategyEvaluationAiVerifyModal: React.FC = ({

LLM이 평가 결과와 차트 시그널을 분석 중입니다…

-

최대 2분 정도 걸릴 수 있습니다.

+

Ta4j 재평가와 LLM 분석에 최대 2~3분 걸릴 수 있습니다.

)} diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index 9af9e6f..015dd22 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -118,17 +118,69 @@ async function requestOrThrow(path: string, init?: RequestInit): Promise { const text = await res.text().catch(() => ''); if (!res.ok) { let msg = `요청 실패 (${res.status})`; - try { - const j = JSON.parse(text) as { message?: string }; - if (j.message) msg = j.message; - } catch { /* plain text */ } - if (!text.startsWith('{') && text) msg = text.slice(0, 200); + if (res.status === 504) { + msg = '게이트웨이 시간 초과(504). LLM 분석은 2~3분 걸릴 수 있습니다. 잠시 후 다시 시도해 주세요.'; + } else { + try { + const j = JSON.parse(text) as { message?: string }; + if (j.message) msg = j.message; + } catch { /* plain text */ } + if (!text.startsWith('{') && text) { + const plain = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + if (plain) msg = plain.slice(0, 200); + } + } throw new Error(msg); } if (res.status === 204 || !text) return undefined as T; return JSON.parse(text) as T; } +/** LLM·AI 검증 등 장시간 API */ +async function requestOrThrowWithTimeout( + path: string, + init?: RequestInit, + timeoutMs = 200_000, +): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetch(`${API_BASE}${path}`, { + ...init, + signal: controller.signal, + headers: { ...authHeaders(), ...(init?.headers ?? {}) }, + }); + const text = await res.text().catch(() => ''); + if (!res.ok) { + let msg = `요청 실패 (${res.status})`; + if (res.status === 504) { + msg = '게이트웨이 시간 초과(504). LLM 분석은 2~3분 걸릴 수 있습니다. 잠시 후 다시 시도해 주세요.'; + } else { + try { + const j = JSON.parse(text) as { message?: string }; + if (j.message) msg = j.message; + } catch { /* plain text */ } + if (!text.startsWith('{') && text) { + const plain = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + if (plain) msg = plain.slice(0, 200); + } + } + throw new Error(msg); + } + if (res.status === 204 || !text) return undefined as T; + return JSON.parse(text) as T; + } catch (e) { + if (e instanceof DOMException && e.name === 'AbortError') { + throw new Error( + `요청 시간 초과(${Math.round(timeoutMs / 1000)}초). LLM이 느리면 설정 > LLM에서 타임아웃을 늘리거나 잠시 후 다시 시도하세요.`, + ); + } + throw e; + } finally { + clearTimeout(timer); + } +} + async function request(path: string, init?: RequestInit): Promise { try { const res = await fetch(`${API_BASE}${path}`, { @@ -1690,11 +1742,15 @@ export interface StrategyEvaluationAiVerifyResponse { export async function fetchStrategyEvaluationAiVerify( context: Record, ): Promise { - return requestOrThrow('/strategy/evaluation/ai-verify', { - method: 'POST', - cache: 'no-store', - body: JSON.stringify({ context }), - }); + return requestOrThrowWithTimeout( + '/strategy/evaluation/ai-verify', + { + method: 'POST', + cache: 'no-store', + body: JSON.stringify({ context }), + }, + 200_000, + ); } export interface LlmConnectionTestResponse { @@ -1706,10 +1762,14 @@ export interface LlmConnectionTestResponse { /** 설정 화면 — LLM 서버 연결 테스트 (저장된 설정 기준) */ export async function fetchLlmConnectionTest(): Promise { - return requestOrThrow('/strategy/evaluation/llm-test', { - method: 'POST', - cache: 'no-store', - }); + return requestOrThrowWithTimeout( + '/strategy/evaluation/llm-test', + { + method: 'POST', + cache: 'no-store', + }, + 130_000, + ); } /** 전략 DSL에 포함된 평가 시간봉 목록 (실시간 체크·STOMP 구독용) */