feat: 도메인 인라인 버튼 등록 + cheapest TLD + Cron 자동취소
- 도메인 등록 인라인 버튼 확인 플로우 (domain-register.ts) - manage_domain에 cheapest action 추가 (가장 저렴한 TLD TOP 15) - 24시간 경과 입금 대기 자동 취소 Cron (UTC 15:00) - 거래 내역 한글 라벨 + description 표시 - CLAUDE.md 문서 업데이트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
192
src/index.ts
192
src/index.ts
@@ -1,6 +1,7 @@
|
||||
import { Env, TelegramUpdate, EmailMessage, BankNotification } from './types';
|
||||
import { validateWebhookRequest, checkRateLimit } from './security';
|
||||
import { sendMessage, sendMessageWithKeyboard, setWebhook, getWebhookInfo, sendChatAction } from './telegram';
|
||||
import { sendMessage, sendMessageWithKeyboard, setWebhook, getWebhookInfo, sendChatAction, answerCallbackQuery, editMessageText } from './telegram';
|
||||
import { executeDomainRegister } from './domain-register';
|
||||
import {
|
||||
addToBuffer,
|
||||
processAndSummarize,
|
||||
@@ -111,9 +112,131 @@ async function handleMessage(
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼 데이터 파싱
|
||||
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);
|
||||
}
|
||||
|
||||
export default {
|
||||
// HTTP 요청 핸들러
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
@@ -263,7 +386,7 @@ export default {
|
||||
).bind(body.amount, user.id),
|
||||
env.DB.prepare(
|
||||
`INSERT INTO deposit_transactions (user_id, type, amount, status, description, confirmed_at)
|
||||
VALUES (?, 'deduct', ?, 'confirmed', ?, CURRENT_TIMESTAMP)`
|
||||
VALUES (?, 'withdrawal', ?, 'confirmed', ?, CURRENT_TIMESTAMP)`
|
||||
).bind(user.id, body.amount, body.reason),
|
||||
]);
|
||||
|
||||
@@ -353,7 +476,16 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
await handleMessage(env, validation.update!);
|
||||
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('Message handling error:', error);
|
||||
@@ -460,6 +592,60 @@ Documentation: https://github.com/your-repo
|
||||
console.error('[Email] 처리 오류:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Cron Trigger: 만료된 입금 대기 자동 취소 (24시간)
|
||||
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||
console.log('[Cron] 만료된 입금 대기 정리 시작');
|
||||
|
||||
try {
|
||||
// 24시간 이상 된 pending 거래 조회
|
||||
const expiredTxs = await env.DB.prepare(
|
||||
`SELECT dt.id, dt.amount, dt.depositor_name, u.telegram_id
|
||||
FROM deposit_transactions dt
|
||||
JOIN users u ON dt.user_id = u.id
|
||||
WHERE dt.status = 'pending'
|
||||
AND dt.type = 'deposit'
|
||||
AND datetime(dt.created_at) < datetime('now', '-1 day')
|
||||
LIMIT 100`
|
||||
).all<{
|
||||
id: number;
|
||||
amount: number;
|
||||
depositor_name: string;
|
||||
telegram_id: string;
|
||||
}>();
|
||||
|
||||
if (!expiredTxs.results?.length) {
|
||||
console.log('[Cron] 만료된 거래 없음');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Cron] 만료된 거래 ${expiredTxs.results.length}건 발견`);
|
||||
|
||||
for (const tx of expiredTxs.results) {
|
||||
// 상태를 cancelled로 변경
|
||||
await env.DB.prepare(
|
||||
"UPDATE deposit_transactions SET status = 'cancelled', description = '입금 대기 만료 (24시간)' WHERE id = ?"
|
||||
).bind(tx.id).run();
|
||||
|
||||
// 사용자에게 알림
|
||||
await sendMessage(
|
||||
env.BOT_TOKEN,
|
||||
parseInt(tx.telegram_id),
|
||||
`⏰ <b>입금 대기 만료</b>\n\n` +
|
||||
`입금자: ${tx.depositor_name}\n` +
|
||||
`금액: ${tx.amount.toLocaleString()}원\n\n` +
|
||||
`24시간 이내에 입금이 확인되지 않아 자동 취소되었습니다.\n` +
|
||||
`다시 입금하시려면 입금 후 알려주세요.`
|
||||
);
|
||||
|
||||
console.log(`[Cron] 거래 #${tx.id} 만료 처리 완료`);
|
||||
}
|
||||
|
||||
console.log('[Cron] 만료된 입금 대기 정리 완료');
|
||||
} catch (error) {
|
||||
console.error('[Cron] 오류:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Quoted-Printable UTF-8 디코딩
|
||||
|
||||
Reference in New Issue
Block a user