From 042d724b3a3090c0101091b776f35baceed4d61b Mon Sep 17 00:00:00 2001 From: ttipo Date: Sun, 21 Jun 2026 13:01:51 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20AI=20=ED=83=80=EC=9E=84=EC=95=84?= =?UTF-8?q?=EC=9B=83=EC=9D=84=20=EB=B0=B1=EC=97=94=EB=93=9C=C2=B7=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=20=EB=8F=99=EC=9D=BC=20=EA=B0=92=20+=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98(AI=5FTIMEOUT=5FMS)?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존: 백엔드 30초(하드코딩 const)·프론트 60초(하드코딩)로 불일치 변경: 루트 .env 의 AI_TIMEOUT_MS(기본 60000)를 단일 소스로 - 백엔드 ai.service: REQUEST_TIMEOUT_MS const → ConfigService(AI_TIMEOUT_MS) 주입, 기본 60s - 프론트 useAi: import.meta.env.VITE_AI_TIMEOUT_MS 사용, 기본 60s - docker-compose: 백엔드 environment 에 AI_TIMEOUT_MS, 프론트 build arg 에 VITE_AI_TIMEOUT_MS=${AI_TIMEOUT_MS} - 프론트 Dockerfile ARG/ENV, env.d.ts 타입, 루트 .env.example 추가 미설정/비정상 값이면 양쪽 모두 기본 60초로 폴백. 검증: 백엔드 build/lint 0, 프론트 type-check/lint/build 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.development.example | 7 +++++++ backend/src/modules/ai/ai.service.ts | 9 +++++++-- docker-compose.yml | 3 +++ frontend/Dockerfile | 2 ++ frontend/env.d.ts | 2 ++ frontend/src/composables/useAi.ts | 8 ++++++-- 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.env.development.example b/.env.development.example index 14b261b..428fd9f 100644 --- a/.env.development.example +++ b/.env.development.example @@ -37,3 +37,10 @@ REDIS_DATA_DIR=./data/redis # ========================================== BACKEND_API_URL=http://localhost:3100/api CORS_ORIGIN=http://localhost:5273 + +# ========================================== +# AI 설정 (백엔드·프론트 공통) +# ========================================== +# AI 요청 타임아웃(ms) — 백엔드 Claude 호출 abort + 프론트 axios 타임아웃에 동일 적용. +# (프론트는 build arg VITE_AI_TIMEOUT_MS 로 주입됨) +AI_TIMEOUT_MS=60000 diff --git a/backend/src/modules/ai/ai.service.ts b/backend/src/modules/ai/ai.service.ts index f8c7a86..92ebf44 100644 --- a/backend/src/modules/ai/ai.service.ts +++ b/backend/src/modules/ai/ai.service.ts @@ -43,7 +43,8 @@ export interface ChecklistSuggestionGroup { } const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages'; -const REQUEST_TIMEOUT_MS = 30_000; +// AI 요청 타임아웃 기본값(ms) — env AI_TIMEOUT_MS 로 재정의. 프론트와 동일 값을 사용한다. +const DEFAULT_AI_TIMEOUT_MS = 60_000; const MAX_TOKENS = 1024; const TITLE_MAX = 60; // 컨텍스트로 주입할 담당 업무 최대 건수(토큰 절약) @@ -99,6 +100,7 @@ export class AiService { private readonly logger = new Logger(AiService.name); private readonly apiKey: string; private readonly model: string; + private readonly timeoutMs: number; constructor( config: ConfigService, @@ -113,6 +115,9 @@ export class AiService { this.model = ( config.get('CLAUDE_MODEL') ?? 'claude-sonnet-4-6' ).trim(); + // AI 타임아웃 — env AI_TIMEOUT_MS(ms), 미설정/비정상이면 기본값 + const t = Number(config.get('AI_TIMEOUT_MS')); + this.timeoutMs = Number.isFinite(t) && t > 0 ? t : DEFAULT_AI_TIMEOUT_MS; if (!this.apiKey) { this.logger.warn('CLAUDE_API_KEY 미설정 — AI 채팅 비활성화'); } @@ -446,7 +451,7 @@ export class AiService { system, messages, }), - signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + signal: AbortSignal.timeout(this.timeoutMs), }); } catch (e) { this.logger.error( diff --git a/docker-compose.yml b/docker-compose.yml index e9abdb1..a341c2c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: args: - BUILD_MODE=${BUILD_MODE} - VITE_API_BASE_URL=${BACKEND_API_URL} + - VITE_AI_TIMEOUT_MS=${AI_TIMEOUT_MS} expose: - "80" environment: @@ -59,6 +60,8 @@ services: - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_PASSWORD=${REDIS_PASSWORD} + # AI 요청 타임아웃(ms) — 프론트(VITE_AI_TIMEOUT_MS)와 동일 값을 루트 .env 에서 공급 + - AI_TIMEOUT_MS=${AI_TIMEOUT_MS} restart: unless-stopped networks: - app-network diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 551091e..e25b4b2 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -12,6 +12,8 @@ COPY . . ARG BUILD_MODE=production ARG VITE_API_BASE_URL ENV VITE_API_BASE_URL=$VITE_API_BASE_URL +ARG VITE_AI_TIMEOUT_MS +ENV VITE_AI_TIMEOUT_MS=$VITE_AI_TIMEOUT_MS RUN npm run build-only -- --mode ${BUILD_MODE} diff --git a/frontend/env.d.ts b/frontend/env.d.ts index 88f11ac..4a6886d 100644 --- a/frontend/env.d.ts +++ b/frontend/env.d.ts @@ -3,6 +3,8 @@ // Vite 환경변수 타입 정의 — VITE_ prefix 만 클라이언트 번들에 포함됨 interface ImportMetaEnv { readonly VITE_API_BASE_URL: string + /** AI 요청 타임아웃(ms) — 백엔드 AI_TIMEOUT_MS 와 동일 값 */ + readonly VITE_AI_TIMEOUT_MS: string } interface ImportMeta { diff --git a/frontend/src/composables/useAi.ts b/frontend/src/composables/useAi.ts index a1b04a2..adfe363 100644 --- a/frontend/src/composables/useAi.ts +++ b/frontend/src/composables/useAi.ts @@ -7,6 +7,10 @@ import type { } from '@/types/ai' import type { Paginated } from '@/types/pagination' +// AI 요청 타임아웃(ms) — env VITE_AI_TIMEOUT_MS, 백엔드 AI_TIMEOUT_MS 와 동일 값 사용. +// 미설정/비정상이면 기본 60초(AI 생성이 axios 기본 10초를 넘기므로). +const AI_TIMEOUT_MS = Number(import.meta.env.VITE_AI_TIMEOUT_MS) || 60000 + // AI 도메인 API Composable — 대화/메시지는 서버(DB)에서 관리(본인 소유 스코프) export function useAi() { const api = useApi() @@ -45,7 +49,7 @@ export function useAi() { } // 할 일(체크리스트) 추천 — 실패는 호출측에서 인라인 처리(silent) - // AI 생성은 기본 10초 타임아웃을 넘기므로 넉넉히(백엔드 abort 30초보다 길게) 둔다 + // AI 생성은 axios 기본 10초를 넘기므로 백엔드와 동일한 AI_TIMEOUT_MS 적용 async function suggestChecklist(payload: { projectId: string title: string @@ -53,7 +57,7 @@ export function useAi() { }): Promise { return (await api.post('/ai/suggest-checklist', payload, { silent: true, - timeout: 60000, + timeout: AI_TIMEOUT_MS, })) as unknown as SuggestChecklistResult }