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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user