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 <noreply@anthropic.com>
This commit is contained in:
@@ -63,34 +63,14 @@ async function getOrCreateUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 엔드포인트 처리
|
* GET /api/deposit/balance - 잔액 조회 (namecheap-api 전용)
|
||||||
*
|
*
|
||||||
* Manual Test:
|
* @param request - HTTP Request
|
||||||
* 1. wrangler dev
|
* @param env - Environment bindings
|
||||||
* 2. Test deposit balance:
|
* @param url - Parsed URL
|
||||||
* curl http://localhost:8787/api/deposit/balance?telegram_id=123 \
|
* @returns JSON response with balance
|
||||||
* -H "X-API-Key: your-secret"
|
|
||||||
* 3. Test deposit deduct:
|
|
||||||
* curl -X POST http://localhost:8787/api/deposit/deduct \
|
|
||||||
* -H "X-API-Key: your-secret" \
|
|
||||||
* -H "Content-Type: application/json" \
|
|
||||||
* -d '{"telegram_id":"123","amount":1000,"reason":"test"}'
|
|
||||||
* 4. Test API:
|
|
||||||
* curl -X POST http://localhost:8787/api/test \
|
|
||||||
* -H "Content-Type: application/json" \
|
|
||||||
* -d '{"text":"hello","secret":"your-secret"}'
|
|
||||||
* 5. Test contact (from allowed origin):
|
|
||||||
* curl -X POST http://localhost:8787/api/contact \
|
|
||||||
* -H "Origin: https://hosting.anvil.it.com" \
|
|
||||||
* -H "Content-Type: application/json" \
|
|
||||||
* -d '{"email":"test@example.com","message":"test message"}'
|
|
||||||
* 6. Test metrics (Circuit Breaker status):
|
|
||||||
* curl http://localhost:8787/api/metrics \
|
|
||||||
* -H "Authorization: Bearer your-webhook-secret"
|
|
||||||
*/
|
*/
|
||||||
export async function handleApiRequest(request: Request, env: Env, url: URL): Promise<Response> {
|
async function handleDepositBalance(request: Request, env: Env, url: URL): Promise<Response> {
|
||||||
// Deposit API - 잔액 조회 (namecheap-api 전용)
|
|
||||||
if (url.pathname === '/api/deposit/balance' && request.method === 'GET') {
|
|
||||||
try {
|
try {
|
||||||
const apiSecret = env.DEPOSIT_API_SECRET;
|
const apiSecret = env.DEPOSIT_API_SECRET;
|
||||||
const authHeader = request.headers.get('X-API-Key');
|
const authHeader = request.headers.get('X-API-Key');
|
||||||
@@ -123,13 +103,19 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
|||||||
balance: deposit?.balance || 0,
|
balance: deposit?.balance || 0,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] Deposit balance error:', error);
|
logger.error('Deposit balance error', error as Error);
|
||||||
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deposit API - 잔액 차감 (namecheap-api 전용)
|
/**
|
||||||
if (url.pathname === '/api/deposit/deduct' && request.method === 'POST') {
|
* 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<Response> {
|
||||||
try {
|
try {
|
||||||
const apiSecret = env.DEPOSIT_API_SECRET;
|
const apiSecret = env.DEPOSIT_API_SECRET;
|
||||||
const authHeader = request.headers.get('X-API-Key');
|
const authHeader = request.headers.get('X-API-Key');
|
||||||
@@ -233,7 +219,12 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
|||||||
|
|
||||||
const newBalance = current.balance - body.amount;
|
const newBalance = current.balance - body.amount;
|
||||||
|
|
||||||
console.log(`[API] Deposit deducted: user=${body.telegram_id}, amount=${body.amount}, reason=${body.reason}`);
|
logger.info('Deposit deducted', {
|
||||||
|
telegram_id: body.telegram_id,
|
||||||
|
amount: body.amount,
|
||||||
|
reason: body.reason,
|
||||||
|
new_balance: newBalance
|
||||||
|
});
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -243,13 +234,19 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
|||||||
new_balance: newBalance,
|
new_balance: newBalance,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] Deposit deduct error:', error);
|
logger.error('Deposit deduct error', error as Error);
|
||||||
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 테스트 API - 메시지 처리 후 응답 직접 반환
|
/**
|
||||||
if (url.pathname === '/api/test' && request.method === 'POST') {
|
* 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<Response> {
|
||||||
try {
|
try {
|
||||||
const jsonData = await request.json();
|
const jsonData = await request.json();
|
||||||
const parseResult = TestApiBodySchema.safeParse(jsonData);
|
const parseResult = TestApiBodySchema.safeParse(jsonData);
|
||||||
@@ -312,13 +309,19 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
|||||||
user_id: telegramUserId,
|
user_id: telegramUserId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Test API] Error:', error);
|
logger.error('Test API error', error as Error);
|
||||||
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 문의 폼 API (웹사이트용)
|
/**
|
||||||
if (url.pathname === '/api/contact' && request.method === 'POST') {
|
* 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<Response> {
|
||||||
// CORS: hosting.anvil.it.com만 허용
|
// CORS: hosting.anvil.it.com만 허용
|
||||||
const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
|
const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
@@ -355,15 +358,6 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
|||||||
|
|
||||||
const body = parseResult.data;
|
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) {
|
if (body.message.length > 2000) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
@@ -386,23 +380,28 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Contact] 문의 수신: ${body.email}`);
|
logger.info('문의 수신', { email: body.email, hasName: !!body.name });
|
||||||
|
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ success: true, message: '문의가 성공적으로 전송되었습니다.' },
|
{ success: true, message: '문의가 성공적으로 전송되었습니다.' },
|
||||||
{ headers: corsHeaders }
|
{ headers: corsHeaders }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Contact] Internal error:', error);
|
logger.error('Contact form error', error as Error);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: '문의 전송 중 오류가 발생했습니다.' },
|
{ error: '문의 전송 중 오류가 발생했습니다.' },
|
||||||
{ status: 500, headers: corsHeaders }
|
{ status: 500, headers: corsHeaders }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORS preflight for contact API
|
/**
|
||||||
if (url.pathname === '/api/contact' && request.method === 'OPTIONS') {
|
* OPTIONS /api/contact - CORS preflight for contact API
|
||||||
|
*
|
||||||
|
* @param env - Environment bindings
|
||||||
|
* @returns Response with CORS headers
|
||||||
|
*/
|
||||||
|
async function handleContactPreflight(env: Env): Promise<Response> {
|
||||||
const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
|
const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -411,10 +410,16 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
|||||||
'Access-Control-Allow-Headers': 'Content-Type',
|
'Access-Control-Allow-Headers': 'Content-Type',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics API - Circuit Breaker 상태 조회 (관리자 전용)
|
/**
|
||||||
if (url.pathname === '/api/metrics' && request.method === 'GET') {
|
* 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<Response> {
|
||||||
try {
|
try {
|
||||||
// WEBHOOK_SECRET 인증
|
// WEBHOOK_SECRET 인증
|
||||||
const authHeader = request.headers.get('Authorization');
|
const authHeader = request.headers.get('Authorization');
|
||||||
@@ -455,17 +460,74 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[Metrics API] Circuit breaker stats retrieved:', {
|
logger.info('Metrics retrieved', {
|
||||||
state: openaiStats.state,
|
state: openaiStats.state,
|
||||||
failures: openaiStats.failures,
|
failures: openaiStats.failures,
|
||||||
requests: openaiStats.stats.totalRequests,
|
requests: openaiStats.stats.totalRequests
|
||||||
});
|
});
|
||||||
|
|
||||||
return Response.json(metrics);
|
return Response.json(metrics);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Metrics API] Internal error:', error);
|
logger.error('Metrics API error', error as Error);
|
||||||
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 엔드포인트 처리 (라우터)
|
||||||
|
*
|
||||||
|
* Manual Test:
|
||||||
|
* 1. wrangler dev
|
||||||
|
* 2. Test deposit balance:
|
||||||
|
* curl http://localhost:8787/api/deposit/balance?telegram_id=123 \
|
||||||
|
* -H "X-API-Key: your-secret"
|
||||||
|
* 3. Test deposit deduct:
|
||||||
|
* curl -X POST http://localhost:8787/api/deposit/deduct \
|
||||||
|
* -H "X-API-Key: your-secret" \
|
||||||
|
* -H "Content-Type: application/json" \
|
||||||
|
* -d '{"telegram_id":"123","amount":1000,"reason":"test"}'
|
||||||
|
* 4. Test API:
|
||||||
|
* curl -X POST http://localhost:8787/api/test \
|
||||||
|
* -H "Content-Type: application/json" \
|
||||||
|
* -d '{"text":"hello","secret":"your-secret"}'
|
||||||
|
* 5. Test contact (from allowed origin):
|
||||||
|
* curl -X POST http://localhost:8787/api/contact \
|
||||||
|
* -H "Origin: https://hosting.anvil.it.com" \
|
||||||
|
* -H "Content-Type: application/json" \
|
||||||
|
* -d '{"email":"test@example.com","message":"test message"}'
|
||||||
|
* 6. Test metrics (Circuit Breaker status):
|
||||||
|
* curl http://localhost:8787/api/metrics \
|
||||||
|
* -H "Authorization: Bearer your-webhook-secret"
|
||||||
|
*/
|
||||||
|
export async function handleApiRequest(request: Request, env: Env, url: URL): Promise<Response> {
|
||||||
|
// Deposit API - 잔액 조회 (namecheap-api 전용)
|
||||||
|
if (url.pathname === '/api/deposit/balance' && request.method === 'GET') {
|
||||||
|
return handleDepositBalance(request, env, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deposit API - 잔액 차감 (namecheap-api 전용)
|
||||||
|
if (url.pathname === '/api/deposit/deduct' && request.method === 'POST') {
|
||||||
|
return handleDepositDeduct(request, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테스트 API - 메시지 처리 후 응답 직접 반환
|
||||||
|
if (url.pathname === '/api/test' && request.method === 'POST') {
|
||||||
|
return handleTestApi(request, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 문의 폼 API (웹사이트용)
|
||||||
|
if (url.pathname === '/api/contact' && request.method === 'POST') {
|
||||||
|
return handleContactForm(request, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS preflight for contact API
|
||||||
|
if (url.pathname === '/api/contact' && request.method === 'OPTIONS') {
|
||||||
|
return handleContactPreflight(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics API - Circuit Breaker 상태 조회 (관리자 전용)
|
||||||
|
if (url.pathname === '/api/metrics' && request.method === 'GET') {
|
||||||
|
return handleMetrics(request, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
|
|||||||
Reference in New Issue
Block a user