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:
157
src/utils/api-helper.ts
Normal file
157
src/utils/api-helper.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user