diff --git a/app.js b/app.js index e18878f..32793fb 100644 --- a/app.js +++ b/app.js @@ -229,6 +229,139 @@ function formatPrice(price) { return '₩' + price.toLocaleString('ko-KR'); } +// ============================================================ +// 서버 추천 마법사 - 기술 스택 기반 추천 규칙 +// ============================================================ +const WIZARD_CONFIG = { + // 용도 목록 (객체 형태) + purposes: { + web: { icon: '🌐', name: '웹 서비스', desc: '웹사이트, API, SaaS' }, + game: { icon: '🎮', name: '게임 서버', desc: '마인크래프트, 발하임 등' }, + ai: { icon: '🤖', name: 'AI / ML', desc: '머신러닝, LLM, 데이터 분석' }, + dev: { icon: '💻', name: '개발 환경', desc: 'CI/CD, 테스트, 스테이징' }, + db: { icon: '🗄️', name: '데이터베이스', desc: 'MySQL, PostgreSQL, Redis' }, + other: { icon: '📦', name: '기타', desc: 'VPN, 프록시, 기타 용도' } + }, + + // 기술 스택 (용도별 필터링, category 추가) + stacks: { + web: [ + { id: 'wordpress', icon: '📝', name: 'WordPress', ram: 1024, cpu: 1, category: 'CMS' }, + { id: 'nginx', icon: '🟢', name: 'Nginx / Static', ram: 512, cpu: 1, category: 'Web Server' }, + { id: 'nodejs', icon: '🟩', name: 'Node.js', ram: 1024, cpu: 1, category: 'Runtime' }, + { id: 'python', icon: '🐍', name: 'Python / Django', ram: 1024, cpu: 1, category: 'Framework' }, + { id: 'php', icon: '🐘', name: 'PHP / Laravel', ram: 1024, cpu: 1, category: 'Framework' }, + { id: 'nextjs', icon: '▲', name: 'Next.js / React', ram: 2048, cpu: 2, category: 'Framework' }, + { id: 'docker', icon: '🐳', name: 'Docker 컨테이너', ram: 2048, cpu: 2, category: 'Container' } + ], + game: [ + { id: 'minecraft', icon: '⛏️', name: 'Minecraft', ram: 4096, cpu: 2, category: 'Sandbox' }, + { id: 'valheim', icon: '⚔️', name: 'Valheim', ram: 4096, cpu: 2, category: 'Survival' }, + { id: 'ark', icon: '🦖', name: 'ARK', ram: 8192, cpu: 4, category: 'Survival' }, + { id: 'palworld', icon: '🐾', name: 'Palworld', ram: 16384, cpu: 4, category: 'Survival' }, + { id: 'rust', icon: '🔫', name: 'Rust', ram: 8192, cpu: 4, category: 'Survival' }, + { id: 'terraria', icon: '🌲', name: 'Terraria', ram: 1024, cpu: 1, category: 'Sandbox' } + ], + ai: [ + { id: 'jupyter', icon: '📓', name: 'Jupyter Notebook', ram: 4096, cpu: 2, category: 'IDE' }, + { id: 'ollama', icon: '🦙', name: 'Ollama / LLM', ram: 16384, cpu: 4, category: 'LLM' }, + { id: 'stable', icon: '🎨', name: 'Stable Diffusion', ram: 16384, cpu: 4, gpu: true, category: 'Image AI' }, + { id: 'pytorch', icon: '🔥', name: 'PyTorch / TensorFlow', ram: 8192, cpu: 4, category: 'ML Framework' } + ], + dev: [ + { id: 'gitlab', icon: '🦊', name: 'GitLab', ram: 4096, cpu: 2, category: 'DevOps' }, + { id: 'jenkins', icon: '🔧', name: 'Jenkins', ram: 2048, cpu: 2, category: 'CI/CD' }, + { id: 'n8n', icon: '🔀', name: 'n8n 자동화', ram: 1024, cpu: 1, category: 'Automation' }, + { id: 'vscode', icon: '💠', name: 'VS Code Server', ram: 2048, cpu: 2, category: 'IDE' } + ], + db: [ + { id: 'mysql', icon: '🐬', name: 'MySQL / MariaDB', ram: 2048, cpu: 2, category: 'RDBMS' }, + { id: 'postgresql', icon: '🐘', name: 'PostgreSQL', ram: 2048, cpu: 2, category: 'RDBMS' }, + { id: 'mongodb', icon: '🍃', name: 'MongoDB', ram: 2048, cpu: 2, category: 'NoSQL' }, + { id: 'redis', icon: '🔴', name: 'Redis', ram: 1024, cpu: 1, category: 'Cache' } + ], + other: [ + { id: 'vpn', icon: '🔒', name: 'VPN / WireGuard', ram: 512, cpu: 1, category: 'Network' }, + { id: 'proxy', icon: '🌐', name: '프록시 서버', ram: 512, cpu: 1, category: 'Network' }, + { id: 'storage', icon: '💾', name: '파일 스토리지', ram: 1024, cpu: 1, category: 'Storage' }, + { id: 'custom', icon: '⚙️', name: '커스텀 설정', ram: 1024, cpu: 1, category: 'Custom' } + ] + }, + + // 규모 선택 (객체 형태) + scales: { + personal: { name: '개인', desc: '1-10명', users: '1-10명', multiplier: 1, icon: '👤' }, + small: { name: '소규모', desc: '10-100명', users: '10-100명', multiplier: 1.5, icon: '👥' }, + medium: { name: '중규모', desc: '100-1000명', users: '100-1,000명', multiplier: 2.5, icon: '🏢' }, + large: { name: '대규모', desc: '1000명+', users: '1,000명+', multiplier: 4, icon: '🏙️' } + } +}; + +/** + * 서버 추천 로직 (룰 기반) + * @param {string[]} selectedStacks - 선택된 스택 ID 목록 + * @param {string} scale - 규모 키 (personal, small, medium, large) + * @returns {Object} 추천 결과 { economy, recommended, performance } + */ +function calculateRecommendation(selectedStacks, scale) { + // 기본 요구사항 계산 + let totalRam = 0; + let totalCpu = 0; + let needsGpu = false; + + selectedStacks.forEach(stackId => { + // 모든 카테고리에서 스택 찾기 + for (const category of Object.values(WIZARD_CONFIG.stacks)) { + const stack = category.find(s => s.id === stackId); + if (stack) { + totalRam += stack.ram; + totalCpu += stack.cpu; + if (stack.gpu) needsGpu = true; + break; + } + } + }); + + // 규모에 따른 배율 적용 (이제 객체 형태) + const scaleConfig = WIZARD_CONFIG.scales[scale]; + const multiplier = scaleConfig?.multiplier || 1; + + totalRam = Math.ceil(totalRam * multiplier); + totalCpu = Math.ceil(totalCpu * multiplier); + + // 최소/최대 제한 + totalRam = Math.max(1024, Math.min(totalRam, 65536)); + totalCpu = Math.max(1, Math.min(totalCpu, 16)); + + // 가격 계산 (대략적인 추정, 실제로는 API 호출 필요) + const estimatePrice = (cpu, ram) => { + // 기본 가격: vCPU당 $5, GB당 $2 (월간) + return cpu * 5 + (ram / 1024) * 2; + }; + + // 추천 플랜 결정 (객체 형태로 반환) + return { + economy: { + cpu: Math.max(1, Math.floor(totalCpu * 0.7)), + ram: Math.max(1024, Math.floor(totalRam * 0.7)), + price: estimatePrice(Math.max(1, Math.floor(totalCpu * 0.7)), Math.max(1024, Math.floor(totalRam * 0.7))) + }, + recommended: { + cpu: totalCpu, + ram: totalRam, + price: estimatePrice(totalCpu, totalRam) + }, + performance: { + cpu: Math.min(16, Math.ceil(totalCpu * 1.5)), + ram: Math.min(65536, Math.ceil(totalRam * 1.5)), + price: estimatePrice(Math.min(16, Math.ceil(totalCpu * 1.5)), Math.min(65536, Math.ceil(totalRam * 1.5))) + }, + needsGpu, + totalStacks: selectedStacks.length, + scale + }; +} + /** * Alpine.js 메인 앱 데이터 - 대화형 위자드 */ @@ -240,6 +373,133 @@ function anvilApp() { wizardStep: 0, // 0: region, 1: plan, 2: os, 3: payment, 4: confirm, 5+: deploying deployStep: 0, + // ============================================================ + // 서버 추천 마법사 상태 + // ============================================================ + wizardOpen: false, + wizardCurrentStep: 0, // 0: purpose, 1: stack, 2: scale, 3: result + wizardPurpose: null, + wizardStacks: [], // 선택된 스택들 (다중 선택) + wizardScale: null, + wizardRecommendations: null, + wizardSelectedPlan: null, + + // 마법사 데이터 접근자 + get wizardPurposes() { return WIZARD_CONFIG.purposes; }, + get wizardScales() { return WIZARD_CONFIG.scales; }, + + // 현재 용도에 맞는 스택 목록 + get wizardAvailableStacks() { + if (!this.wizardPurpose) return []; + return WIZARD_CONFIG.stacks[this.wizardPurpose] || []; + }, + + // 마법사 열기 + openWizard() { + this.wizardOpen = true; + this.wizardCurrentStep = 0; + this.wizardPurpose = null; + this.wizardStacks = []; + this.wizardScale = null; + this.wizardRecommendations = null; + this.wizardSelectedPlan = null; + }, + + // 마법사 닫기 + closeWizard() { + this.wizardOpen = false; + }, + + // 용도 선택 + selectWizardPurpose(purposeId) { + this.wizardPurpose = purposeId; + this.wizardStacks = []; // 스택 초기화 + this.wizardCurrentStep = 1; + }, + + // 스택 토글 (다중 선택) + toggleWizardStack(stackId) { + const idx = this.wizardStacks.indexOf(stackId); + if (idx === -1) { + this.wizardStacks.push(stackId); + } else { + this.wizardStacks.splice(idx, 1); + } + }, + + // 스택 선택 완료 → 규모 선택으로 + confirmWizardStacks() { + if (this.wizardStacks.length === 0) { + alert('최소 1개의 기술 스택을 선택해주세요.'); + return; + } + this.wizardCurrentStep = 2; + }, + + // 규모 선택 → 추천 결과로 + selectWizardScale(scaleId) { + this.wizardScale = scaleId; + this.wizardRecommendations = calculateRecommendation(this.wizardStacks, scaleId); + this.wizardCurrentStep = 3; + }, + + // 이전 단계로 + wizardGoBack() { + if (this.wizardCurrentStep > 0) { + this.wizardCurrentStep--; + } + }, + + // 추천 플랜 선택 → 텔레그램으로 연결 + selectWizardPlan(tierKey) { + const plan = this.wizardRecommendations[tierKey]; + if (!plan) return; + + this.wizardSelectedPlan = plan; + + // 텔레그램 봇으로 연결 (선택한 사양 정보 포함) + const purposeName = WIZARD_CONFIG.purposes[this.wizardPurpose]?.name || this.wizardPurpose; + const scaleName = WIZARD_CONFIG.scales[this.wizardScale]?.name || this.wizardScale; + const stackNames = this.wizardStacks.map(id => this.getWizardStackName(id)).join(', '); + const tierLabels = { economy: 'Economy', recommended: 'Recommended', performance: 'Performance' }; + + window.open(`https://t.me/AnvilForgeBot?start=wizard_${tierKey}_${plan.cpu}c_${plan.ram}m`, '_blank'); + + this.closeWizard(); + }, + + // RAM 포맷팅 + formatWizardRam(mb) { + if (mb >= 1024) { + return (mb / 1024).toFixed(mb % 1024 === 0 ? 0 : 1) + ' GB'; + } + return mb + ' MB'; + }, + + // 현재 용도에 맞는 스택 그룹 반환 + getWizardStacksByPurpose() { + if (!this.wizardPurpose) return {}; + const purposeStacks = WIZARD_CONFIG.stacks[this.wizardPurpose] || []; + // 카테고리별로 그룹핑 + const grouped = {}; + for (const stack of purposeStacks) { + if (!grouped[stack.category]) { + grouped[stack.category] = []; + } + grouped[stack.category].push(stack); + } + return grouped; + }, + + // 스택 ID로 이름 찾기 + getWizardStackName(stackId) { + for (const purposeStacks of Object.values(WIZARD_CONFIG.stacks)) { + const stack = purposeStacks.find(s => s.id === stackId); + if (stack) return stack.name; + } + return stackId; + }, + // 대시보드 상태 dashboardMode: false, // true면 대시보드, false면 랜딩 currentView: 'servers', // 'servers' | 'stats' | 'notifications' @@ -937,19 +1197,19 @@ function pricingTable() { // 글로벌(Linode) 서브탭 (인스턴스 타입별) globalTypes: [ - { id: 'standard', name: 'Standard', desc: '공유 CPU, 저렴' }, - { id: 'dedicated', name: 'Dedicated', desc: '전용 CPU, 안정' }, - { id: 'premium', name: 'Premium', desc: 'AMD EPYC, 고성능' }, - { id: 'highmem', name: 'High Memory', desc: '대용량 RAM' }, + { id: 'standard', name: 'Standard', tooltip: '공유 CPU · 1-32 vCPU · 1-192GB RAM\n버스트 가능한 CPU로 비용 효율적\n웹서버, 개발환경에 적합' }, + { id: 'dedicated', name: 'Dedicated', tooltip: '전용 CPU · 4-64 vCPU · 8-512GB RAM\n100% CPU 자원 보장\nCI/CD, 게임서버, 고부하 작업에 적합' }, + { id: 'premium', name: 'Premium', tooltip: 'AMD EPYC 9004 · 1-64 vCPU · 2-512GB RAM\nDDR5 메모리 · NVMe 스토리지\n최신 세대 고성능 컴퓨팅' }, + { id: 'highmem', name: 'High Memory', tooltip: '고밀도 메모리 · 2-16 vCPU · 24-300GB RAM\nvCPU당 RAM 비율 최대화\n데이터베이스, 캐싱, 분석 워크로드' }, ], // 서울 서브탭 (인스턴스 타입별) seoulTypes: [ - { id: 'vc2', name: '가성비', desc: '일반 SSD' }, - { id: 'vhf', name: '고주파', desc: 'NVMe·3GHz+' }, - { id: 'vhp', name: '고성능', desc: 'AMD/Intel' }, - { id: 'voc', name: '최적화', desc: 'CPU/메모리/스토리지' }, - { id: 'vx1', name: '차세대', desc: '고밀도 메모리' }, + { id: 'vc2', name: 'Cloud Compute', tooltip: '일반 SSD · 1-24 vCPU · 1-96GB RAM\n가성비 좋은 범용 인스턴스\n웹호스팅, 소규모 앱에 적합' }, + { id: 'vhf', name: 'High Frequency', tooltip: 'NVMe SSD · 3GHz+ CPU · 1-12 vCPU\n고클럭 프로세서로 단일 스레드 성능 극대화\n게임서버, 실시간 처리에 적합' }, + { id: 'vhp', name: 'High Performance', tooltip: 'AMD EPYC · 1-16 vCPU · 1-64GB RAM\n전용 CPU 코어 · NVMe 스토리지\n고성능 컴퓨팅, ML 추론에 적합' }, + { id: 'voc', name: 'Optimized', tooltip: '특화 인스턴스 · 다양한 구성\nCPU/메모리/스토리지 최적화 버전\n특정 워크로드에 맞춤 선택' }, + { id: 'vx1', name: 'Extreme', tooltip: '초고밀도 · 4-24 vCPU · 96-384GB RAM\n고메모리 비율 인스턴스\n대규모 DB, 인메모리 캐시에 적합' }, ], // 데이터 상태 @@ -1157,8 +1417,12 @@ function pricingTable() { } case 'seoul': // 서울은 서브탭(selectedSeoulType)으로 필터링, GPU 제외 + // vhp는 AMD/Intel 중복 → AMD만 표시 const isSeoul = regionName.includes('seoul'); - return isSeoul && instId.startsWith(this.selectedSeoulType + '-') && !hasGpu; + if (!isSeoul || !instId.startsWith(this.selectedSeoulType + '-') || hasGpu) return false; + // vhp: AMD/Intel 중복 방지 → AMD만 표시 (Intel은 레거시) + if (this.selectedSeoulType === 'vhp' && instId.endsWith('-intel')) return false; + return true; case 'gpu-japan': // 일본 GPU (도쿄만, 중복 방지) return regionName.includes('tokyo 2') && hasGpu; @@ -1342,6 +1606,59 @@ function pricingTable() { if (diff < 60) return '방금 전'; if (diff < 3600) return `${Math.floor(diff / 60)}분 전`; return this.lastUpdate.toLocaleTimeString('ko-KR'); + }, + + // 인스턴스 선택 상태 + selectedInstance: null, + + // 모달 상태 + showInstanceModal: false, + selectedInstanceDetail: null, + + // 클립보드 복사 상태 + copiedToClipboard: false, + + // 인스턴스 선택 핸들러 + selectInstance(inst) { + // 같은 인스턴스 다시 클릭하면 선택 해제 및 모달 닫기 + if (this.selectedInstance?.id === inst.id && + this.selectedInstance?.region?.region_code === inst.region?.region_code) { + this.selectedInstance = null; + this.showInstanceModal = false; + this.selectedInstanceDetail = null; + return; + } + + this.selectedInstance = inst; + this.selectedInstanceDetail = inst; + this.showInstanceModal = true; + }, + + // 모달 닫기 + closeInstanceModal() { + this.showInstanceModal = false; + }, + + // 인스턴스 스펙 복사 + copyInstanceSpec() { + const inst = this.selectedInstanceDetail; + if (!inst) return; + + const spec = `vCPU: ${inst.vcpu}, RAM: ${this.formatMemory(inst.memory_mb)}, Storage: ${this.formatStorage(inst.storage_gb)}, 월 ${this.formatKrw(inst.pricing?.monthly_price_krw)}`; + + navigator.clipboard.writeText(spec).then(() => { + console.log('[PricingTable] Copied to clipboard:', spec); + this.copiedToClipboard = true; + setTimeout(() => { this.copiedToClipboard = false; }, 2000); + }).catch(err => { + console.error('[PricingTable] Failed to copy:', err); + }); + }, + + // 텔레그램 링크 가져오기 + getInstanceTelegramLink() { + if (!this.selectedInstanceDetail) return '#'; + return `https://t.me/AnvilForgeBot?start=order_${encodeURIComponent(this.selectedInstanceDetail.id)}`; } }; } diff --git a/index.html b/index.html index dd73129..717a6c1 100644 --- a/index.html +++ b/index.html @@ -213,9 +213,9 @@
- @@ -1253,6 +1253,220 @@
+ + +