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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
24
src/types.ts
24
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"
|
||||
|
||||
72
src/utils/formatters.ts
Normal file
72
src/utils/formatters.ts
Normal file
@@ -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} (포함 범위 내)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user