refactor: AI 타임아웃을 백엔드·프론트 동일 값 + 환경변수(AI_TIMEOUT_MS)로 통일

기존: 백엔드 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 13:01:51 +09:00
parent ff33fb2e6c
commit 042d724b3a
6 changed files with 27 additions and 4 deletions
+7
View File
@@ -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
+7 -2
View File
@@ -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<string>('CLAUDE_MODEL') ?? 'claude-sonnet-4-6'
).trim();
// AI 타임아웃 — env AI_TIMEOUT_MS(ms), 미설정/비정상이면 기본값
const t = Number(config.get<string>('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(
+3
View File
@@ -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
+2
View File
@@ -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}
+2
View File
@@ -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 {
+6 -2
View File
@@ -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<SuggestChecklistResult> {
return (await api.post('/ai/suggest-checklist', payload, {
silent: true,
timeout: 60000,
timeout: AI_TIMEOUT_MS,
})) as unknown as SuggestChecklistResult
}