From 160ba5f4279b272237aa0d1d5ae10c826fe88fde Mon Sep 17 00:00:00 2001 From: kappa Date: Tue, 20 Jan 2026 00:06:26 +0900 Subject: [PATCH] refactor: improve code quality for 9.0 score - Split handleApiRequest (380 lines) into focused handler functions: - handleDepositBalance, handleDepositDeduct, handleTestApi - handleContactForm, handleContactPreflight, handleMetrics - Clean router pattern with JSDoc documentation - Unify logging: Replace all console.log/error with structured logger - 8 console statements converted to logger calls - Add structured metadata for better debugging - Remove duplicate email validation (Zod already validates) Co-Authored-By: Claude Opus 4.5 --- src/routes/api.ts | 774 +++++++++++++++++++++++++--------------------- 1 file changed, 418 insertions(+), 356 deletions(-) diff --git a/src/routes/api.ts b/src/routes/api.ts index eeb035a..bb96d18 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -63,7 +63,418 @@ async function getOrCreateUser( } /** - * API 엔드포인트 처리 + * GET /api/deposit/balance - 잔액 조회 (namecheap-api 전용) + * + * @param request - HTTP Request + * @param env - Environment bindings + * @param url - Parsed URL + * @returns JSON response with balance + */ +async function handleDepositBalance(request: Request, env: Env, url: URL): Promise { + try { + const apiSecret = env.DEPOSIT_API_SECRET; + const authHeader = request.headers.get('X-API-Key'); + + if (!apiSecret || authHeader !== apiSecret) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const telegramId = url.searchParams.get('telegram_id'); + if (!telegramId) { + return Response.json({ error: 'telegram_id required' }, { status: 400 }); + } + + // 사용자 조회 + const user = await env.DB.prepare( + 'SELECT id FROM users WHERE telegram_id = ?' + ).bind(telegramId).first<{ id: number }>(); + + if (!user) { + return Response.json({ error: 'User not found' }, { status: 404 }); + } + + // 잔액 조회 + const deposit = await env.DB.prepare( + 'SELECT balance FROM user_deposits WHERE user_id = ?' + ).bind(user.id).first<{ balance: number }>(); + + return Response.json({ + telegram_id: telegramId, + balance: deposit?.balance || 0, + }); + } catch (error) { + logger.error('Deposit balance error', error as Error); + return Response.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +/** + * POST /api/deposit/deduct - 잔액 차감 (namecheap-api 전용) + * + * @param request - HTTP Request with body + * @param env - Environment bindings + * @returns JSON response with transaction result + */ +async function handleDepositDeduct(request: Request, env: Env): Promise { + try { + const apiSecret = env.DEPOSIT_API_SECRET; + const authHeader = request.headers.get('X-API-Key'); + + if (!apiSecret || authHeader !== apiSecret) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const jsonData = await request.json(); + const parseResult = DepositDeductBodySchema.safeParse(jsonData); + + if (!parseResult.success) { + logger.warn('Deposit deduct - Invalid request body', { errors: parseResult.error.issues }); + return Response.json({ + error: 'Invalid request body', + details: parseResult.error.issues + }, { status: 400 }); + } + + const body = parseResult.data; + + // 사용자 조회 + const user = await env.DB.prepare( + 'SELECT id FROM users WHERE telegram_id = ?' + ).bind(body.telegram_id).first<{ id: number }>(); + + if (!user) { + return Response.json({ error: 'User not found' }, { status: 404 }); + } + + // 현재 잔액과 version 확인 (Optimistic Locking) + const current = await env.DB.prepare( + 'SELECT balance, version FROM user_deposits WHERE user_id = ?' + ).bind(user.id).first<{ balance: number; version: number }>(); + + if (!current) { + return Response.json({ error: 'User deposit account not found' }, { status: 404 }); + } + + if (current.balance < body.amount) { + return Response.json({ + error: 'Insufficient balance', + current_balance: current.balance, + required: body.amount, + }, { status: 400 }); + } + + // Optimistic Locking: version 조건으로 잔액 차감 + const balanceUpdate = await env.DB.prepare( + 'UPDATE user_deposits SET balance = balance - ?, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND version = ?' + ).bind(body.amount, user.id, current.version).run(); + + if (!balanceUpdate.success || balanceUpdate.meta.changes === 0) { + logger.warn('Optimistic locking conflict (외부 API 잔액 차감)', { + userId: user.id, + telegram_id: body.telegram_id, + amount: body.amount, + expectedVersion: current.version, + context: 'api_deposit_deduct' + }); + return Response.json({ + error: 'Concurrent modification detected', + message: '동시 요청 감지 - 다시 시도해주세요' + }, { status: 409 }); + } + + // 거래 기록 INSERT + const transactionInsert = await env.DB.prepare( + `INSERT INTO deposit_transactions (user_id, type, amount, status, description, confirmed_at) + VALUES (?, 'withdrawal', ?, 'confirmed', ?, CURRENT_TIMESTAMP)` + ).bind(user.id, body.amount, body.reason).run(); + + if (!transactionInsert.success) { + // 잔액 복구 시도 (rollback) + try { + await env.DB.prepare( + 'UPDATE user_deposits SET balance = balance + ?, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?' + ).bind(body.amount, user.id).run(); + + logger.error('거래 기록 INSERT 실패 - 잔액 복구 완료', undefined, { + userId: user.id, + telegram_id: body.telegram_id, + amount: body.amount, + reason: body.reason, + context: 'api_deposit_deduct_rollback' + }); + } catch (rollbackError) { + logger.error('잔액 복구 실패 - 수동 확인 필요', rollbackError as Error, { + userId: user.id, + telegram_id: body.telegram_id, + amount: body.amount, + context: 'api_deposit_deduct_rollback_failed' + }); + } + + return Response.json({ + error: 'Transaction processing failed', + message: '거래 처리 실패 - 관리자에게 문의하세요' + }, { status: 500 }); + } + + const newBalance = current.balance - body.amount; + + logger.info('Deposit deducted', { + telegram_id: body.telegram_id, + amount: body.amount, + reason: body.reason, + new_balance: newBalance + }); + + return Response.json({ + success: true, + telegram_id: body.telegram_id, + deducted: body.amount, + previous_balance: current.balance, + new_balance: newBalance, + }); + } catch (error) { + logger.error('Deposit deduct error', error as Error); + return Response.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +/** + * POST /api/test - 테스트 API (메시지 처리 후 응답 직접 반환) + * + * @param request - HTTP Request with body + * @param env - Environment bindings + * @returns JSON response with AI response + */ +async function handleTestApi(request: Request, env: Env): Promise { + try { + const jsonData = await request.json(); + const parseResult = TestApiBodySchema.safeParse(jsonData); + + if (!parseResult.success) { + logger.warn('Test API - Invalid request body', { errors: parseResult.error.issues }); + return Response.json({ + error: 'Invalid request body', + details: parseResult.error.issues + }, { status: 400 }); + } + + const body = parseResult.data; + + // 간단한 인증 + if (body.secret !== env.WEBHOOK_SECRET) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + if (!body.text) { + return Response.json({ error: 'text required' }, { status: 400 }); + } + + const telegramUserId = body.user_id || '821596605'; + const chatIdStr = telegramUserId; + + // 사용자 조회/생성 + const userId = await getOrCreateUser(env.DB, telegramUserId, 'TestUser', 'testuser'); + + let responseText: string; + + // 명령어 처리 + if (body.text.startsWith('/')) { + const [command, ...argParts] = body.text.split(' '); + const args = argParts.join(' '); + responseText = await handleCommand(env, userId, chatIdStr, command, args); + } else { + // 1. 사용자 메시지 버퍼에 추가 + await addToBuffer(env.DB, userId, chatIdStr, 'user', body.text); + + // 2. AI 응답 생성 + responseText = await generateAIResponse(env, userId, chatIdStr, body.text, telegramUserId); + + // 3. 봇 응답 버퍼에 추가 + await addToBuffer(env.DB, userId, chatIdStr, 'bot', responseText); + + // 4. 임계값 도달시 프로필 업데이트 + const { summarized } = await processAndSummarize(env, userId, chatIdStr); + if (summarized) { + responseText += '\n\n👤 프로필이 업데이트되었습니다.'; + } + } + + // HTML 태그 제거 (CLI 출력용) + const plainText = responseText.replace(/<[^>]*>/g, ''); + + return Response.json({ + input: body.text, + response: plainText, + user_id: telegramUserId, + }); + } catch (error) { + logger.error('Test API error', error as Error); + return Response.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +/** + * POST /api/contact - 문의 폼 API (웹사이트용) + * + * @param request - HTTP Request with body + * @param env - Environment bindings + * @returns JSON response with success status + */ +async function handleContactForm(request: Request, env: Env): Promise { + // CORS: hosting.anvil.it.com만 허용 + const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com'; + const corsHeaders = { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }; + + // Origin 헤더 검증 (curl 우회 방지) + const origin = request.headers.get('Origin'); + + if (!origin || origin !== allowedOrigin) { + logger.warn('Contact API - 허용되지 않은 Origin', { origin }); + return Response.json( + { error: 'Forbidden' }, + { status: 403 } + ); + } + + try { + const jsonData = await request.json(); + const parseResult = ContactFormBodySchema.safeParse(jsonData); + + if (!parseResult.success) { + logger.warn('Contact form - Invalid request body', { errors: parseResult.error.issues }); + return Response.json( + { + error: '올바르지 않은 요청 형식입니다.', + details: parseResult.error.issues + }, + { status: 400, headers: corsHeaders } + ); + } + + const body = parseResult.data; + + // 메시지 길이 제한 + if (body.message.length > 2000) { + return Response.json( + { error: '메시지는 2000자 이내로 작성해주세요.' }, + { status: 400, headers: corsHeaders } + ); + } + + // 관리자에게 텔레그램 알림 + const adminId = env.DEPOSIT_ADMIN_ID || env.DOMAIN_OWNER_ID; + if (env.BOT_TOKEN && adminId) { + const timestamp = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }); + await sendMessage( + env.BOT_TOKEN, + parseInt(adminId), + `📬 웹사이트 문의\n\n` + + `📧 이메일: ${body.email}\n` + + `🕐 시간: ${timestamp}\n\n` + + `💬 내용:\n${body.message}` + ); + } + + logger.info('문의 수신', { email: body.email, hasName: !!body.name }); + + return Response.json( + { success: true, message: '문의가 성공적으로 전송되었습니다.' }, + { headers: corsHeaders } + ); + } catch (error) { + logger.error('Contact form error', error as Error); + return Response.json( + { error: '문의 전송 중 오류가 발생했습니다.' }, + { status: 500, headers: corsHeaders } + ); + } +} + +/** + * OPTIONS /api/contact - CORS preflight for contact API + * + * @param env - Environment bindings + * @returns Response with CORS headers + */ +async function handleContactPreflight(env: Env): Promise { + const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com'; + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} + +/** + * GET /api/metrics - Circuit Breaker 상태 조회 (관리자 전용) + * + * @param request - HTTP Request + * @param env - Environment bindings + * @returns JSON response with metrics + */ +async function handleMetrics(request: Request, env: Env): Promise { + try { + // WEBHOOK_SECRET 인증 + const authHeader = request.headers.get('Authorization'); + if (!env.WEBHOOK_SECRET || authHeader !== `Bearer ${env.WEBHOOK_SECRET}`) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Circuit Breaker 상태 수집 + const openaiStats = openaiCircuitBreaker.getStats(); + + // 메트릭 응답 생성 + const metrics = { + timestamp: new Date().toISOString(), + circuitBreakers: { + openai: { + state: openaiStats.state, + failures: openaiStats.failures, + lastFailureTime: openaiStats.lastFailureTime?.toISOString(), + stats: openaiStats.stats, + config: openaiStats.config, + }, + }, + // 추후 확장 가능: API 호출 통계, 캐시 hit rate 등 + metrics: { + api_calls: { + // 추후 구현: 실제 API 호출 통계 + openai: { count: openaiStats.stats.totalRequests, avg_duration: 0 }, + }, + errors: { + // 추후 구현: 에러 통계 + retry_exhausted: 0, + circuit_breaker_open: openaiStats.state === 'OPEN' ? 1 : 0, + }, + cache: { + // 추후 구현: 캐시 hit rate + hit_rate: 0, + }, + }, + }; + + logger.info('Metrics retrieved', { + state: openaiStats.state, + failures: openaiStats.failures, + requests: openaiStats.stats.totalRequests + }); + + return Response.json(metrics); + } catch (error) { + logger.error('Metrics API error', error as Error); + return Response.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +/** + * API 엔드포인트 처리 (라우터) * * Manual Test: * 1. wrangler dev @@ -91,381 +502,32 @@ async function getOrCreateUser( export async function handleApiRequest(request: Request, env: Env, url: URL): Promise { // Deposit API - 잔액 조회 (namecheap-api 전용) if (url.pathname === '/api/deposit/balance' && request.method === 'GET') { - try { - const apiSecret = env.DEPOSIT_API_SECRET; - const authHeader = request.headers.get('X-API-Key'); - - if (!apiSecret || authHeader !== apiSecret) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const telegramId = url.searchParams.get('telegram_id'); - if (!telegramId) { - return Response.json({ error: 'telegram_id required' }, { status: 400 }); - } - - // 사용자 조회 - const user = await env.DB.prepare( - 'SELECT id FROM users WHERE telegram_id = ?' - ).bind(telegramId).first<{ id: number }>(); - - if (!user) { - return Response.json({ error: 'User not found' }, { status: 404 }); - } - - // 잔액 조회 - const deposit = await env.DB.prepare( - 'SELECT balance FROM user_deposits WHERE user_id = ?' - ).bind(user.id).first<{ balance: number }>(); - - return Response.json({ - telegram_id: telegramId, - balance: deposit?.balance || 0, - }); - } catch (error) { - console.error('[API] Deposit balance error:', error); - return Response.json({ error: 'Internal server error' }, { status: 500 }); - } + return handleDepositBalance(request, env, url); } // Deposit API - 잔액 차감 (namecheap-api 전용) if (url.pathname === '/api/deposit/deduct' && request.method === 'POST') { - try { - const apiSecret = env.DEPOSIT_API_SECRET; - const authHeader = request.headers.get('X-API-Key'); - - if (!apiSecret || authHeader !== apiSecret) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const jsonData = await request.json(); - const parseResult = DepositDeductBodySchema.safeParse(jsonData); - - if (!parseResult.success) { - logger.warn('Deposit deduct - Invalid request body', { errors: parseResult.error.issues }); - return Response.json({ - error: 'Invalid request body', - details: parseResult.error.issues - }, { status: 400 }); - } - - const body = parseResult.data; - - // 사용자 조회 - const user = await env.DB.prepare( - 'SELECT id FROM users WHERE telegram_id = ?' - ).bind(body.telegram_id).first<{ id: number }>(); - - if (!user) { - return Response.json({ error: 'User not found' }, { status: 404 }); - } - - // 현재 잔액과 version 확인 (Optimistic Locking) - const current = await env.DB.prepare( - 'SELECT balance, version FROM user_deposits WHERE user_id = ?' - ).bind(user.id).first<{ balance: number; version: number }>(); - - if (!current) { - return Response.json({ error: 'User deposit account not found' }, { status: 404 }); - } - - if (current.balance < body.amount) { - return Response.json({ - error: 'Insufficient balance', - current_balance: current.balance, - required: body.amount, - }, { status: 400 }); - } - - // Optimistic Locking: version 조건으로 잔액 차감 - const balanceUpdate = await env.DB.prepare( - 'UPDATE user_deposits SET balance = balance - ?, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND version = ?' - ).bind(body.amount, user.id, current.version).run(); - - if (!balanceUpdate.success || balanceUpdate.meta.changes === 0) { - logger.warn('Optimistic locking conflict (외부 API 잔액 차감)', { - userId: user.id, - telegram_id: body.telegram_id, - amount: body.amount, - expectedVersion: current.version, - context: 'api_deposit_deduct' - }); - return Response.json({ - error: 'Concurrent modification detected', - message: '동시 요청 감지 - 다시 시도해주세요' - }, { status: 409 }); - } - - // 거래 기록 INSERT - const transactionInsert = await env.DB.prepare( - `INSERT INTO deposit_transactions (user_id, type, amount, status, description, confirmed_at) - VALUES (?, 'withdrawal', ?, 'confirmed', ?, CURRENT_TIMESTAMP)` - ).bind(user.id, body.amount, body.reason).run(); - - if (!transactionInsert.success) { - // 잔액 복구 시도 (rollback) - try { - await env.DB.prepare( - 'UPDATE user_deposits SET balance = balance + ?, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?' - ).bind(body.amount, user.id).run(); - - logger.error('거래 기록 INSERT 실패 - 잔액 복구 완료', undefined, { - userId: user.id, - telegram_id: body.telegram_id, - amount: body.amount, - reason: body.reason, - context: 'api_deposit_deduct_rollback' - }); - } catch (rollbackError) { - logger.error('잔액 복구 실패 - 수동 확인 필요', rollbackError as Error, { - userId: user.id, - telegram_id: body.telegram_id, - amount: body.amount, - context: 'api_deposit_deduct_rollback_failed' - }); - } - - return Response.json({ - error: 'Transaction processing failed', - message: '거래 처리 실패 - 관리자에게 문의하세요' - }, { status: 500 }); - } - - const newBalance = current.balance - body.amount; - - console.log(`[API] Deposit deducted: user=${body.telegram_id}, amount=${body.amount}, reason=${body.reason}`); - - return Response.json({ - success: true, - telegram_id: body.telegram_id, - deducted: body.amount, - previous_balance: current.balance, - new_balance: newBalance, - }); - } catch (error) { - console.error('[API] Deposit deduct error:', error); - return Response.json({ error: 'Internal server error' }, { status: 500 }); - } + return handleDepositDeduct(request, env); } // 테스트 API - 메시지 처리 후 응답 직접 반환 if (url.pathname === '/api/test' && request.method === 'POST') { - try { - const jsonData = await request.json(); - const parseResult = TestApiBodySchema.safeParse(jsonData); - - if (!parseResult.success) { - logger.warn('Test API - Invalid request body', { errors: parseResult.error.issues }); - return Response.json({ - error: 'Invalid request body', - details: parseResult.error.issues - }, { status: 400 }); - } - - const body = parseResult.data; - - // 간단한 인증 - if (body.secret !== env.WEBHOOK_SECRET) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - - if (!body.text) { - return Response.json({ error: 'text required' }, { status: 400 }); - } - - const telegramUserId = body.user_id || '821596605'; - const chatIdStr = telegramUserId; - - // 사용자 조회/생성 - const userId = await getOrCreateUser(env.DB, telegramUserId, 'TestUser', 'testuser'); - - let responseText: string; - - // 명령어 처리 - if (body.text.startsWith('/')) { - const [command, ...argParts] = body.text.split(' '); - const args = argParts.join(' '); - responseText = await handleCommand(env, userId, chatIdStr, command, args); - } else { - // 1. 사용자 메시지 버퍼에 추가 - await addToBuffer(env.DB, userId, chatIdStr, 'user', body.text); - - // 2. AI 응답 생성 - responseText = await generateAIResponse(env, userId, chatIdStr, body.text, telegramUserId); - - // 3. 봇 응답 버퍼에 추가 - await addToBuffer(env.DB, userId, chatIdStr, 'bot', responseText); - - // 4. 임계값 도달시 프로필 업데이트 - const { summarized } = await processAndSummarize(env, userId, chatIdStr); - if (summarized) { - responseText += '\n\n👤 프로필이 업데이트되었습니다.'; - } - } - - // HTML 태그 제거 (CLI 출력용) - const plainText = responseText.replace(/<[^>]*>/g, ''); - - return Response.json({ - input: body.text, - response: plainText, - user_id: telegramUserId, - }); - } catch (error) { - console.error('[Test API] Error:', error); - return Response.json({ error: 'Internal server error' }, { status: 500 }); - } + return handleTestApi(request, env); } // 문의 폼 API (웹사이트용) if (url.pathname === '/api/contact' && request.method === 'POST') { - // CORS: hosting.anvil.it.com만 허용 - const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com'; - const corsHeaders = { - 'Access-Control-Allow-Origin': allowedOrigin, - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }; - - // Origin 헤더 검증 (curl 우회 방지) - const origin = request.headers.get('Origin'); - - if (!origin || origin !== allowedOrigin) { - logger.warn('Contact API - 허용되지 않은 Origin', { origin }); - return Response.json( - { error: 'Forbidden' }, - { status: 403 } - ); - } - - try { - const jsonData = await request.json(); - const parseResult = ContactFormBodySchema.safeParse(jsonData); - - if (!parseResult.success) { - logger.warn('Contact form - Invalid request body', { errors: parseResult.error.issues }); - return Response.json( - { - error: '올바르지 않은 요청 형식입니다.', - details: parseResult.error.issues - }, - { status: 400, headers: corsHeaders } - ); - } - - const body = parseResult.data; - - // 이메일 형식 검증 (Zod로 이미 검증됨, 추가 체크) - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(body.email)) { - return Response.json( - { error: '올바른 이메일 형식이 아닙니다.' }, - { status: 400, headers: corsHeaders } - ); - } - - // 메시지 길이 제한 - if (body.message.length > 2000) { - return Response.json( - { error: '메시지는 2000자 이내로 작성해주세요.' }, - { status: 400, headers: corsHeaders } - ); - } - - // 관리자에게 텔레그램 알림 - const adminId = env.DEPOSIT_ADMIN_ID || env.DOMAIN_OWNER_ID; - if (env.BOT_TOKEN && adminId) { - const timestamp = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }); - await sendMessage( - env.BOT_TOKEN, - parseInt(adminId), - `📬 웹사이트 문의\n\n` + - `📧 이메일: ${body.email}\n` + - `🕐 시간: ${timestamp}\n\n` + - `💬 내용:\n${body.message}` - ); - } - - console.log(`[Contact] 문의 수신: ${body.email}`); - - return Response.json( - { success: true, message: '문의가 성공적으로 전송되었습니다.' }, - { headers: corsHeaders } - ); - } catch (error) { - console.error('[Contact] Internal error:', error); - return Response.json( - { error: '문의 전송 중 오류가 발생했습니다.' }, - { status: 500, headers: corsHeaders } - ); - } + return handleContactForm(request, env); } // CORS preflight for contact API if (url.pathname === '/api/contact' && request.method === 'OPTIONS') { - const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com'; - return new Response(null, { - headers: { - 'Access-Control-Allow-Origin': allowedOrigin, - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }, - }); + return handleContactPreflight(env); } // Metrics API - Circuit Breaker 상태 조회 (관리자 전용) if (url.pathname === '/api/metrics' && request.method === 'GET') { - try { - // WEBHOOK_SECRET 인증 - const authHeader = request.headers.get('Authorization'); - if (!env.WEBHOOK_SECRET || authHeader !== `Bearer ${env.WEBHOOK_SECRET}`) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); - } - - // Circuit Breaker 상태 수집 - const openaiStats = openaiCircuitBreaker.getStats(); - - // 메트릭 응답 생성 - const metrics = { - timestamp: new Date().toISOString(), - circuitBreakers: { - openai: { - state: openaiStats.state, - failures: openaiStats.failures, - lastFailureTime: openaiStats.lastFailureTime?.toISOString(), - stats: openaiStats.stats, - config: openaiStats.config, - }, - }, - // 추후 확장 가능: API 호출 통계, 캐시 hit rate 등 - metrics: { - api_calls: { - // 추후 구현: 실제 API 호출 통계 - openai: { count: openaiStats.stats.totalRequests, avg_duration: 0 }, - }, - errors: { - // 추후 구현: 에러 통계 - retry_exhausted: 0, - circuit_breaker_open: openaiStats.state === 'OPEN' ? 1 : 0, - }, - cache: { - // 추후 구현: 캐시 hit rate - hit_rate: 0, - }, - }, - }; - - console.log('[Metrics API] Circuit breaker stats retrieved:', { - state: openaiStats.state, - failures: openaiStats.failures, - requests: openaiStats.stats.totalRequests, - }); - - return Response.json(metrics); - } catch (error) { - console.error('[Metrics API] Internal error:', error); - return Response.json({ error: 'Internal server error' }, { status: 500 }); - } + return handleMetrics(request, env); } return new Response('Not Found', { status: 404 });