/** * Bandwidth estimation utility functions */ import type { BandwidthEstimate, BandwidthInfo, UseCaseConfig } from '../types'; import { USE_CASE_CONFIGS } from '../config'; /** * Find use case configuration by matching patterns */ export function findUseCaseConfig(useCase: string): UseCaseConfig { const useCaseLower = useCase.toLowerCase(); for (const config of USE_CASE_CONFIGS) { if (config.patterns.test(useCaseLower)) { return config; } } // Default configuration return { category: 'default', patterns: /.*/, dauMultiplier: { min: 10, max: 14 }, 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) */ export function getDauMultiplier(useCase: string): { min: number; max: number } { return findUseCaseConfig(useCase).dauMultiplier; } /** * Get active user ratio (what percentage of DAU actually performs the bandwidth-heavy action) */ 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, cdnOptions?: CdnOptions ): BandwidthEstimate { const useCaseLower = useCase.toLowerCase(); // Get use case configuration const config = findUseCaseConfig(useCase); const useCaseCategory = config.category; // Calculate DAU estimate from concurrent users with use-case-specific multipliers const dauMultiplier = config.dauMultiplier; const estimatedDauMin = Math.round(concurrentUsers * dauMultiplier.min); const estimatedDauMax = Math.round(concurrentUsers * dauMultiplier.max); const dailyUniqueVisitors = Math.round((estimatedDauMin + estimatedDauMax) / 2); const activeUserRatio = config.activeRatio; const activeDau = Math.round(dailyUniqueVisitors * activeUserRatio); // Traffic pattern adjustment let patternMultiplier = 1.0; if (trafficPattern === 'spiky') { patternMultiplier = 1.5; // Account for peak loads } else if (trafficPattern === 'growing') { patternMultiplier = 1.3; // Headroom for growth } let dailyBandwidthGB: number; let bandwidthModel: string; // ========== IMPROVED BANDWIDTH MODELS ========== // Each use case uses the most appropriate calculation method switch (useCaseCategory) { case 'video': { // VIDEO/STREAMING: Bitrate-based model const is4K = /4k|uhd|ultra/i.test(useCaseLower); const bitrateGBperHour = is4K ? 11.25 : 2.25; // 4K vs HD const avgWatchTimeHours = is4K ? 1.0 : 1.5; const gbPerActiveUser = bitrateGBperHour * avgWatchTimeHours; dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `bitrate-based: ${activeDau} active × ${bitrateGBperHour} GB/hr × ${avgWatchTimeHours}hr`; break; } case 'file': { // FILE DOWNLOAD: File-size based model const isLargeFiles = /iso|video|backup|대용량/.test(useCaseLower); const avgFileSizeGB = isLargeFiles ? 2.0 : 0.2; const downloadsPerUser = isLargeFiles ? 1 : 3; const gbPerActiveUser = avgFileSizeGB * downloadsPerUser; dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `file-based: ${activeDau} active × ${avgFileSizeGB} GB × ${downloadsPerUser} downloads`; break; } case 'gaming': { // GAMING: Session-duration based model const isMinecraft = /minecraft|마인크래프트/.test(useCaseLower); const mbPerHour = isMinecraft ? 150 : 80; const avgSessionHours = isMinecraft ? 3 : 2.5; const gbPerActiveUser = (mbPerHour * avgSessionHours) / 1024; dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `session-based: ${activeDau} active × ${mbPerHour} MB/hr × ${avgSessionHours}hr`; break; } case 'api': { // API/SAAS: Request-based model const avgRequestKB = 20; const requestsPerUserPerDay = 1000; const gbPerActiveUser = (avgRequestKB * requestsPerUserPerDay) / (1024 * 1024); dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `request-based: ${activeDau} active × ${avgRequestKB}KB × ${requestsPerUserPerDay} req`; break; } case 'ecommerce': { // E-COMMERCE: Page-based model (images heavy) const avgPageSizeMB = 2.5; const pagesPerSession = 20; const gbPerActiveUser = (avgPageSizeMB * pagesPerSession) / 1024; dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `page-based: ${activeDau} active × ${avgPageSizeMB}MB × ${pagesPerSession} pages`; break; } case 'forum': { // FORUM/COMMUNITY: Page-based model (text + some images) const avgPageSizeMB = 0.7; const pagesPerSession = 30; const gbPerActiveUser = (avgPageSizeMB * pagesPerSession) / 1024; dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `page-based: ${activeDau} active × ${avgPageSizeMB}MB × ${pagesPerSession} pages`; break; } case 'blog': { // STATIC/BLOG: Lightweight page-based model const avgPageSizeMB = 1.5; const pagesPerSession = 4; const gbPerActiveUser = (avgPageSizeMB * pagesPerSession) / 1024; dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `page-based: ${activeDau} active × ${avgPageSizeMB}MB × ${pagesPerSession} pages`; break; } case 'chat': { // CHAT/MESSAGING: Message-based model const textBandwidthMB = (3 * 200) / 1024; // 3KB × 200 messages const attachmentBandwidthMB = 20; // occasional images/files const gbPerActiveUser = (textBandwidthMB + attachmentBandwidthMB) / 1024; dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `message-based: ${activeDau} active × ~20MB/user (text+attachments)`; break; } default: { // DEFAULT: General web app (page-based) const avgPageSizeMB = 1.0; const pagesPerSession = 10; const gbPerActiveUser = (avgPageSizeMB * pagesPerSession) / 1024; dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier; bandwidthModel = `page-based (default): ${activeDau} active × ${avgPageSizeMB}MB × ${pagesPerSession} pages`; break; } } // 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 (gross - before CDN) const grossMonthlyGB = dailyBandwidthGB * 30; const grossMonthlyTB = grossMonthlyGB / 1024; // 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 = 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 = 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 = 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 = 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 { monthly_gb: Math.round(monthlyGB), monthly_tb: Math.round(monthlyTB * 10) / 10, daily_gb: Math.round(dailyBandwidthGB * 10) / 10, category, description, estimated_dau_min: estimatedDauMin, estimated_dau_max: estimatedDauMax, 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 }; } /** * Get provider bandwidth allocation based on memory size * Returns included transfer in TB/month */ export function getProviderBandwidthAllocation(providerName: string, memoryGb: number): { included_tb: number; overage_per_gb: number; overage_per_tb: number; } { const provider = providerName.toLowerCase(); if (provider.includes('linode')) { // Linode: roughly 1TB per 1GB RAM (Nanode 1GB = 1TB, 2GB = 2TB, etc.) const includedTb = Math.min(Math.max(memoryGb, 1), 20); return { included_tb: includedTb, overage_per_gb: 0.005, // $0.005/GB = $5/TB overage_per_tb: 5 }; } else if (provider.includes('vultr')) { // Vultr: varies by plan, roughly 1-2TB for small, up to 10TB for large let includedTb: number; if (memoryGb <= 2) includedTb = 1; else if (memoryGb <= 4) includedTb = 2; else if (memoryGb <= 8) includedTb = 3; else if (memoryGb <= 16) includedTb = 4; else if (memoryGb <= 32) includedTb = 5; else includedTb = Math.min(memoryGb / 4, 10); return { included_tb: includedTb, overage_per_gb: 0.01, // $0.01/GB = $10/TB overage_per_tb: 10 }; } else { // Default/Other providers: conservative estimate return { included_tb: Math.min(memoryGb, 5), overage_per_gb: 0.01, overage_per_tb: 10 }; } } /** * Calculate bandwidth cost info for a server * Uses actual DB values from anvil_transfer_pricing when available * @param server Server object * @param bandwidthEstimate Bandwidth estimate * @param lang Language code (ko = KRW, others = USD) * @param exchangeRate Exchange rate (USD to KRW) */ export function calculateBandwidthInfo( server: import('../types').Server, bandwidthEstimate: BandwidthEstimate, lang: string = 'en', exchangeRate: number = 1 ): BandwidthInfo { // Use actual DB values if available (Anvil servers), fallback to provider-based estimation let includedTb: number; let overagePerGbUsd: number; let overagePerTbUsd: number; if (server.transfer_tb !== null && server.transfer_price_per_gb !== null) { // Use actual values from anvil_instances + anvil_transfer_pricing includedTb = server.transfer_tb; overagePerGbUsd = server.transfer_price_per_gb; overagePerTbUsd = server.transfer_price_per_gb * 1024; } else { // Fallback to provider-based estimation for non-Anvil servers const allocation = getProviderBandwidthAllocation(server.provider_name, server.memory_gb); includedTb = allocation.included_tb; overagePerGbUsd = allocation.overage_per_gb; overagePerTbUsd = allocation.overage_per_tb; } const estimatedTb = bandwidthEstimate.monthly_tb; const overageTb = Math.max(0, estimatedTb - includedTb); const overageCostUsd = overageTb * overagePerTbUsd; // Get server price in USD for total calculation const serverPriceUsd = server.currency === 'KRW' ? server.monthly_price / exchangeRate : server.monthly_price; const totalCostUsd = serverPriceUsd + overageCostUsd; // Convert to KRW if Korean language, round to nearest 100 const isKorean = lang === 'ko'; const currency: 'USD' | 'KRW' = isKorean ? 'KRW' : 'USD'; // KRW 반올림: GB당 1원, TB당 500원, 총 비용 1원 const toKrw = (usd: number) => Math.round(usd * exchangeRate); // 1원 단위 const toKrw500 = (usd: number) => Math.round((usd * exchangeRate) / 500) * 500; // 500원 단위 const overagePerGb = isKorean ? toKrw(overagePerGbUsd) : overagePerGbUsd; const overagePerTb = isKorean ? toKrw500(overagePerTbUsd) : overagePerTbUsd; // 500원 단위 const overageCost = isKorean ? toKrw500(overageCostUsd) : overageCostUsd; // 500원 단위 // totalCost: 서버 가격(500원) + 초과 비용(500원) const totalCost = isKorean ? server.monthly_price + toKrw500(overageCostUsd) : totalCostUsd; // 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 ? toKrw500(cdnSavingsCostUsd) : cdnSavingsCostUsd; // 500원 단위 let warning: string | undefined; if (overageTb > includedTb) { const costStr = isKorean ? `₩${overageCost.toLocaleString()}` : `$${overageCost.toFixed(0)}`; 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 = cdnEnabled ? `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (CDN ${(cdnCacheHitRate * 100).toFixed(0)}% 캐시 적용 후, 추가 비용 ~${costStr}/월)` : `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~${costStr}/월)`; } return { included_transfer_tb: includedTb, overage_cost_per_gb: overagePerGb, // 반올림 없음 overage_cost_per_tb: overagePerTb, // 반올림 없음 estimated_monthly_tb: Math.round(estimatedTb * 10) / 10, estimated_overage_tb: Math.round(overageTb * 10) / 10, estimated_overage_cost: overageCost, // 반올림 없음 total_estimated_cost: totalCost, // 서버 가격(반올림) + 초과 비용(반올림 X) currency, 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 // 반올림 없음 }; }