refactor: improve type safety and code quality for 9.0 score
Type Safety Improvements: - Add isErrorResult() type guard for API responses (domain-tool.ts) - Replace `any` with `unknown` in executeTool args (tools/index.ts) - Add JSON.parse error handling in function calling (openai-service.ts) - Fix nullable price handling with nullish coalescing - Add array type guard for nameservers validation Code Quality Improvements: - Extract convertNamecheapDate() to eliminate duplicate functions - Move hardcoded bank account info to environment variables - Add JSDoc documentation to executeDepositFunction - Fix unused variables in optimistic-lock.ts - Handle Error.captureStackTrace for Workers environment All TypeScript strict mode checks now pass. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,12 @@ function isNamecheapPriceResponse(result: unknown): result is NamecheapPriceResp
|
||||
return typeof result === 'object' && result !== null && 'krw' in result;
|
||||
}
|
||||
|
||||
// Namecheap 날짜 형식 변환 (MM/DD/YYYY → YYYY-MM-DD)
|
||||
function convertNamecheapDate(date: string): string {
|
||||
const [month, day, year] = date.split('/');
|
||||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// KV 캐싱 인터페이스
|
||||
interface CachedTLDPrice {
|
||||
tld: string;
|
||||
@@ -75,9 +81,10 @@ async function setCachedTLDPrice(
|
||||
): Promise<void> {
|
||||
try {
|
||||
const key = `tld_price:${tld}`;
|
||||
const krwPrice = price.krw ?? price.register_krw ?? 0;
|
||||
const data: CachedTLDPrice = {
|
||||
tld,
|
||||
krw: price.krw || price.register_krw,
|
||||
krw: krwPrice,
|
||||
usd: price.usd,
|
||||
cached_at: new Date().toISOString(),
|
||||
};
|
||||
@@ -210,18 +217,13 @@ async function callNamecheapApi(
|
||||
}).then(r => r.json()),
|
||||
{ maxRetries: 3 }
|
||||
) as NamecheapDomainListItem[];
|
||||
// MM/DD/YYYY → YYYY-MM-DD 변환 (Namecheap은 미국 형식 사용)
|
||||
const convertDate = (date: string) => {
|
||||
const [month, day, year] = date.split('/');
|
||||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
};
|
||||
// 허용된 도메인만 필터링, 날짜는 ISO 형식으로 변환
|
||||
return result
|
||||
.filter((d: NamecheapDomainListItem) => allowedDomains.includes(d.name))
|
||||
.map((d: NamecheapDomainListItem) => ({
|
||||
...d,
|
||||
created: convertDate(d.created),
|
||||
expires: convertDate(d.expires),
|
||||
created: convertNamecheapDate(d.created),
|
||||
expires: convertNamecheapDate(d.expires),
|
||||
user: undefined, // 민감 정보 제거
|
||||
}));
|
||||
}
|
||||
@@ -238,16 +240,11 @@ async function callNamecheapApi(
|
||||
if (!domainInfo) {
|
||||
return { error: `도메인을 찾을 수 없습니다: ${funcArgs.domain}` };
|
||||
}
|
||||
// MM/DD/YYYY → YYYY-MM-DD 변환 (Namecheap은 미국 형식 사용)
|
||||
const convertDate = (date: string) => {
|
||||
const [month, day, year] = date.split('/');
|
||||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
};
|
||||
// 민감 정보 필터링 (user/owner 제거), 날짜는 ISO 형식으로 변환
|
||||
return {
|
||||
domain: domainInfo.name,
|
||||
created: convertDate(domainInfo.created),
|
||||
expires: convertDate(domainInfo.expires),
|
||||
created: convertNamecheapDate(domainInfo.created),
|
||||
expires: convertNamecheapDate(domainInfo.expires),
|
||||
is_expired: domainInfo.is_expired,
|
||||
auto_renew: domainInfo.auto_renew,
|
||||
is_locked: domainInfo.is_locked,
|
||||
@@ -275,8 +272,9 @@ async function callNamecheapApi(
|
||||
if (!res.ok) {
|
||||
// Namecheap 에러 메시지 파싱
|
||||
if (text.includes('subordinate hosts') || text.includes('Non existen')) {
|
||||
const nsArray = Array.isArray(nameservers) ? nameservers : [];
|
||||
return {
|
||||
error: `네임서버 변경 실패: ${funcArgs.nameservers.join(', ')}는 등록되지 않은 네임서버입니다. 자기 도메인을 네임서버로 사용하려면 먼저 Namecheap에서 Child Nameserver(글루 레코드)를 IP 주소와 함께 등록해야 합니다.`
|
||||
error: `네임서버 변경 실패: ${nsArray.join(', ')}는 등록되지 않은 네임서버입니다. 자기 도메인을 네임서버로 사용하려면 먼저 Namecheap에서 Child Nameserver(글루 레코드)를 IP 주소와 함께 등록해야 합니다.`
|
||||
};
|
||||
}
|
||||
return { error: `네임서버 변경 실패: ${text}` };
|
||||
@@ -485,19 +483,34 @@ async function executeDomainAction(
|
||||
}
|
||||
|
||||
const result = await callNamecheapApi('get_domain_info', { domain }, allowedDomains, env, telegramUserId, db, userId);
|
||||
|
||||
if (result.error) {
|
||||
|
||||
if (isErrorResult(result)) {
|
||||
// 계정 내 도메인 정보 조회 실패 시 WHOIS로 폴백
|
||||
return executeDomainAction('whois', args, allowedDomains, env, telegramUserId, db, userId);
|
||||
}
|
||||
return `📋 ${domain} 정보\n\n• 생성일: ${result.created}\n• 만료일: ${result.expires}\n• 자동갱신: ${result.auto_renew ? '✅' : '❌'}\n• 잠금: ${result.is_locked ? '🔒' : '🔓'}\n• WHOIS Guard: ${result.whois_guard ? '✅' : '❌'}`;
|
||||
|
||||
// Type assertion after error check
|
||||
const domainInfo = result as {
|
||||
domain: string;
|
||||
created: string;
|
||||
expires: string;
|
||||
is_expired: boolean;
|
||||
auto_renew: boolean;
|
||||
is_locked: boolean;
|
||||
whois_guard: boolean;
|
||||
};
|
||||
|
||||
return `📋 ${domain} 정보\n\n• 생성일: ${domainInfo.created}\n• 만료일: ${domainInfo.expires}\n• 자동갱신: ${domainInfo.auto_renew ? '✅' : '❌'}\n• 잠금: ${domainInfo.is_locked ? '🔒' : '🔓'}\n• WHOIS Guard: ${domainInfo.whois_guard ? '✅' : '❌'}`;
|
||||
}
|
||||
|
||||
case 'get_ns': {
|
||||
if (!domain) return '🚫 도메인을 지정해주세요.';
|
||||
const result = await callNamecheapApi('get_nameservers', { domain }, allowedDomains, env, telegramUserId, db, userId);
|
||||
if (result.error) return `🚫 ${result.error}`;
|
||||
const nsList = (result.nameservers || result).map((ns: string) => `• ${ns}`).join('\n');
|
||||
if (isErrorResult(result)) return `🚫 ${result.error}`;
|
||||
|
||||
const nsResult = result as { nameservers?: string[] } | string[];
|
||||
const nameserverList = Array.isArray(nsResult) ? nsResult : (nsResult.nameservers || []);
|
||||
const nsList = nameserverList.map((ns: string) => `• ${ns}`).join('\n');
|
||||
return `🌐 ${domain} 네임서버\n\n${nsList}`;
|
||||
}
|
||||
|
||||
@@ -506,7 +519,7 @@ async function executeDomainAction(
|
||||
if (!nameservers?.length) return '🚫 네임서버를 지정해주세요.';
|
||||
if (!allowedDomains.includes(domain)) return `🚫 ${domain}은 관리 권한이 없습니다.`;
|
||||
const result = await callNamecheapApi('set_nameservers', { domain, nameservers }, allowedDomains, env, telegramUserId, db, userId);
|
||||
if (result.error) return `🚫 ${result.error}`;
|
||||
if (isErrorResult(result)) return `🚫 ${result.error}`;
|
||||
return `✅ ${domain} 네임서버 변경 완료\n\n${nameservers.map(ns => `• ${ns}`).join('\n')}`;
|
||||
}
|
||||
|
||||
@@ -542,7 +555,8 @@ async function executeDomainAction(
|
||||
}
|
||||
}
|
||||
|
||||
return `✅ ${domain}은 등록 가능합니다.\n\n💰 가격: ${price?.toLocaleString()}원/년\n\n등록하시려면 "${domain} 등록해줘"라고 말씀해주세요.`;
|
||||
const priceStr = price ? `${price.toLocaleString()}원/년` : '가격 조회 중';
|
||||
return `✅ ${domain}은 등록 가능합니다.\n\n💰 가격: ${priceStr}\n\n등록하시려면 "${domain} 등록해줘"라고 말씀해주세요.`;
|
||||
}
|
||||
return `❌ ${domain}은 이미 등록된 도메인입니다.`;
|
||||
}
|
||||
@@ -635,7 +649,9 @@ async function executeDomainAction(
|
||||
if (statuses.length) response += `• 상태: ${statuses.join(', ')}\n`;
|
||||
if (dnssec !== '-') response += `• DNSSEC: ${dnssec}`;
|
||||
|
||||
if (result.available === true) {
|
||||
// Type guard to check if result has 'available' property
|
||||
const typedResult = result as { available?: boolean };
|
||||
if (typedResult.available === true) {
|
||||
response += `\n\n✅ 이 도메인은 등록 가능합니다!`;
|
||||
}
|
||||
|
||||
@@ -657,16 +673,19 @@ async function executeDomainAction(
|
||||
|
||||
// API 호출
|
||||
const result = await callNamecheapApi('get_price', { tld: targetTld }, allowedDomains, env, telegramUserId, db, userId);
|
||||
if (result.error) return `🚫 ${result.error}`;
|
||||
if (isErrorResult(result)) return `🚫 ${result.error}`;
|
||||
|
||||
// Type assertion after error check
|
||||
const priceResult = result as NamecheapPriceResponse;
|
||||
|
||||
// 캐시 저장
|
||||
if (env?.RATE_LIMIT_KV) {
|
||||
await setCachedTLDPrice(env.RATE_LIMIT_KV, targetTld, result);
|
||||
await setCachedTLDPrice(env.RATE_LIMIT_KV, targetTld, priceResult);
|
||||
}
|
||||
|
||||
// API 응답: { tld, usd, krw }
|
||||
const price = result.krw || result.register_krw;
|
||||
return `💰 .${targetTld} 도메인 가격\n\n• 등록/갱신: ${price?.toLocaleString()}원/년`;
|
||||
const price = priceResult.krw ?? priceResult.register_krw ?? 0;
|
||||
return `💰 .${targetTld} 도메인 가격\n\n• 등록/갱신: ${price.toLocaleString()}원/년`;
|
||||
}
|
||||
|
||||
case 'cheapest': {
|
||||
@@ -719,14 +738,18 @@ async function executeDomainAction(
|
||||
|
||||
// 1. 가용성 확인
|
||||
const checkResult = await callNamecheapApi('check_domains', { domains: [domain] }, allowedDomains, env, telegramUserId, db, userId);
|
||||
if (checkResult.error) return `🚫 ${checkResult.error}`;
|
||||
if (!checkResult[domain]) return `❌ ${domain}은 이미 등록된 도메인입니다.`;
|
||||
if (isErrorResult(checkResult)) return `🚫 ${checkResult.error}`;
|
||||
|
||||
const availability = checkResult as NamecheapCheckResult;
|
||||
if (!availability[domain]) return `❌ ${domain}은 이미 등록된 도메인입니다.`;
|
||||
|
||||
// 2. 가격 조회
|
||||
const domainTld = domain.split('.').pop() || '';
|
||||
const priceResult = await callNamecheapApi('get_price', { tld: domainTld }, allowedDomains, env, telegramUserId, db, userId);
|
||||
if (priceResult.error) return `🚫 가격 조회 실패: ${priceResult.error}`;
|
||||
const price = priceResult.krw || priceResult.register_krw;
|
||||
if (isErrorResult(priceResult)) return `🚫 가격 조회 실패: ${priceResult.error}`;
|
||||
|
||||
const priceData = priceResult as NamecheapPriceResponse;
|
||||
const price = priceData.krw ?? priceData.register_krw ?? 0;
|
||||
|
||||
// 3. 잔액 조회
|
||||
let balance = 0;
|
||||
@@ -846,6 +869,9 @@ export async function executeSuggestDomains(args: { keywords: string }, env?: En
|
||||
return '🚫 도메인 추천 기능이 설정되지 않았습니다. (NAMECHEAP_API_KEY 미설정)';
|
||||
}
|
||||
|
||||
// Store API key after null check
|
||||
const namecheapApiKey = env.NAMECHEAP_API_KEY;
|
||||
|
||||
try {
|
||||
const namecheapApiUrl = env.NAMECHEAP_API_URL || 'https://namecheap-api.anvil.it.com';
|
||||
const TARGET_COUNT = 10;
|
||||
@@ -926,7 +952,7 @@ ${excludeList ? `- 다음 도메인은 제외하세요: ${excludeList}` : ''}
|
||||
() => fetch(`${namecheapApiUrl}/domains/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-API-Key': env.NAMECHEAP_API_KEY!, // Already checked above
|
||||
'X-API-Key': namecheapApiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ domains: newDomains }),
|
||||
@@ -971,7 +997,7 @@ ${excludeList ? `- 다음 도메인은 제외하세요: ${excludeList}` : ''}
|
||||
// 캐시 미스 시 API 호출
|
||||
const priceRes = await retryWithBackoff(
|
||||
() => fetch(`${namecheapApiUrl}/prices/${tld}`, {
|
||||
headers: { 'X-API-Key': env.NAMECHEAP_API_KEY! }, // Already checked above
|
||||
headers: { 'X-API-Key': namecheapApiKey },
|
||||
}),
|
||||
{ maxRetries: 3 }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user