feat(domain): enhance domain info lookup & handler refactoring

- 도메인 조회(info): 내 도메인 아니면 자동으로 WHOIS 조회 (naver.com 등 지원)
- SMS 파싱: 정규식 실패 시 AI 폴백 로직 추가
- 리팩토링: UserService, ConversationService 분리
- 문서: README.md 및 CODE_REVIEW.md 업데이트
This commit is contained in:
kappa
2026-01-19 17:12:07 +09:00
parent d4c0525451
commit 410676e322
10 changed files with 1900 additions and 160 deletions

View File

@@ -1,45 +1,12 @@
import { Env, TelegramUpdate } from '../types';
import { validateWebhookRequest, checkRateLimit } from '../security';
import { sendMessage, sendMessageWithKeyboard, sendChatAction, answerCallbackQuery, editMessageText } from '../telegram';
import { sendMessage, sendMessageWithKeyboard, answerCallbackQuery, editMessageText } from '../telegram';
import { executeDomainRegister } from '../domain-register';
import {
addToBuffer,
processAndSummarize,
generateAIResponse,
} from '../summary-service';
import { handleCommand } from '../commands';
import { UserService } from '../services/user-service';
import { ConversationService } from '../services/conversation-service';
// 사용자 조회/생성
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
@@ -49,10 +16,10 @@ async function handleMessage(
const { message } = update;
const chatId = message.chat.id;
const chatIdStr = chatId.toString();
const text = message.text!; // Already checked above
const text = message.text!;
const telegramUserId = message.from.id.toString();
// Rate Limiting 체크 (KV 기반)
// 1. Rate Limiting 체크
if (!(await checkRateLimit(env.RATE_LIMIT_KV, telegramUserId))) {
await sendMessage(
env.BOT_TOKEN,
@@ -62,11 +29,14 @@ async function handleMessage(
return;
}
// 사용자 처리 (오류 시 사용자에게 알림)
// 2. 서비스 인스턴스 초기화
const userService = new UserService(env.DB);
const conversationService = new ConversationService(env);
// 3. 사용자 조회/생성
let userId: number;
try {
userId = await getOrCreateUser(
env.DB,
userId = await userService.getOrCreateUser(
telegramUserId,
message.from.first_name,
message.from.username
@@ -81,14 +51,12 @@ async function handleMessage(
return;
}
let responseText: string;
try {
// 명령어 처리
// 4. 명령어 처리
if (text.startsWith('/')) {
const [command, ...argParts] = text.split(' ');
const args = argParts.join(' ');
responseText = await handleCommand(env, userId, chatIdStr, command, args);
const responseText = await handleCommand(env, userId, chatIdStr, command, args);
// /start 명령어는 미니앱 버튼과 함께 전송
if (command === '/start') {
@@ -98,55 +66,47 @@ async function handleMessage(
]);
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>';
}
await sendMessage(env.BOT_TOKEN, chatId, responseText);
return;
}
// 5. 일반 대화 처리 (ConversationService 위임)
const result = await conversationService.processUserMessage(
userId,
chatIdStr,
text,
telegramUserId
);
let finalResponse = result.responseText;
if (result.isProfileUpdated) {
finalResponse += '\n\n<i>👤 프로필이 업데이트되었습니다.</i>';
}
// 6. 응답 전송 (키보드 포함 여부 확인)
if (result.keyboardData && result.keyboardData.type === 'domain_register') {
const { domain, price } = result.keyboardData;
const callbackData = `domain_reg:${domain}:${price}`;
await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, finalResponse, [
[
{ text: '✅ 등록하기', callback_data: callbackData },
{ text: '❌ 취소', callback_data: 'domain_cancel' }
]
]);
} else {
await sendMessage(env.BOT_TOKEN, chatId, finalResponse);
}
} catch (error) {
console.error('[handleMessage] 처리 오류:', error);
responseText = '⚠️ 메시지 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
await sendMessage(
env.BOT_TOKEN,
chatId,
'⚠️ 메시지 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'
);
}
// 버튼 데이터 파싱
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 처리 (인라인 버튼 클릭)
@@ -166,10 +126,8 @@ async function handleCallbackQuery(
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 }>();
const userService = new UserService(env.DB);
const user = await userService.getUserByTelegramId(telegramUserId);
if (!user) {
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '사용자를 찾을 수 없습니다.' });
@@ -187,7 +145,6 @@ async function handleCallbackQuery(
const domain = parts[1];
const price = parseInt(parts[2]);
// 처리 중 표시
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '등록 처리 중...' });
await editMessageText(
env.BOT_TOKEN,
@@ -196,7 +153,6 @@ async function handleCallbackQuery(
`⏳ <b>${domain}</b> 등록 처리 중...`
);
// 도메인 등록 실행
const result = await executeDomainRegister(env, user.id, telegramUserId, domain, price);
if (result.success) {
@@ -250,17 +206,8 @@ ${result.error}
/**
* 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) {
@@ -271,17 +218,15 @@ export async function handleWebhook(request: Request, env: Env): Promise<Respons
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 });
}
}
}