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:
kappa
2026-01-18 15:24:03 +09:00
parent 89f8ea19f1
commit db859efc56
8 changed files with 567 additions and 23 deletions

137
src/domain-register.ts Normal file
View File

@@ -0,0 +1,137 @@
import { Env } from './types';
interface RegisterResult {
success: boolean;
domain?: string;
price?: number;
newBalance?: number;
nameservers?: string[];
expiresAt?: string;
error?: string;
}
// 도메인 등록 실행
export async function executeDomainRegister(
env: Env,
userId: number,
telegramUserId: string,
domain: string,
price: number
): Promise<RegisterResult> {
const apiKey = env.NAMECHEAP_API_KEY;
const apiUrl = 'https://namecheap-api.anvil.it.com';
if (!apiKey) {
return { success: false, error: 'API 키가 설정되지 않았습니다.' };
}
try {
// 1. 현재 잔액 확인
const balanceRow = await env.DB.prepare(
'SELECT balance FROM user_deposits WHERE user_id = ?'
).bind(userId).first<{ balance: number }>();
const currentBalance = balanceRow?.balance || 0;
if (currentBalance < price) {
return {
success: false,
error: `잔액이 부족합니다. (현재: ${currentBalance.toLocaleString()}원, 필요: ${price.toLocaleString()}원)`
};
}
// 2. Namecheap API로 도메인 등록
console.log(`[DomainRegister] 도메인 등록 요청: ${domain}, 가격: ${price}`);
const registerResponse = await fetch(`${apiUrl}/domains/register`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
domain: domain,
years: 1,
telegram_id: telegramUserId,
}),
});
const registerResult = await registerResponse.json() as {
registered?: boolean;
domain?: string;
error?: string;
detail?: string;
};
if (!registerResponse.ok || !registerResult.registered) {
const errorMsg = registerResult.error || registerResult.detail || '도메인 등록에 실패했습니다.';
console.error(`[DomainRegister] 등록 실패:`, registerResult);
return { success: false, error: errorMsg };
}
console.log(`[DomainRegister] 등록 성공:`, registerResult);
// 3. 잔액 차감 + 거래 기록 (트랜잭션)
await env.DB.batch([
env.DB.prepare(
'UPDATE user_deposits SET balance = balance - ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?'
).bind(price, userId),
env.DB.prepare(
`INSERT INTO deposit_transactions (user_id, type, amount, status, description, confirmed_at)
VALUES (?, 'withdrawal', ?, 'confirmed', ?, CURRENT_TIMESTAMP)`
).bind(userId, price, `도메인 등록: ${domain}`),
]);
// 4. user_domains 테이블에 추가
await env.DB.prepare(
'INSERT INTO user_domains (user_id, domain, verified, created_at) VALUES (?, ?, 1, datetime("now"))'
).bind(userId, domain).run();
// 5. 도메인 정보 조회 (네임서버 + 만료일)
let nameservers: string[] = [];
let expiresAt: string | undefined;
try {
// 도메인 정보에서 만료일 조회
const infoResponse = await fetch(`${apiUrl}/domains/${domain}/info`, {
headers: { 'X-API-Key': apiKey }
});
if (infoResponse.ok) {
const infoResult = await infoResponse.json() as { expires?: string };
if (infoResult.expires) {
// MM/DD/YYYY → YYYY-MM-DD 변환
const [month, day, year] = infoResult.expires.split('/');
expiresAt = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
}
}
// 네임서버 조회
const nsResponse = await fetch(`${apiUrl}/domains/${domain}/nameservers`, {
headers: { 'X-API-Key': apiKey }
});
if (nsResponse.ok) {
const nsResult = await nsResponse.json() as { nameservers?: string[] };
nameservers = nsResult.nameservers || [];
}
} catch (infoError) {
console.log(`[DomainRegister] 도메인 정보 조회 실패 (무시):`, infoError);
}
const newBalance = currentBalance - price;
console.log(`[DomainRegister] 완료: ${domain}, 잔액: ${currentBalance} -> ${newBalance}, 만료: ${expiresAt}, NS: ${nameservers.join(', ')}`);
return {
success: true,
domain: domain,
price: price,
newBalance: newBalance,
nameservers: nameservers,
expiresAt: expiresAt,
};
} catch (error) {
console.error(`[DomainRegister] 오류:`, error);
return {
success: false,
error: `도메인 등록 중 오류가 발생했습니다: ${String(error)}`
};
}
}