From 23abd0e64e47e41d8f5f2cfaa004db896837baa4 Mon Sep 17 00:00:00 2001 From: kappa Date: Mon, 26 Jan 2026 11:34:53 +0900 Subject: [PATCH] feat: add CDN cache hit rate for accurate bandwidth cost estimation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add cdn_enabled and cdn_cache_hit_rate API parameters - Use case별 기본 캐시 히트율 자동 적용 (video: 92%, blog: 90%, etc.) - 원본 서버 트래픽(origin_monthly_tb)과 절감 비용(cdn_savings_cost) 계산 - 응답에 CDN breakdown 필드 추가 (bandwidth_estimate, bandwidth_info) - 캐시 키에 CDN 옵션 포함하여 정확한 캐시 분리 - 4개 CDN 관련 테스트 추가 (총 59 tests) - CLAUDE.md 문서 업데이트 Cost impact example (10K video streaming): - Without CDN: $18,370 → With CDN 92%: $1,464 (92% savings) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 73 +++++++++++++++++++++++- src/__tests__/bandwidth.test.ts | 52 +++++++++++++++-- src/config.ts | 24 +++++--- src/handlers/recommend.ts | 16 +++++- src/types.ts | 17 +++++- src/utils/bandwidth.ts | 99 ++++++++++++++++++++++++++++----- src/utils/cache.ts | 8 +++ src/utils/index.ts | 2 + 8 files changed, 257 insertions(+), 34 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9e1a46c..d3192ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ src/ │ └── report.ts # GET /api/recommend/report └── __tests__/ ├── utils.test.ts # Validation & security tests (27 tests) - └── bandwidth.test.ts # Bandwidth estimation tests (28 tests) + └── bandwidth.test.ts # Bandwidth estimation tests (32 tests, including CDN) ``` ### Key Data Flow @@ -115,6 +115,44 @@ Estimates monthly bandwidth based on use_case patterns: Heavy bandwidth (>1TB/month) triggers warning about overage costs. +### CDN Cache Hit Rate (`bandwidth.ts`) + +CDN 사용 시 원본 서버 트래픽을 자동 계산하여 실제 비용을 추정합니다. + +**API Parameters**: +- `cdn_enabled`: CDN 사용 여부 (기본: `true`) +- `cdn_cache_hit_rate`: 캐시 히트율 override (0.0-1.0, 기본: use_case별 자동) + +**Use Case별 기본 캐시 히트율**: + +| Use Case | 캐시 히트율 | 설명 | +|----------|-------------|------| +| video/streaming | 92% | 동일 영상 반복 시청 | +| blog/static | 90% | 대부분 정적 콘텐츠 | +| file/download | 85% | 소프트웨어/에셋 다운로드 | +| ecommerce | 70% | 상품 이미지 캐시 가능 | +| forum | 60% | 정적/동적 혼합 | +| api/saas | 30% | 동적 응답 위주 | +| gaming | 20% | 실시간 통신 | +| chat | 10% | 실시간 메시지 | + +**응답 필드** (`bandwidth_estimate`): +- `cdn_enabled`: CDN 사용 여부 +- `cdn_cache_hit_rate`: 적용된 캐시 히트율 +- `gross_monthly_tb`: CDN 적용 전 총 트래픽 +- `origin_monthly_tb`: CDN 적용 후 원본 서버 트래픽 + +**응답 필드** (`bandwidth_info`): +- `cdn_savings_tb`: CDN으로 절감된 트래픽 (TB) +- `cdn_savings_cost`: CDN으로 절감된 비용 ($) + +**비용 절감 예시** (10,000 동시접속 video streaming): +``` +CDN 없음: 총 2,966TB → 원본 2,966TB → 초과비용 $18,370 +CDN 92%: 총 2,966TB → 원본 237TB → 초과비용 $1,464 +절감: 2,729TB 절감, $16,906 절감 (92%) +``` + ### Flexible Region Matching Both `/api/recommend` and `/api/servers` use `buildFlexibleRegionConditionsAnvil()` for anvil_regions: @@ -266,6 +304,15 @@ curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend - # Server list with filters (supports flexible region: korea, seoul, tokyo, etc.) curl -s "https://cloud-orchestrator.kappa-d8e.workers.dev/api/servers?region=korea&minCpu=4" | jq . +# CDN cache hit rate - default (use_case별 자동 적용) +curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"]}' | jq '.bandwidth_estimate | {cdn_enabled, cdn_cache_hit_rate, gross_monthly_tb, origin_monthly_tb}' + +# CDN disabled (원본 서버 전체 트래픽) +curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"],"cdn_enabled":false}' | jq '.bandwidth_estimate' + +# Custom CDN cache hit rate (80%) +curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nginx"],"expected_users":10000,"use_case":"video streaming","region_preference":["japan"],"cdn_cache_hit_rate":0.80}' | jq '.recommendations[0].bandwidth_info | {cdn_cache_hit_rate, cdn_savings_tb, cdn_savings_cost}' + # HTML Report (encode recommendation result as base64) # 1. Get recommendation and save to variable RESULT=$(curl -s -X POST https://cloud-orchestrator.kappa-d8e.workers.dev/api/recommend -H "Content-Type: application/json" -d '{"tech_stack":["nodejs"],"expected_users":500,"use_case":"simple api","lang":"ko"}') @@ -277,7 +324,29 @@ echo $REPORT_URL ## Recent Changes -### Major Architecture Refactoring (Latest) +### CDN Cache Hit Rate (Latest) + +**New Feature**: CDN 캐시 히트율 기반 원본 서버 트래픽 및 비용 계산 + +**API Parameters Added**: +- `cdn_enabled`: CDN 사용 여부 (기본: true) +- `cdn_cache_hit_rate`: 캐시 히트율 override (0.0-1.0) + +**Use Case별 기본 캐시 히트율**: +- video/streaming: 92%, blog/static: 90%, file: 85% +- ecommerce: 70%, forum: 60%, api: 30%, gaming: 20%, chat: 10% + +**Response Fields Added**: +- `bandwidth_estimate`: cdn_enabled, cdn_cache_hit_rate, gross_monthly_tb, origin_monthly_tb +- `bandwidth_info`: cdn_savings_tb, cdn_savings_cost + +**Cost Impact Example** (10K concurrent video streaming): +- Without CDN: $18,370 overage +- With CDN 92%: $1,464 overage (92% savings) + +**Tests Added**: 4 new CDN-specific tests (total 59 tests) + +### Major Architecture Refactoring **Security Hardening**: - Fixed XSS vulnerability in HTML reports with `escapeHtml()` diff --git a/src/__tests__/bandwidth.test.ts b/src/__tests__/bandwidth.test.ts index 6f10512..38824f3 100644 --- a/src/__tests__/bandwidth.test.ts +++ b/src/__tests__/bandwidth.test.ts @@ -3,19 +3,61 @@ import { estimateBandwidth } from '../utils'; describe('estimateBandwidth', () => { describe('Video streaming use cases', () => { - it('should estimate bandwidth for video streaming', () => { + it('should estimate bandwidth for video streaming with CDN', () => { const result = estimateBandwidth(100, 'video streaming platform'); - // Video streaming produces very_heavy bandwidth (>6TB/month) - expect(result.category).toBe('very_heavy'); - expect(result.monthly_tb).toBeGreaterThan(6); + // Video streaming with CDN (92% cache) reduces origin traffic significantly + expect(result.cdn_enabled).toBe(true); + expect(result.cdn_cache_hit_rate).toBe(0.92); + expect(result.gross_monthly_tb).toBeGreaterThan(6); // Gross traffic is very_heavy + expect(result.origin_monthly_tb).toBeLessThan(result.gross_monthly_tb); // Origin is reduced + expect(result.category).toBe('heavy'); // Origin traffic category expect(result.estimated_dau_min).toBeGreaterThan(0); expect(result.estimated_dau_max).toBeGreaterThan(result.estimated_dau_min); }); + it('should estimate very_heavy bandwidth without CDN', () => { + const result = estimateBandwidth(100, 'video streaming platform', undefined, { enabled: false }); + // Without CDN, video streaming is very_heavy + expect(result.cdn_enabled).toBe(false); + expect(result.category).toBe('very_heavy'); + expect(result.monthly_tb).toBeGreaterThan(6); + expect(result.gross_monthly_tb).toBe(result.origin_monthly_tb); // No CDN reduction + }); + it('should estimate higher bandwidth for 4K streaming', () => { const hd = estimateBandwidth(100, 'HD video streaming'); const fourK = estimateBandwidth(100, '4K UHD streaming'); - expect(fourK.monthly_tb).toBeGreaterThan(hd.monthly_tb); + expect(fourK.gross_monthly_tb).toBeGreaterThan(hd.gross_monthly_tb); + }); + }); + + describe('CDN cache hit rate', () => { + it('should apply use-case specific CDN cache hit rates', () => { + const video = estimateBandwidth(100, 'video streaming'); + const blog = estimateBandwidth(100, 'personal blog'); + const api = estimateBandwidth(100, 'REST API'); + const gaming = estimateBandwidth(100, 'game server'); + + expect(video.cdn_cache_hit_rate).toBe(0.92); // Video: high cache + expect(blog.cdn_cache_hit_rate).toBe(0.90); // Blog: high cache + expect(api.cdn_cache_hit_rate).toBe(0.30); // API: low cache + expect(gaming.cdn_cache_hit_rate).toBe(0.20); // Gaming: very low cache + }); + + it('should allow CDN cache hit rate override', () => { + const result = estimateBandwidth(100, 'video streaming', undefined, { cacheHitRate: 0.50 }); + expect(result.cdn_cache_hit_rate).toBe(0.50); + expect(result.origin_monthly_tb).toBeCloseTo(result.gross_monthly_tb * 0.50, 1); + }); + + it('should calculate CDN savings correctly', () => { + const withCdn = estimateBandwidth(100, 'video streaming'); + const withoutCdn = estimateBandwidth(100, 'video streaming', undefined, { enabled: false }); + + // CDN should reduce origin traffic significantly + expect(withCdn.origin_monthly_tb).toBeLessThan(withoutCdn.origin_monthly_tb); + // Gross should be the same + expect(withCdn.gross_monthly_tb).toBe(withoutCdn.gross_monthly_tb); }); }); diff --git a/src/config.ts b/src/config.ts index 02f9724..1b0256e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -24,49 +24,57 @@ export const USE_CASE_CONFIGS: UseCaseConfig[] = [ category: 'video', patterns: /video|stream|media|youtube|netflix|vod|동영상|스트리밍|미디어/i, dauMultiplier: { min: 8, max: 12 }, - activeRatio: 0.3 + activeRatio: 0.3, + cdnCacheHitRate: 0.92 // 높은 캐시 히트율 (동일 영상 반복 시청) }, { category: 'file', patterns: /download|file|storage|cdn|파일|다운로드|저장소/i, dauMultiplier: { min: 10, max: 14 }, - activeRatio: 0.5 + activeRatio: 0.5, + cdnCacheHitRate: 0.85 // 소프트웨어/에셋 다운로드 }, { category: 'gaming', patterns: /game|gaming|minecraft|게임/i, dauMultiplier: { min: 10, max: 20 }, - activeRatio: 0.5 + activeRatio: 0.5, + cdnCacheHitRate: 0.20 // 실시간 통신 위주, 캐시 어려움 }, { category: 'api', patterns: /api|saas|backend|서비스|백엔드/i, dauMultiplier: { min: 5, max: 10 }, - activeRatio: 0.6 + activeRatio: 0.6, + cdnCacheHitRate: 0.30 // 동적 응답 위주, 일부만 캐시 가능 }, { category: 'ecommerce', patterns: /e-?commerce|shop|store|쇼핑|커머스|온라인몰/i, dauMultiplier: { min: 20, max: 30 }, - activeRatio: 0.4 + activeRatio: 0.4, + cdnCacheHitRate: 0.70 // 상품 이미지 캐시 가능, 동적 페이지는 낮음 }, { category: 'forum', patterns: /forum|community|board|게시판|커뮤니티|포럼/i, dauMultiplier: { min: 15, max: 25 }, - activeRatio: 0.5 + activeRatio: 0.5, + cdnCacheHitRate: 0.60 // 정적/동적 콘텐츠 혼합 }, { category: 'blog', patterns: /blog|news|static|portfolio|블로그|뉴스|포트폴리오|landing/i, dauMultiplier: { min: 30, max: 50 }, - activeRatio: 0.3 + activeRatio: 0.3, + cdnCacheHitRate: 0.90 // 대부분 정적 콘텐츠 }, { category: 'chat', patterns: /chat|messaging|slack|discord|채팅|메신저/i, dauMultiplier: { min: 10, max: 14 }, - activeRatio: 0.7 + activeRatio: 0.7, + cdnCacheHitRate: 0.10 // 실시간 메시지, 거의 캐시 불가 } ]; diff --git a/src/handlers/recommend.ts b/src/handlers/recommend.ts index 4dbc639..7896ecf 100644 --- a/src/handlers/recommend.ts +++ b/src/handlers/recommend.ts @@ -245,9 +245,14 @@ export async function handleRecommend( console.log(`[Recommend] Bottleneck: '${bottleneckCategory}' with ${maxWeightedVcpu.toFixed(1)} weighted vCPU → ${minVcpu} vCPU (for ${body.expected_users} users)`); } - // Calculate bandwidth estimate for provider filtering - const bandwidthEstimate = estimateBandwidth(body.expected_users, body.use_case, body.traffic_pattern); - console.log(`[Recommend] Bandwidth estimate: ${bandwidthEstimate.monthly_tb >= 1 ? bandwidthEstimate.monthly_tb + ' TB' : bandwidthEstimate.monthly_gb + ' GB'}/month (${bandwidthEstimate.category})`); + // Calculate bandwidth estimate for provider filtering (with CDN options) + // cdn_enabled가 명시적으로 false면 비활성화, 그 외에는 기본 활성화 + const cdnOptions = { + enabled: body.cdn_enabled !== false, // undefined면 true (기본값), false면 false + cacheHitRate: body.cdn_cache_hit_rate + }; + const bandwidthEstimate = estimateBandwidth(body.expected_users, body.use_case, body.traffic_pattern, cdnOptions); + console.log(`[Recommend] Bandwidth estimate: Gross ${bandwidthEstimate.gross_monthly_tb}TB → Origin ${bandwidthEstimate.monthly_tb >= 1 ? bandwidthEstimate.monthly_tb + ' TB' : bandwidthEstimate.monthly_gb + ' GB'}/month (${bandwidthEstimate.category}, CDN: ${bandwidthEstimate.cdn_enabled ? `${(bandwidthEstimate.cdn_cache_hit_rate * 100).toFixed(0)}%` : 'disabled'})`); // Estimate specs for VPS benchmark query (doesn't need exact candidates) const estimatedCores = minVcpu || 2; @@ -358,6 +363,11 @@ export async function handleRecommend( description: bandwidthEstimate.description, active_ratio: bandwidthEstimate.active_ratio, calculation_note: `Based on ${body.expected_users} concurrent users with ${Math.round(bandwidthEstimate.active_ratio * 100)}% active ratio`, + // CDN breakdown + cdn_enabled: bandwidthEstimate.cdn_enabled, + cdn_cache_hit_rate: bandwidthEstimate.cdn_cache_hit_rate, + gross_monthly_tb: bandwidthEstimate.gross_monthly_tb, + origin_monthly_tb: bandwidthEstimate.origin_monthly_tb, }, total_candidates: candidates.length, cached: false, diff --git a/src/types.ts b/src/types.ts index 18c436b..0b489ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,9 @@ export interface RecommendRequest { region_preference?: string[]; budget_limit?: number; lang?: 'en' | 'zh' | 'ja' | 'ko'; // Response language + // CDN options + cdn_enabled?: boolean; // CDN 사용 여부 (기본: true - use_case별 자동 추정) + cdn_cache_hit_rate?: number; // 캐시 히트율 (0.0-1.0, 기본: use_case별 자동 추정) } export interface ExchangeRateCache { @@ -59,12 +62,18 @@ export interface BandwidthInfo { included_transfer_tb: number; // 기본 포함 트래픽 (TB/월) overage_cost_per_gb: number; // 초과 비용 ($/GB 또는 ₩/GB) overage_cost_per_tb: number; // 초과 비용 ($/TB 또는 ₩/TB) - estimated_monthly_tb: number; // 예상 월간 사용량 (TB) + estimated_monthly_tb: number; // 예상 월간 사용량 (TB) - CDN 적용 후 원본 서버 estimated_overage_tb: number; // 예상 초과량 (TB) estimated_overage_cost: number; // 예상 초과 비용 total_estimated_cost: number; // 총 예상 비용 (서버 + 트래픽) currency: 'USD' | 'KRW'; // 통화 warning?: string; // 트래픽 관련 경고 + // CDN breakdown + cdn_enabled?: boolean; // CDN 사용 여부 + cdn_cache_hit_rate?: number; // CDN 캐시 히트율 (0.0-1.0) + gross_monthly_tb?: number; // CDN 적용 전 총 트래픽 (TB) + cdn_savings_tb?: number; // CDN으로 절감된 트래픽 (TB) + cdn_savings_cost?: number; // CDN으로 절감된 비용 } export interface AvailableRegion { @@ -160,6 +169,11 @@ export interface BandwidthEstimate { estimated_dau_min: number; // Daily Active Users estimate (min) estimated_dau_max: number; // Daily Active Users estimate (max) active_ratio: number; // Active user ratio (0.0-1.0) + // CDN traffic breakdown + cdn_enabled: boolean; // CDN 사용 여부 + cdn_cache_hit_rate: number; // 캐시 히트율 (0.0-1.0) + gross_monthly_tb: number; // CDN 적용 전 총 트래픽 (TB) + origin_monthly_tb: number; // CDN 적용 후 원본 서버 트래픽 (TB) } // Use case configuration for bandwidth estimation and user metrics @@ -168,6 +182,7 @@ export interface UseCaseConfig { patterns: RegExp; dauMultiplier: { min: number; max: number }; activeRatio: number; + cdnCacheHitRate: number; // 기본 CDN 캐시 히트율 (0.0-1.0) } export interface AIRecommendationResponse { diff --git a/src/utils/bandwidth.ts b/src/utils/bandwidth.ts index 2b258a1..384299f 100644 --- a/src/utils/bandwidth.ts +++ b/src/utils/bandwidth.ts @@ -22,10 +22,19 @@ export function findUseCaseConfig(useCase: string): UseCaseConfig { category: 'default', patterns: /.*/, dauMultiplier: { min: 10, max: 14 }, - activeRatio: 0.5 + activeRatio: 0.5, + cdnCacheHitRate: 0.50 // 기본값: 50% }; } +/** + * Get CDN cache hit rate based on use case + * Returns default rate for the use case category + */ +export function getCdnCacheHitRate(useCase: string): number { + return findUseCaseConfig(useCase).cdnCacheHitRate; +} + /** * Get DAU multiplier based on use case (how many daily active users per concurrent user) */ @@ -40,10 +49,24 @@ export function getActiveUserRatio(useCase: string): number { return findUseCaseConfig(useCase).activeRatio; } +export interface CdnOptions { + enabled?: boolean; // CDN 사용 여부 (기본: true) + cacheHitRate?: number; // 캐시 히트율 override (0.0-1.0) +} + /** * Estimate monthly bandwidth based on concurrent users and use case + * @param concurrentUsers Expected concurrent users + * @param useCase Use case description + * @param trafficPattern Traffic pattern (steady, spiky, growing) + * @param cdnOptions CDN configuration options */ -export function estimateBandwidth(concurrentUsers: number, useCase: string, trafficPattern?: string): BandwidthEstimate { +export function estimateBandwidth( + concurrentUsers: number, + useCase: string, + trafficPattern?: string, + cdnOptions?: CdnOptions +): BandwidthEstimate { const useCaseLower = useCase.toLowerCase(); // Get use case configuration @@ -167,29 +190,54 @@ export function estimateBandwidth(concurrentUsers: number, useCase: string, traf } } + // CDN configuration + const cdnEnabled = cdnOptions?.enabled !== false; // 기본값: true + const cdnCacheHitRate = cdnOptions?.cacheHitRate ?? config.cdnCacheHitRate; + console.log(`[Bandwidth] Model: ${bandwidthModel}`); console.log(`[Bandwidth] DAU: ${dailyUniqueVisitors} (${dauMultiplier.min}-${dauMultiplier.max}x), Active: ${activeDau} (${(activeUserRatio * 100).toFixed(0)}%), Daily: ${dailyBandwidthGB.toFixed(1)} GB`); - // Monthly bandwidth - const monthlyGB = dailyBandwidthGB * 30; - const monthlyTB = monthlyGB / 1024; + // Monthly bandwidth (gross - before CDN) + const grossMonthlyGB = dailyBandwidthGB * 30; + const grossMonthlyTB = grossMonthlyGB / 1024; - // Categorize + // Origin bandwidth (after CDN cache hit) + const originMonthlyTB = cdnEnabled + ? grossMonthlyTB * (1 - cdnCacheHitRate) + : grossMonthlyTB; + const originMonthlyGB = originMonthlyTB * 1024; + + // Use origin traffic for categorization (actual server load) + const monthlyGB = originMonthlyGB; + const monthlyTB = originMonthlyTB; + + console.log(`[Bandwidth] CDN: ${cdnEnabled ? 'enabled' : 'disabled'}, Hit Rate: ${(cdnCacheHitRate * 100).toFixed(0)}%`); + console.log(`[Bandwidth] Gross: ${grossMonthlyTB.toFixed(1)} TB → Origin: ${originMonthlyTB.toFixed(1)} TB (${((1 - cdnCacheHitRate) * 100).toFixed(0)}%)`); + + // Categorize based on ORIGIN traffic (actual server load) let category: 'light' | 'moderate' | 'heavy' | 'very_heavy'; let description: string; if (monthlyTB < 0.5) { category = 'light'; - description = `~${Math.round(monthlyGB)} GB/month - Most VPS plans include sufficient bandwidth`; + description = cdnEnabled + ? `총 ${grossMonthlyTB.toFixed(1)}TB 중 원본 서버 ~${Math.round(monthlyGB)}GB/월 (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시)` + : `~${Math.round(monthlyGB)} GB/month - Most VPS plans include sufficient bandwidth`; } else if (monthlyTB < 2) { category = 'moderate'; - description = `~${monthlyTB.toFixed(1)} TB/month - Check provider bandwidth limits`; + description = cdnEnabled + ? `총 ${grossMonthlyTB.toFixed(1)}TB 중 원본 서버 ~${monthlyTB.toFixed(1)}TB/월 (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시)` + : `~${monthlyTB.toFixed(1)} TB/month - Check provider bandwidth limits`; } else if (monthlyTB < 6) { category = 'heavy'; - description = `~${monthlyTB.toFixed(1)} TB/month - Prefer providers with generous bandwidth (Linode: 1-6TB included)`; + description = cdnEnabled + ? `총 ${grossMonthlyTB.toFixed(1)}TB 중 원본 서버 ~${monthlyTB.toFixed(1)}TB/월 (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시) - 대역폭 여유 있는 플랜 권장` + : `~${monthlyTB.toFixed(1)} TB/month - Prefer providers with generous bandwidth (Linode: 1-6TB included)`; } else { category = 'very_heavy'; - description = `~${monthlyTB.toFixed(1)} TB/month - HIGH BANDWIDTH: Linode strongly recommended for cost savings`; + description = cdnEnabled + ? `총 ${grossMonthlyTB.toFixed(1)}TB 중 원본 서버 ~${monthlyTB.toFixed(1)}TB/월 (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시) - 고대역폭 플랜 필수` + : `~${monthlyTB.toFixed(1)} TB/month - HIGH BANDWIDTH: Linode strongly recommended for cost savings`; } return { @@ -200,7 +248,12 @@ export function estimateBandwidth(concurrentUsers: number, useCase: string, traf description, estimated_dau_min: estimatedDauMin, estimated_dau_max: estimatedDauMax, - active_ratio: activeUserRatio + active_ratio: activeUserRatio, + // CDN breakdown + cdn_enabled: cdnEnabled, + cdn_cache_hit_rate: cdnCacheHitRate, + gross_monthly_tb: Math.round(grossMonthlyTB * 10) / 10, + origin_monthly_tb: Math.round(originMonthlyTB * 10) / 10 }; } @@ -304,14 +357,24 @@ export function calculateBandwidthInfo( const overageCost = isKorean ? roundKrw100(overageCostUsd) : Math.round(overageCostUsd * 100) / 100; const totalCost = isKorean ? roundKrw100(totalCostUsd) : Math.round(totalCostUsd * 100) / 100; + // CDN savings calculation + const cdnEnabled = bandwidthEstimate.cdn_enabled; + const cdnCacheHitRate = bandwidthEstimate.cdn_cache_hit_rate; + const grossMonthlyTb = bandwidthEstimate.gross_monthly_tb; + const cdnSavingsTb = cdnEnabled ? grossMonthlyTb - estimatedTb : 0; + const cdnSavingsCostUsd = cdnSavingsTb * overagePerTbUsd; + const cdnSavingsCost = isKorean ? roundKrw100(cdnSavingsCostUsd) : Math.round(cdnSavingsCostUsd * 100) / 100; + let warning: string | undefined; if (overageTb > includedTb) { const costStr = isKorean ? `₩${overageCost.toLocaleString()}` : `$${overageCost.toFixed(0)}`; - warning = `⚠️ 예상 트래픽(${estimatedTb.toFixed(1)}TB)이 기본 포함량(${includedTb}TB)의 2배 이상입니다. 상위 플랜을 고려하세요.`; + warning = cdnEnabled + ? `⚠️ CDN 적용 후에도 원본 서버 트래픽(${estimatedTb.toFixed(1)}TB)이 기본 포함량(${includedTb}TB)의 2배 이상입니다. 상위 플랜을 고려하세요.` + : `⚠️ 예상 트래픽(${estimatedTb.toFixed(1)}TB)이 기본 포함량(${includedTb}TB)의 2배 이상입니다. 상위 플랜을 고려하세요.`; } else if (overageTb > 0) { const costStr = isKorean ? `₩${overageCost.toLocaleString()}` : `$${overageCost.toFixed(0)}`; - warning = isKorean - ? `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~${costStr}/월)` + warning = cdnEnabled + ? `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시 적용 후, 추가 비용 ~${costStr}/월)` : `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~${costStr}/월)`; } @@ -324,6 +387,12 @@ export function calculateBandwidthInfo( estimated_overage_cost: overageCost, total_estimated_cost: totalCost, currency, - warning + warning, + // CDN breakdown + cdn_enabled: cdnEnabled, + cdn_cache_hit_rate: cdnCacheHitRate, + gross_monthly_tb: Math.round(grossMonthlyTb * 10) / 10, + cdn_savings_tb: Math.round(cdnSavingsTb * 10) / 10, + cdn_savings_cost: cdnSavingsCost }; } diff --git a/src/utils/cache.ts b/src/utils/cache.ts index aa013eb..54e26db 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -69,6 +69,14 @@ export function generateCacheKey(req: RecommendRequest): string { parts.push(`lang:${req.lang}`); } + // Include CDN options in cache key + if (req.cdn_enabled !== undefined) { + parts.push(`cdn:${req.cdn_enabled}`); + } + if (req.cdn_cache_hit_rate !== undefined) { + parts.push(`cdnrate:${req.cdn_cache_hit_rate}`); + } + return `recommend:${parts.join('|')}`; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 046b8bd..e9a5bb1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -25,10 +25,12 @@ export { findUseCaseConfig, getDauMultiplier, getActiveUserRatio, + getCdnCacheHitRate, estimateBandwidth, getProviderBandwidthAllocation, calculateBandwidthInfo } from './bandwidth'; +export type { CdnOptions } from './bandwidth'; // Cache and rate limiting utilities export {