From 6392a17d4ff5a787f86578b429e556e395ea0424 Mon Sep 17 00:00:00 2001 From: kappa Date: Tue, 27 Jan 2026 12:33:58 +0900 Subject: [PATCH] refactor: extract common bandwidth formatting utilities - Add BandwidthInfo interface to types.ts (single source of truth) - Create formatters.ts with formatTB() and formatTrafficInfo() - Display CDN hit rate and gross/origin traffic in recommendations - Fix floating point formatting (consistent decimal places) - Fix undefined handling in toLocaleString() calls - Unify overage detection logic (overage_tb > 0 && cost > 0) - Add CDN hit rate range validation (0-100%) - Extract CDN_CACHE_HIT_RATES constants Co-Authored-By: Claude Opus 4.5 --- src/server-agent.ts | 42 +++++++++++++++++------ src/tools/server-tool.ts | 54 ++++++++++++++---------------- src/types.ts | 24 +++++++++++++- src/utils/formatters.ts | 72 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 40 deletions(-) create mode 100644 src/utils/formatters.ts diff --git a/src/server-agent.ts b/src/server-agent.ts index f7f683d..3a521dd 100644 --- a/src/server-agent.ts +++ b/src/server-agent.ts @@ -9,9 +9,10 @@ * - Brave Search / Context7 도구로 최신 트렌드 반영 */ -import type { Env, ServerSession } from './types'; +import type { Env, ServerSession, BandwidthInfo } from './types'; import { createLogger } from './utils/logger'; import { executeSearchWeb, executeLookupDocs } from './tools/search-tool'; +import { formatTrafficInfo } from './utils/formatters'; const logger = createLogger('server-agent'); @@ -215,11 +216,7 @@ interface RecommendResponse { estimated_capacity?: { max_concurrent_users?: number; }; - bandwidth_analysis?: { - estimated_monthly_tb?: number; - overage_tb?: number; - overage_cost_krw?: number; - }; + bandwidth_info?: BandwidthInfo; }>; } @@ -561,12 +558,32 @@ export async function processServerConsultation( plan: selected.plan_name }); + // 트래픽 정보 포맷팅 + let trafficInfo = ''; + if (selected.price.estimated_monthly_tb !== undefined) { + const bandwidthInfo: BandwidthInfo = { + included_transfer_tb: selected.price.bandwidth_tb, + overage_cost_per_gb: 0, + overage_cost_per_tb: 0, + estimated_monthly_tb: selected.price.estimated_monthly_tb, + estimated_overage_tb: selected.price.overage_tb || 0, + estimated_overage_cost: selected.price.overage_cost_krw || 0, + total_estimated_cost: selected.price.monthly_krw + (selected.price.overage_cost_krw || 0), + currency: 'KRW', + gross_monthly_tb: selected.price.gross_monthly_tb, + cdn_cache_hit_rate: selected.price.cdn_cache_hit_rate, + }; + trafficInfo = `• ${formatTrafficInfo(bandwidthInfo, 'KRW')}\n`; + } + return `🖥️ ${selected.plan_name} 신청 확인\n\n` + `• 제공사: ${selected.provider}\n` + - `• 스펙: ${selected.specs.vcpu}vCPU / ${selected.specs.ram_gb}GB / ${selected.specs.storage_gb}GB\n` + + `• 스펙: ${selected.specs.vcpu}vCPU / ${selected.specs.ram_gb}GB RAM / ${selected.specs.storage_gb}GB SSD\n` + `• 리전: ${selected.region.name} (${selected.region.code})\n` + - `• 가격: ₩${selected.price.monthly_krw.toLocaleString()}/월\n\n` + - `신청하시겠습니까?\n\n` + + `• 가격: ₩${selected.price.monthly_krw.toLocaleString()}/월\n` + + `• 대역폭: ${selected.price.bandwidth_tb}TB 포함\n` + + trafficInfo + + `\n신청하시겠습니까?\n\n` + `__KEYBOARD__${keyboardData}__END__`; } else { return `번호를 다시 확인해주세요. 1번부터 ${session.lastRecommendation.recommendations.length}번 중에서 선택해주세요.`; @@ -646,7 +663,12 @@ export async function processServerConsultation( }, price: { monthly_krw: Math.round(rec.server.monthly_price), - bandwidth_tb: rec.server.transfer_tb + bandwidth_tb: rec.server.transfer_tb, + estimated_monthly_tb: rec.bandwidth_info?.estimated_monthly_tb, + gross_monthly_tb: rec.bandwidth_info?.gross_monthly_tb, + cdn_cache_hit_rate: rec.bandwidth_info?.cdn_cache_hit_rate, + overage_tb: rec.bandwidth_info?.estimated_overage_tb, + overage_cost_krw: rec.bandwidth_info?.estimated_overage_cost }, score: rec.score, max_users: rec.estimated_capacity?.max_concurrent_users || 0 diff --git a/src/tools/server-tool.ts b/src/tools/server-tool.ts index 744dd8a..b297a43 100644 --- a/src/tools/server-tool.ts +++ b/src/tools/server-tool.ts @@ -1,10 +1,20 @@ -import type { Env } from '../types'; +import type { Env, BandwidthInfo } from '../types'; import { retryWithBackoff, RetryError } from '../utils/retry'; import { createLogger, maskUserId } from '../utils/logger'; import { ERROR_MESSAGES } from '../constants/messages'; +import { formatTrafficInfo } from '../utils/formatters'; const logger = createLogger('server-tool'); +// CDN 캐시 히트율 상수 +const CDN_CACHE_HIT_RATES = { + VIDEO_STREAMING: 0.92, + STATIC_SITE: 0.95, + API: 0.30, + ECOMMERCE: 0.70, + DEFAULT: 0.85, +} as const; + // 언어 감지 (한글/일본어/중국어/영어) function detectLanguage(text: string): 'ko' | 'ja' | 'zh' | 'en' { if (/[가-힣]/.test(text)) return 'ko'; @@ -32,12 +42,12 @@ function estimateCdnCacheHitRate(techStack: string[], useCase: string): number | const isEcommerce = /shop|store|commerce|쇼핑|이커머스/.test(useCaseLower); // 콘텐츠 타입별 예상 캐시 히트율 - if (isVideoStreaming) return 0.92; // 비디오: 92% (대부분 캐시 가능) - if (isStaticSite) return 0.95; // 정적 사이트: 95% - if (isApi) return 0.30; // API: 30% (동적 콘텐츠 많음) - if (isEcommerce) return 0.70; // 이커머스: 70% (상품 이미지 캐시) + if (isVideoStreaming) return CDN_CACHE_HIT_RATES.VIDEO_STREAMING; + if (isStaticSite) return CDN_CACHE_HIT_RATES.STATIC_SITE; + if (isApi) return CDN_CACHE_HIT_RATES.API; + if (isEcommerce) return CDN_CACHE_HIT_RATES.ECOMMERCE; - return 0.85; // 기본값: 85% + return CDN_CACHE_HIT_RATES.DEFAULT; } // Type guards @@ -94,16 +104,7 @@ interface ServerRecommendation { max_concurrent_users: number; requests_per_second: number; }; - bandwidth_info?: { - included_transfer_tb: number; - overage_cost_per_gb: number; - overage_cost_per_tb: number; - estimated_monthly_tb: number; - estimated_overage_tb: number; - estimated_overage_cost: number; - total_estimated_cost: number; - currency: string; - }; + bandwidth_info?: BandwidthInfo; benchmark_reference?: { processor_name: string; benchmarks: BenchmarkItem[]; @@ -334,20 +335,15 @@ function formatRecommendations(data: RecommendResponse): string { // 대역폭 정보 (항상 표시) if (rec.bandwidth_info) { - const bw = rec.bandwidth_info; - if (bw.estimated_overage_cost > 0) { - // 초과 비용 있음 - const overageCost = bw.currency === 'KRW' - ? `₩${Math.round(bw.estimated_overage_cost).toLocaleString()}` - : `$${bw.estimated_overage_cost.toFixed(2)}`; - const totalCost = bw.currency === 'KRW' - ? `₩${Math.round(bw.total_estimated_cost).toLocaleString()}` - : `$${bw.total_estimated_cost.toFixed(2)}`; - response += ` • 예상 트래픽: ${bw.estimated_monthly_tb}TB → 초과 ${bw.estimated_overage_tb}TB (${overageCost})\n`; + const trafficInfo = formatTrafficInfo(rec.bandwidth_info, server.currency); + response += ` • ${trafficInfo}\n`; + + // 총 예상 비용 (초과 있을 때만 표시) + if (rec.bandwidth_info.estimated_overage_tb > 0 && rec.bandwidth_info.estimated_overage_cost > 0) { + const totalCost = server.currency === 'KRW' + ? `₩${Math.round(rec.bandwidth_info.total_estimated_cost).toLocaleString()}` + : `$${rec.bandwidth_info.total_estimated_cost.toFixed(2)}`; response += ` • 총 예상 비용: ${totalCost}/월\n`; - } else { - // 포함 범위 내 - response += ` • 예상 트래픽: ${bw.estimated_monthly_tb}TB (포함 범위 내)\n`; } } diff --git a/src/types.ts b/src/types.ts index 290292b..18e756c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -242,7 +242,15 @@ export interface ServerSession { provider: string; specs: { vcpu: number; ram_gb: number; storage_gb: number }; region: { code: string; name: string }; - price: { monthly_krw: number; bandwidth_tb: number }; + price: { + monthly_krw: number; + bandwidth_tb: number; + estimated_monthly_tb?: number; // origin 트래픽 (서버 도달) + gross_monthly_tb?: number; // CDN 전 트래픽 + cdn_cache_hit_rate?: number; // CDN 히트율 (0.0-1.0) + overage_tb?: number; + overage_cost_krw?: number; + }; score: number; max_users: number; }>; @@ -404,6 +412,20 @@ export interface ServerOrderKeyboardData { export type KeyboardData = DomainRegisterKeyboardData | ServerOrderKeyboardData; +// Bandwidth Info (shared by server-agent and server-tool) +export interface BandwidthInfo { + included_transfer_tb: number; + overage_cost_per_gb: number; + overage_cost_per_tb: number; + estimated_monthly_tb: number; + estimated_overage_tb: number; + estimated_overage_cost: number; + total_estimated_cost: number; + currency: string; + gross_monthly_tb?: number; + cdn_cache_hit_rate?: number; +} + // Workers AI Types (from worker-configuration.d.ts) export type WorkersAIModel = | "@cf/meta/llama-3.1-8b-instruct" diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts new file mode 100644 index 0000000..e38fa48 --- /dev/null +++ b/src/utils/formatters.ts @@ -0,0 +1,72 @@ +/** + * formatters.ts - 공통 포맷팅 유틸리티 + * + * 목적: 트래픽, 대역폭 정보를 일관되게 포맷팅 + */ + +import type { BandwidthInfo } from '../types'; + +/** + * 트래픽 값을 적절한 소수점 자리로 포맷팅 + * + * @param value - TB 단위 트래픽 값 + * @returns 포맷팅된 문자열 (예: "1.23TB", "12.3TB") + * + * 규칙: + * - 10TB 미만: 소수점 2자리 + * - 10TB 이상: 소수점 1자리 + */ +export function formatTB(value: number): string { + if (value < 10) { + return `${value.toFixed(2)}TB`; + } + return `${value.toFixed(1)}TB`; +} + +/** + * 대역폭 정보를 사용자 친화적 문자열로 포맷팅 + * + * @param bandwidth_info - 대역폭 정보 객체 + * @param currency - 통화 코드 ('KRW' 또는 'USD') + * @returns 포맷팅된 트래픽 정보 문자열 + * + * 포맷: + * - CDN 있음 + 초과: "예상 트래픽: 5.00TB (CDN 85% → 원본 0.75TB) → 초과 0.25TB (₩5,000)" + * - CDN 있음 + 포함: "예상 트래픽: 5.00TB (CDN 85% → 원본 0.75TB)" + * - CDN 없음 + 초과: "예상 트래픽: 1.50TB → 초과 0.50TB (₩10,000)" + * - CDN 없음 + 포함: "예상 트래픽: 0.80TB (포함 범위 내)" + */ +export function formatTrafficInfo(bandwidth_info: BandwidthInfo, currency: string): string { + // CDN 정보가 있는 경우 + if (bandwidth_info.gross_monthly_tb !== undefined && bandwidth_info.cdn_cache_hit_rate !== undefined) { + // CDN 캐시 히트율 범위 검증 (0-100%) + const hitRate = Math.round(Math.min(1, Math.max(0, bandwidth_info.cdn_cache_hit_rate)) * 100); + const grossTB = formatTB(bandwidth_info.gross_monthly_tb); + const estimatedTB = formatTB(bandwidth_info.estimated_monthly_tb); + + // 초과 여부 판단 (통일된 조건) + if (bandwidth_info.estimated_overage_tb > 0 && bandwidth_info.estimated_overage_cost > 0) { + const overageTB = formatTB(bandwidth_info.estimated_overage_tb); + const overageCost = currency === 'KRW' + ? `₩${Math.round(bandwidth_info.estimated_overage_cost).toLocaleString()}` + : `$${bandwidth_info.estimated_overage_cost.toFixed(2)}`; + return `예상 트래픽: ${grossTB} (CDN ${hitRate}% → 원본 ${estimatedTB}) → 초과 ${overageTB} (${overageCost})`; + } else { + return `예상 트래픽: ${grossTB} (CDN ${hitRate}% → 원본 ${estimatedTB})`; + } + } else { + // CDN 정보 없는 경우 + const estimatedTB = formatTB(bandwidth_info.estimated_monthly_tb); + + // 초과 여부 판단 (통일된 조건) + if (bandwidth_info.estimated_overage_tb > 0 && bandwidth_info.estimated_overage_cost > 0) { + const overageTB = formatTB(bandwidth_info.estimated_overage_tb); + const overageCost = currency === 'KRW' + ? `₩${Math.round(bandwidth_info.estimated_overage_cost).toLocaleString()}` + : `$${bandwidth_info.estimated_overage_cost.toFixed(2)}`; + return `예상 트래픽: ${estimatedTB} → 초과 ${overageTB} (${overageCost})`; + } else { + return `예상 트래픽: ${estimatedTB} (포함 범위 내)`; + } + } +}