refactor: complete P0-P1 improvements

Constants migration:
- server-agent.ts: SERVER_CONSULTATION_STATUS, LANGUAGE_CODE
- troubleshoot-agent.ts: TROUBLESHOOT_STATUS
- notification.ts: NOTIFICATION_TYPE

API improvements:
- search-tool.ts: Zod schema validation for Brave/Context7 APIs
- api-helper.ts: Centralized API call utility with retry/timeout

Testing:
- kv-cache.test.ts: 38 test cases for cache abstraction

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-29 11:02:27 +09:00
parent f304c6a7d4
commit fbe696b88c
8 changed files with 832 additions and 56 deletions

157
src/utils/api-helper.ts Normal file
View File

@@ -0,0 +1,157 @@
/**
* API 호출 추상화 유틸리티
*
* Cloudflare Workers 환경에서 외부 API 호출을 위한 추상화 레이어입니다.
* 재시도 로직, 타임아웃, 응답 검증을 자동으로 처리합니다.
*
* @module api-helper
* @example
* ```typescript
* import { callApi } from './utils/api-helper';
* import { z } from 'zod';
*
* // 스키마 정의
* const UserSchema = z.object({
* id: z.number(),
* name: z.string(),
* });
*
* // API 호출 (자동 검증)
* const user = await callApi('https://api.example.com/user', {
* schema: UserSchema,
* });
* ```
*/
import { z } from 'zod';
import { createLogger } from './logger';
import { retryWithBackoff } from './retry';
const logger = createLogger('api-helper');
/**
* API 호출 옵션 인터페이스
*
* @template T - 응답 타입
*/
export interface ApiOptions<T> {
/** HTTP 메서드 (기본값: 'GET') */
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
/** HTTP 헤더 (기본 헤더와 병합됨) */
headers?: Record<string, string>;
/** 요청 본문 (JSON으로 자동 직렬화) */
body?: Record<string, unknown>;
/** 타임아웃 (밀리초, 기본값: 30000) */
timeout?: number;
/** 재시도 횟수 (기본값: 3) */
retries?: number;
/** 응답 검증 스키마 (Zod) */
schema?: z.ZodType<T>;
}
/**
* 외부 API 호출 추상화 함수
*
* 다음 기능을 자동으로 처리합니다:
* - 재시도 로직 (지수 백오프 + 지터)
* - 타임아웃 제어
* - JSON 응답 파싱
* - 응답 검증 (Zod 스키마)
* - 구조화된 로깅
*
* @template T - 응답 타입
* @param url - 호출할 API URL
* @param options - API 호출 옵션
* @returns 파싱 및 검증된 응답 데이터
* @throws Error if API call fails after retries
*
* @example
* ```typescript
* // 기본 GET 요청
* const data = await callApi<{ status: string }>('https://api.example.com/health');
*
* // POST 요청 with body
* const result = await callApi('https://api.example.com/data', {
* method: 'POST',
* body: { name: 'test' },
* });
*
* // 스키마 검증
* const ResponseSchema = z.object({
* success: z.boolean(),
* data: z.array(z.string()),
* });
*
* const validated = await callApi('https://api.example.com/list', {
* schema: ResponseSchema,
* });
*
* // 커스텀 타임아웃 및 재시도
* const important = await callApi('https://api.example.com/critical', {
* timeout: 60000, // 60초
* retries: 5, // 5회 재시도
* });
* ```
*/
export async function callApi<T>(
url: string,
options?: ApiOptions<T>
): Promise<T> {
return retryWithBackoff(async () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options?.timeout || 30000);
try {
logger.debug('API 호출 시작', {
url,
method: options?.method || 'GET',
timeout: options?.timeout || 30000,
});
const response = await fetch(url, {
method: options?.method || 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...options?.headers,
},
body: options?.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal,
});
if (!response.ok) {
logger.error('API 호출 실패', undefined, {
url,
status: response.status,
statusText: response.statusText,
});
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
const json = await response.json();
if (options?.schema) {
try {
const validated = options.schema.parse(json);
logger.debug('API 응답 검증 성공', { url });
return validated;
} catch (error) {
logger.error('API 응답 검증 실패', error as Error, {
url,
response: json,
});
throw error;
}
}
logger.debug('API 호출 성공', { url });
return json as T;
} finally {
clearTimeout(timeoutId);
}
}, {
maxRetries: options?.retries || 3,
initialDelayMs: 1000,
maxDelayMs: 10000,
});
}