refactor: 파일 분리 리팩토링 (routes, services, tools, utils)
아키텍처 개선: - index.ts: 921줄 → 205줄 (77% 감소) - openai-service.ts: 1,356줄 → 148줄 (89% 감소) 새로운 디렉토리 구조: - src/routes/ - Webhook, API, Health check 핸들러 - webhook.ts (287줄) - api.ts (318줄) - health.ts (14줄) - src/services/ - 비즈니스 로직 - bank-sms-parser.ts (143줄) - deposit-matcher.ts (88줄) - src/tools/ - Function Calling 도구 모듈화 - weather-tool.ts (37줄) - search-tool.ts (156줄) - domain-tool.ts (725줄) - deposit-tool.ts (183줄) - utility-tools.ts (60줄) - index.ts (104줄) - 도구 레지스트리 - src/utils/ - 유틸리티 함수 - email-decoder.ts - Quoted-Printable 디코더 타입 에러 수정: - routes/webhook.ts: text undefined 체크 - summary-service.ts: D1 타입 캐스팅 - summary-service.ts: Workers AI 타입 처리 - n8n-service.ts: Workers AI 타입 + 미사용 변수 제거 빌드 검증: - TypeScript 타입 체크 통과 - Wrangler dev 로컬 빌드 성공 문서: - REFACTORING_SUMMARY.md 추가 - ROUTE_ARCHITECTURE.md 추가 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
318
src/routes/api.ts
Normal file
318
src/routes/api.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { Env } from '../types';
|
||||
import { sendMessage } from '../telegram';
|
||||
import {
|
||||
addToBuffer,
|
||||
processAndSummarize,
|
||||
generateAIResponse,
|
||||
} from '../summary-service';
|
||||
import { handleCommand } from '../commands';
|
||||
|
||||
// 사용자 조회/생성
|
||||
async function getOrCreateUser(
|
||||
db: D1Database,
|
||||
telegramId: string,
|
||||
firstName: string,
|
||||
username?: string
|
||||
): Promise<number> {
|
||||
const existing = await db
|
||||
.prepare('SELECT id FROM users WHERE telegram_id = ?')
|
||||
.bind(telegramId)
|
||||
.first<{ id: number }>();
|
||||
|
||||
if (existing) {
|
||||
// 마지막 활동 시간 업데이트
|
||||
await db
|
||||
.prepare('UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
.bind(existing.id)
|
||||
.run();
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
// 새 사용자 생성
|
||||
const result = await db
|
||||
.prepare('INSERT INTO users (telegram_id, first_name, username) VALUES (?, ?, ?)')
|
||||
.bind(telegramId, firstName, username || null)
|
||||
.run();
|
||||
|
||||
return result.meta.last_row_id as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"}'
|
||||
*/
|
||||
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') {
|
||||
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: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 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 body = await request.json() as {
|
||||
telegram_id: string;
|
||||
amount: number;
|
||||
reason: string;
|
||||
reference_id?: string;
|
||||
};
|
||||
|
||||
if (!body.telegram_id || !body.amount || !body.reason) {
|
||||
return Response.json({ error: 'telegram_id, amount, reason required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (body.amount <= 0) {
|
||||
return Response.json({ error: 'Amount must be positive' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 사용자 조회
|
||||
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 });
|
||||
}
|
||||
|
||||
// 현재 잔액 확인
|
||||
const deposit = await env.DB.prepare(
|
||||
'SELECT balance FROM user_deposits WHERE user_id = ?'
|
||||
).bind(user.id).first<{ balance: number }>();
|
||||
|
||||
const currentBalance = deposit?.balance || 0;
|
||||
if (currentBalance < body.amount) {
|
||||
return Response.json({
|
||||
error: 'Insufficient balance',
|
||||
current_balance: currentBalance,
|
||||
required: body.amount,
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 트랜잭션: 잔액 차감 + 거래 기록
|
||||
await env.DB.batch([
|
||||
env.DB.prepare(
|
||||
'UPDATE user_deposits SET balance = balance - ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?'
|
||||
).bind(body.amount, user.id),
|
||||
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),
|
||||
]);
|
||||
|
||||
const newBalance = currentBalance - 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: currentBalance,
|
||||
new_balance: newBalance,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[API] Deposit deduct error:', error);
|
||||
return Response.json({ error: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 테스트 API - 메시지 처리 후 응답 직접 반환
|
||||
if (url.pathname === '/api/test' && request.method === 'POST') {
|
||||
try {
|
||||
const body = await request.json() as { text: string; user_id?: string; secret?: string };
|
||||
|
||||
// 간단한 인증
|
||||
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: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 문의 폼 API (웹사이트용)
|
||||
if (url.pathname === '/api/contact' && request.method === 'POST') {
|
||||
// CORS: hosting.anvil.it.com만 허용
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
};
|
||||
|
||||
try {
|
||||
const body = await request.json() as {
|
||||
email: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!body.email || !body.message) {
|
||||
return Response.json(
|
||||
{ error: '이메일과 메시지는 필수 항목입니다.' },
|
||||
{ status: 400, headers: corsHeaders }
|
||||
);
|
||||
}
|
||||
|
||||
// 이메일 형식 검증
|
||||
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),
|
||||
`📬 <b>웹사이트 문의</b>\n\n` +
|
||||
`📧 이메일: <code>${body.email}</code>\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] 오류:', error);
|
||||
return Response.json(
|
||||
{ error: '문의 전송 중 오류가 발생했습니다.' },
|
||||
{ status: 500, headers: corsHeaders }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// CORS preflight for contact API
|
||||
if (url.pathname === '/api/contact' && request.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
14
src/routes/health.ts
Normal file
14
src/routes/health.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Health Check 엔드포인트
|
||||
*
|
||||
* Manual Test:
|
||||
* 1. wrangler dev
|
||||
* 2. curl http://localhost:8787/health
|
||||
* 3. Expected: {"status":"ok","timestamp":"..."}
|
||||
*/
|
||||
export async function handleHealthCheck(): Promise<Response> {
|
||||
return Response.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
287
src/routes/webhook.ts
Normal file
287
src/routes/webhook.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { Env, TelegramUpdate } from '../types';
|
||||
import { validateWebhookRequest, checkRateLimit } from '../security';
|
||||
import { sendMessage, sendMessageWithKeyboard, sendChatAction, answerCallbackQuery, editMessageText } from '../telegram';
|
||||
import { executeDomainRegister } from '../domain-register';
|
||||
import {
|
||||
addToBuffer,
|
||||
processAndSummarize,
|
||||
generateAIResponse,
|
||||
} from '../summary-service';
|
||||
import { handleCommand } from '../commands';
|
||||
|
||||
// 사용자 조회/생성
|
||||
async function getOrCreateUser(
|
||||
db: D1Database,
|
||||
telegramId: string,
|
||||
firstName: string,
|
||||
username?: string
|
||||
): Promise<number> {
|
||||
const existing = await db
|
||||
.prepare('SELECT id FROM users WHERE telegram_id = ?')
|
||||
.bind(telegramId)
|
||||
.first<{ id: number }>();
|
||||
|
||||
if (existing) {
|
||||
// 마지막 활동 시간 업데이트
|
||||
await db
|
||||
.prepare('UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
.bind(existing.id)
|
||||
.run();
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
// 새 사용자 생성
|
||||
const result = await db
|
||||
.prepare('INSERT INTO users (telegram_id, first_name, username) VALUES (?, ?, ?)')
|
||||
.bind(telegramId, firstName, username || null)
|
||||
.run();
|
||||
|
||||
return result.meta.last_row_id as number;
|
||||
}
|
||||
|
||||
// 메시지 처리
|
||||
async function handleMessage(
|
||||
env: Env,
|
||||
update: TelegramUpdate
|
||||
): Promise<void> {
|
||||
if (!update.message?.text) return;
|
||||
|
||||
const { message } = update;
|
||||
const chatId = message.chat.id;
|
||||
const chatIdStr = chatId.toString();
|
||||
const text = message.text!; // Already checked above
|
||||
const telegramUserId = message.from.id.toString();
|
||||
|
||||
// Rate Limiting 체크 (KV 기반)
|
||||
if (!(await checkRateLimit(env.RATE_LIMIT_KV, telegramUserId))) {
|
||||
await sendMessage(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
'⚠️ 너무 많은 요청입니다. 잠시 후 다시 시도해주세요.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 처리 (오류 시 사용자에게 알림)
|
||||
let userId: number;
|
||||
try {
|
||||
userId = await getOrCreateUser(
|
||||
env.DB,
|
||||
telegramUserId,
|
||||
message.from.first_name,
|
||||
message.from.username
|
||||
);
|
||||
} catch (dbError) {
|
||||
console.error('[handleMessage] 사용자 DB 오류:', dbError);
|
||||
await sendMessage(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
'⚠️ 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let responseText: string;
|
||||
|
||||
try {
|
||||
// 명령어 처리
|
||||
if (text.startsWith('/')) {
|
||||
const [command, ...argParts] = text.split(' ');
|
||||
const args = argParts.join(' ');
|
||||
responseText = await handleCommand(env, userId, chatIdStr, command, args);
|
||||
|
||||
// /start 명령어는 미니앱 버튼과 함께 전송
|
||||
if (command === '/start') {
|
||||
await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, responseText, [
|
||||
[{ text: '🌐 서비스 보기', web_app: { url: 'https://hosting.anvil.it.com' } }],
|
||||
[{ text: '💬 문의하기', url: 'https://t.me/AnvilForgeBot' }],
|
||||
]);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 타이핑 표시
|
||||
await sendChatAction(env.BOT_TOKEN, chatId, 'typing');
|
||||
|
||||
// 1. 사용자 메시지 버퍼에 추가
|
||||
await addToBuffer(env.DB, userId, chatIdStr, 'user', text);
|
||||
|
||||
// 2. AI 응답 생성
|
||||
responseText = await generateAIResponse(env, userId, chatIdStr, text, telegramUserId);
|
||||
|
||||
// 3. 봇 응답 버퍼에 추가
|
||||
await addToBuffer(env.DB, userId, chatIdStr, 'bot', responseText);
|
||||
|
||||
// 4. 임계값 도달시 프로필 업데이트
|
||||
const { summarized } = await processAndSummarize(env, userId, chatIdStr);
|
||||
|
||||
if (summarized) {
|
||||
responseText += '\n\n<i>👤 프로필이 업데이트되었습니다.</i>';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[handleMessage] 처리 오류:', error);
|
||||
responseText = '⚠️ 메시지 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
|
||||
}
|
||||
|
||||
// 버튼 데이터 파싱
|
||||
const keyboardMatch = responseText.match(/__KEYBOARD__(.+?)__END__\n?/);
|
||||
if (keyboardMatch) {
|
||||
const cleanText = responseText.replace(/__KEYBOARD__.+?__END__\n?/, '');
|
||||
try {
|
||||
const keyboardData = JSON.parse(keyboardMatch[1]);
|
||||
|
||||
if (keyboardData.type === 'domain_register') {
|
||||
// 도메인 등록 확인 버튼
|
||||
const callbackData = `domain_reg:${keyboardData.domain}:${keyboardData.price}`;
|
||||
await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, cleanText, [
|
||||
[
|
||||
{ text: '✅ 등록하기', callback_data: callbackData },
|
||||
{ text: '❌ 취소', callback_data: 'domain_cancel' }
|
||||
]
|
||||
]);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Keyboard] 파싱 오류:', e);
|
||||
}
|
||||
}
|
||||
|
||||
await sendMessage(env.BOT_TOKEN, chatId, responseText);
|
||||
}
|
||||
|
||||
// Callback Query 처리 (인라인 버튼 클릭)
|
||||
async function handleCallbackQuery(
|
||||
env: Env,
|
||||
callbackQuery: TelegramUpdate['callback_query']
|
||||
): Promise<void> {
|
||||
if (!callbackQuery) return;
|
||||
|
||||
const { id: queryId, from, message, data } = callbackQuery;
|
||||
if (!data || !message) {
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 요청입니다.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = message.chat.id;
|
||||
const messageId = message.message_id;
|
||||
const telegramUserId = from.id.toString();
|
||||
|
||||
// 사용자 조회
|
||||
const user = await env.DB.prepare(
|
||||
'SELECT id FROM users WHERE telegram_id = ?'
|
||||
).bind(telegramUserId).first<{ id: number }>();
|
||||
|
||||
if (!user) {
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '사용자를 찾을 수 없습니다.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 도메인 등록 처리
|
||||
if (data.startsWith('domain_reg:')) {
|
||||
const parts = data.split(':');
|
||||
if (parts.length !== 3) {
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const domain = parts[1];
|
||||
const price = parseInt(parts[2]);
|
||||
|
||||
// 처리 중 표시
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '등록 처리 중...' });
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
`⏳ <b>${domain}</b> 등록 처리 중...`
|
||||
);
|
||||
|
||||
// 도메인 등록 실행
|
||||
const result = await executeDomainRegister(env, user.id, telegramUserId, domain, price);
|
||||
|
||||
if (result.success) {
|
||||
const expiresInfo = result.expiresAt ? `\n• 만료일: ${result.expiresAt}` : '';
|
||||
const nsInfo = result.nameservers && result.nameservers.length > 0
|
||||
? `\n\n🌐 <b>현재 네임서버:</b>\n${result.nameservers.map(ns => `• <code>${ns}</code>`).join('\n')}`
|
||||
: '';
|
||||
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
`✅ <b>도메인 등록 완료!</b>
|
||||
|
||||
• 도메인: <code>${result.domain}</code>
|
||||
• 결제 금액: ${result.price?.toLocaleString()}원
|
||||
• 현재 잔액: ${result.newBalance?.toLocaleString()}원${expiresInfo}${nsInfo}
|
||||
|
||||
🎉 축하합니다! 도메인이 성공적으로 등록되었습니다.
|
||||
네임서버 변경이 필요하면 말씀해주세요.`
|
||||
);
|
||||
} else {
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
`❌ <b>등록 실패</b>
|
||||
|
||||
${result.error}
|
||||
|
||||
다시 시도하시려면 도메인 등록을 요청해주세요.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 도메인 등록 취소
|
||||
if (data === 'domain_cancel') {
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '취소되었습니다.' });
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
'❌ 도메인 등록이 취소되었습니다.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Telegram Webhook 요청 처리
|
||||
*
|
||||
* Manual Test:
|
||||
* 1. wrangler dev
|
||||
* 2. curl -X POST http://localhost:8787/webhook \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -H "X-Telegram-Bot-Api-Secret-Token: test-secret" \
|
||||
* -d '{"message":{"chat":{"id":123},"text":"테스트"}}'
|
||||
* 3. Expected: OK response, message processed
|
||||
*/
|
||||
export async function handleWebhook(request: Request, env: Env): Promise<Response> {
|
||||
// 보안 검증
|
||||
const validation = await validateWebhookRequest(request, env);
|
||||
|
||||
if (!validation.valid) {
|
||||
console.error('[Webhook] 검증 실패:', validation.error);
|
||||
return new Response(validation.error, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const update = validation.update!;
|
||||
|
||||
// Callback Query 처리 (인라인 버튼 클릭)
|
||||
if (update.callback_query) {
|
||||
await handleCallbackQuery(env, update.callback_query);
|
||||
return new Response('OK');
|
||||
}
|
||||
|
||||
// 일반 메시지 처리
|
||||
await handleMessage(env, update);
|
||||
return new Response('OK');
|
||||
} catch (error) {
|
||||
console.error('[Webhook] 메시지 처리 오류:', error);
|
||||
return new Response('Error', { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user