/** * Anvil Hosting - Main Application JavaScript * 대화형 서버 런처 및 가격 데이터 관리 */ // 텔레그램 봇 URL 상수 const TELEGRAM_BOT_URL = 'https://t.me/AnvilForgeBot'; // 단일 가격 데이터 소스 (VAT 포함, 월간 기준) const PRICING_DATA = { global: [ { plan: 'Micro', vcpu: '1 Core', ram: '1 GB', ssd: '25 GB', transfer: '1 TB', price: 8500 }, { plan: 'Starter', vcpu: '1 Core', ram: '2 GB', ssd: '50 GB', transfer: '2 TB', price: 20400 }, { plan: 'Pro', vcpu: '2 Cores', ram: '4 GB', ssd: '80 GB', transfer: '4 TB', price: 40700, featured: true }, { plan: 'Business', vcpu: '4 Cores', ram: '8 GB', ssd: '160 GB', transfer: '5 TB', price: 67800 } ], seoul: [ { plan: 'Nano', vcpu: '1 Core', ram: '512 MB', ssd: '20 GB', transfer: '1 TB', price: 6000 }, { plan: 'Micro', vcpu: '1 Core', ram: '1 GB', ssd: '40 GB', transfer: '2 TB', price: 8500 }, { plan: 'Starter', vcpu: '1 Core', ram: '2 GB', ssd: '60 GB', transfer: '3 TB', price: 17000 }, { plan: 'Pro', vcpu: '2 Cores', ram: '4 GB', ssd: '80 GB', transfer: '4 TB', price: 33900, featured: true }, { plan: 'Business', vcpu: '2 Cores', ram: '8 GB', ssd: '160 GB', transfer: '5 TB', price: 67800 } ] }; // 런처 모달용 가격 - PRICING_DATA에서 파생 const LAUNCHER_PRICES = Object.fromEntries( PRICING_DATA.global.map(p => [p.plan, { base: p.price, seoul: PRICING_DATA.seoul.find(s => s.plan === p.plan)?.price || p.price }]) ); // 플랜별 스펙 정보 - PRICING_DATA에서 파생 const PLAN_SPECS = Object.fromEntries( PRICING_DATA.global.map(p => [p.plan, `${p.vcpu} / ${p.ram} RAM`]) ); // 리전 정보 const REGIONS = [ { id: 'Seoul', flag: '🇰🇷', name: '서울', ping: '2ms' }, { id: 'Tokyo', flag: '🇯🇵', name: '도쿄', ping: '35ms' }, { id: 'Singapore', flag: '🇸🇬', name: '싱가포르', ping: '65ms' }, { id: 'HongKong', flag: '🇭🇰', name: '홍콩', ping: '45ms' } ]; // OS 정보 const OS_LIST = [ { id: 'Debian 12', icon: '🐧', name: 'Debian 12' }, { id: 'Ubuntu 24.04', icon: '🟠', name: 'Ubuntu 24.04' }, { id: 'CentOS 9', icon: '🔵', name: 'CentOS 9' }, { id: 'Alpine', icon: '🏔️', name: 'Alpine' } ]; // 결제 방식 const PAYMENT_METHODS = [ { id: 'monthly', name: '월간 결제', desc: '매월 자동 결제', discount: 0 }, { id: 'yearly', name: '연간 결제', desc: '2개월 무료 (17% 할인)', discount: 17 } ]; // 배포 시뮬레이션 타이밍 상수 const DEPLOY_TIMING = { IMAGE_READY: 1500, CONTAINER_CREATED: 3000, NETWORK_CONFIGURED: 4500, COMPLETE: 6000 }; // Mock 데이터 (API 없이 UI 테스트용) const MOCK_SERVERS = [ { id: 'srv-001', name: 'production-web', region: '🇯🇵 Tokyo', ip: '45.12.89.101', os: 'Debian 12', plan: 'Pro', status: 'running', created_at: '2026-01-15', vcpu: '2 Cores', ram: '4 GB', cost: 40700 }, { id: 'srv-002', name: 'dev-api', region: '🇰🇷 Seoul', ip: '45.12.89.102', os: 'Ubuntu 24.04', plan: 'Starter', status: 'stopped', created_at: '2026-01-20', vcpu: '1 Core', ram: '2 GB', cost: 17000 } ]; const MOCK_STATS = { totalCost: 57700, totalServers: 2, runningServers: 1, costBreakdown: [ { plan: 'Pro', count: 1, cost: 40700 }, { plan: 'Starter', count: 1, cost: 17000 } ] }; const MOCK_NOTIFICATIONS = [ { id: 'n-001', type: 'info', title: '서버 생성 완료', message: 'production-web 서버가 Tokyo 리전에 생성되었습니다.', time: '10분 전', read: false }, { id: 'n-002', type: 'warning', title: '결제 예정', message: '이번 달 결제가 3일 후 예정되어 있습니다. (₩57,700)', time: '1시간 전', read: false }, { id: 'n-003', type: 'success', title: '시스템 업데이트 완료', message: 'dev-api 서버의 Ubuntu 패키지가 업데이트되었습니다.', time: '2일 전', read: true } ]; /** * 가격 포맷팅 (한국 원화) */ function formatPrice(price) { return '₩' + price.toLocaleString('ko-KR'); } /** * Alpine.js 메인 앱 데이터 - 대화형 위자드 */ function anvilApp() { return { // 모달 상태 launcherOpen: false, launching: false, wizardStep: 0, // 0: region, 1: plan, 2: os, 3: payment, 4: confirm, 5+: deploying deployStep: 0, // 대시보드 상태 dashboardMode: false, // true면 대시보드, false면 랜딩 currentView: 'servers', // 'servers' | 'stats' | 'notifications' // 텔레그램 연동 telegram: { isAvailable: false, // 텔레그램 환경인지 user: null, // 사용자 정보 initData: null // 검증용 데이터 }, // 서버 구성 config: { region: null, plan: null, os: null, payment: null, telegram_id: null }, // 서버 목록 servers: [], loadingServers: false, // 통계 stats: { totalCost: 0, totalServers: 0, runningServers: 0, costBreakdown: [] }, // 알림 notifications: [], unreadCount: 0, // API 상태 apiLoading: false, apiError: null, // 대화 메시지 messages: [], // 배포 로그 logs: [], // 데이터 접근자 regions: REGIONS, osList: OS_LIST, paymentMethods: PAYMENT_METHODS, // 플랜 목록 (리전에 따라 다름) get plans() { return ['Micro', 'Starter', 'Pro', 'Business']; }, // 초기화 (텔레그램 연동 + 대시보드) init() { if (window.Telegram?.WebApp) { const tg = window.Telegram.WebApp; tg.ready(); tg.expand(); this.telegram.isAvailable = true; this.telegram.user = tg.initDataUnsafe.user || null; this.telegram.initData = tg.initData; // 텔레그램 테마 색상 적용 (선택) // document.body.style.backgroundColor = tg.backgroundColor; console.log('[Telegram] Mini App initialized', { user: this.telegram.user, platform: tg.platform }); // 텔레그램 환경이면 대시보드 모드 활성화 // (user가 없어도 텔레그램에서 열린 경우 대시보드 표시) this.dashboardMode = true; this.loadDashboard(); console.log('[Telegram] Dashboard mode activated', { isAvailable: this.telegram.isAvailable, hasUser: !!this.telegram.user }); } else { console.log('[Telegram] Running in web browser mode'); } }, // 대시보드 초기 로드 async loadDashboard() { console.log('[Dashboard] Loading dashboard data...'); await Promise.all([ this.fetchServers(), this.fetchStats(), this.fetchNotifications() ]); }, // 서버 목록 조회 async fetchServers() { this.loadingServers = true; this.apiError = null; try { // TODO: 실제 API 호출로 교체 // const response = await fetch('/api/servers', { // headers: { 'X-Telegram-Init-Data': this.telegram.initData } // }); // this.servers = await response.json(); // Mock 데이터 사용 await new Promise(resolve => setTimeout(resolve, 500)); this.servers = [...MOCK_SERVERS]; console.log('[Dashboard] Servers loaded:', this.servers.length); } catch (error) { console.error('[Dashboard] Failed to fetch servers:', error); this.apiError = '서버 목록을 불러오는데 실패했습니다.'; } finally { this.loadingServers = false; } }, // 통계 조회 async fetchStats() { try { // TODO: 실제 API 호출로 교체 await new Promise(resolve => setTimeout(resolve, 300)); this.stats = { ...MOCK_STATS }; console.log('[Dashboard] Stats loaded:', this.stats); } catch (error) { console.error('[Dashboard] Failed to fetch stats:', error); } }, // 알림 조회 async fetchNotifications() { try { // TODO: 실제 API 호출로 교체 await new Promise(resolve => setTimeout(resolve, 400)); this.notifications = [...MOCK_NOTIFICATIONS]; this.unreadCount = this.notifications.filter(n => !n.read).length; console.log('[Dashboard] Notifications loaded:', this.notifications.length); } catch (error) { console.error('[Dashboard] Failed to fetch notifications:', error); } }, // 서버 시작 async startServer(serverId) { const server = this.servers.find(s => s.id === serverId); if (!server) return; console.log('[Dashboard] Starting server:', serverId); this.apiLoading = true; try { // TODO: 실제 API 호출로 교체 await new Promise(resolve => setTimeout(resolve, 1000)); server.status = 'running'; this.stats.runningServers++; console.log('[Dashboard] Server started:', serverId); } catch (error) { console.error('[Dashboard] Failed to start server:', error); alert('서버를 시작하는데 실패했습니다.'); } finally { this.apiLoading = false; } }, // 서버 중지 async stopServer(serverId) { const server = this.servers.find(s => s.id === serverId); if (!server) return; console.log('[Dashboard] Stopping server:', serverId); this.apiLoading = true; try { // TODO: 실제 API 호출로 교체 await new Promise(resolve => setTimeout(resolve, 1000)); server.status = 'stopped'; this.stats.runningServers--; console.log('[Dashboard] Server stopped:', serverId); } catch (error) { console.error('[Dashboard] Failed to stop server:', error); alert('서버를 중지하는데 실패했습니다.'); } finally { this.apiLoading = false; } }, // 서버 삭제 async deleteServer(serverId) { if (!confirm('정말로 이 서버를 삭제하시겠습니까?')) return; console.log('[Dashboard] Deleting server:', serverId); this.apiLoading = true; try { // TODO: 실제 API 호출로 교체 await new Promise(resolve => setTimeout(resolve, 800)); const serverIndex = this.servers.findIndex(s => s.id === serverId); if (serverIndex !== -1) { const deletedServer = this.servers[serverIndex]; this.servers.splice(serverIndex, 1); // 통계 업데이트 this.stats.totalServers--; this.stats.totalCost -= deletedServer.cost; if (deletedServer.status === 'running') { this.stats.runningServers--; } console.log('[Dashboard] Server deleted:', serverId); } } catch (error) { console.error('[Dashboard] Failed to delete server:', error); alert('서버를 삭제하는데 실패했습니다.'); } finally { this.apiLoading = false; } }, // 모든 알림 읽음 처리 markAllRead() { this.notifications.forEach(n => n.read = true); this.unreadCount = 0; console.log('[Dashboard] All notifications marked as read'); }, // 뷰 전환 switchView(view) { this.currentView = view; console.log('[Dashboard] View switched to:', view); }, // 가격 조회 getPrice(plan) { const prices = LAUNCHER_PRICES[plan]; if (!prices) return '0'; const price = this.config.region === 'Seoul' ? prices.seoul : prices.base; return price.toLocaleString('ko-KR'); }, // 월간 가격 (할인 적용) getFinalPrice() { const prices = LAUNCHER_PRICES[this.config.plan]; if (!prices) return 0; let price = this.config.region === 'Seoul' ? prices.seoul : prices.base; if (this.config.payment === 'yearly') { price = Math.round(price * 0.83); // 17% 할인 } return price; }, // 플랜 스펙 조회 getPlanSpec(plan) { return PLAN_SPECS[plan] || ''; }, // 리전 선택 selectRegion(region) { this.config.region = region.id; this.addMessage('user', `${region.flag} ${region.name}`); this.wizardStep = 1; setTimeout(() => this.addMessage('bot', '어떤 사양이 필요하신가요?'), 300); }, // 플랜 선택 selectPlan(plan) { this.config.plan = plan; this.addMessage('user', `${plan} (${this.getPlanSpec(plan)})`); this.wizardStep = 2; setTimeout(() => this.addMessage('bot', '어떤 운영체제를 설치할까요?'), 300); }, // OS 선택 selectOS(os) { this.config.os = os.id; this.addMessage('user', `${os.icon} ${os.name}`); this.wizardStep = 3; setTimeout(() => this.addMessage('bot', '결제 방식을 선택해주세요.'), 300); }, // 결제 방식 선택 selectPayment(payment) { this.config.payment = payment.id; this.addMessage('user', payment.name); this.wizardStep = 4; setTimeout(() => this.addMessage('bot', '구성을 확인하고 서버를 생성해주세요! 🚀'), 300); }, // 메시지 추가 addMessage(type, text) { this.messages.push({ type, text, time: new Date() }); // 스크롤 하단으로 this.$nextTick(() => { const container = document.getElementById('chat-container'); if (container) container.scrollTop = container.scrollHeight; }); }, // 이전 단계로 goBack() { if (this.wizardStep > 0 && !this.launching) { // 마지막 2개 메시지 제거 (사용자 선택 + 봇 질문) this.messages.pop(); this.messages.pop(); this.wizardStep--; // 선택 초기화 if (this.wizardStep === 0) this.config.region = null; else if (this.wizardStep === 1) this.config.plan = null; else if (this.wizardStep === 2) this.config.os = null; else if (this.wizardStep === 3) this.config.payment = null; } }, // 서버 배포 시작 startLaunch() { // 텔레그램 사용자 ID 추가 if (this.telegram.user) { this.config.telegram_id = this.telegram.user.id; } this.launching = true; this.wizardStep = 5; this.deployStep = 1; this.logs = [ '🔄 배포를 시작합니다...', `📍 ${this.config.region} 리전 노드 선택 중...`, `📦 ${this.config.os} 이미지 준비 중...` ]; // 디버그: config 출력 console.log('[Server Launch] Config:', this.config); setTimeout(() => { this.logs.push('✅ 이미지 준비 완료'); this.logs.push('🔧 컨테이너 인스턴스 생성 중...'); this.deployStep = 2; }, DEPLOY_TIMING.IMAGE_READY); setTimeout(() => { this.logs.push('✅ 컨테이너 생성 완료'); this.logs.push('🌐 네트워크 및 방화벽 구성 중...'); this.deployStep = 3; }, DEPLOY_TIMING.CONTAINER_CREATED); setTimeout(() => { const randomIP = Math.floor(Math.random() * 254 + 1); this.logs.push(`✅ IP 할당 완료: 45.12.89.${randomIP}`); this.logs.push('⚙️ 시스템 서비스 시작 중...'); this.deployStep = 4; }, DEPLOY_TIMING.NETWORK_CONFIGURED); setTimeout(() => { this.launching = false; this.deployStep = 5; this.logs.push('🎉 서버가 활성화되었습니다!'); // 대시보드 모드에서는 서버 목록 새로고침 if (this.dashboardMode) { setTimeout(() => { this.fetchServers(); this.fetchStats(); }, 1000); } }, DEPLOY_TIMING.COMPLETE); }, // 런처 열기 (대시보드용 추가 기능) openLauncher() { this.launcherOpen = true; this.messages = [{ type: 'bot', text: '어느 리전에 서버를 생성할까요?', time: new Date() }]; // 대시보드 모드에서는 서버 생성 후 목록 새로고침 if (this.dashboardMode) { console.log('[Dashboard] Launcher opened from dashboard'); } }, // 런처 초기화 resetLauncher() { this.launcherOpen = false; this.launching = false; this.wizardStep = 0; this.deployStep = 0; this.config = { region: null, plan: null, os: null, payment: null, telegram_id: null }; this.messages = []; this.logs = []; }, // ESC 키로 모달 닫기 handleKeydown(event) { if (event.key === 'Escape' && this.launcherOpen && !this.launching) { this.resetLauncher(); } } }; } /** * 가격표 컴포넌트 */ function pricingTable() { return { region: 'global', get plans() { return PRICING_DATA[this.region] || []; }, formatPrice(price) { return formatPrice(price); }, isSeoul() { return this.region === 'seoul'; } }; } /** * 탭 전환 (n8n/Terraform) */ function switchTab(tab) { const btnN8n = document.getElementById('btn-n8n'); const btnTf = document.getElementById('btn-tf'); const panelN8n = document.getElementById('panel-n8n'); const panelTf = document.getElementById('panel-tf'); // Null checks for DOM elements if (!btnN8n || !btnTf || !panelN8n || !panelTf) return; if (tab === 'n8n') { // Update ARIA states for n8n tab btnN8n.setAttribute('aria-selected', 'true'); btnN8n.setAttribute('tabindex', '0'); btnTf.setAttribute('aria-selected', 'false'); btnTf.setAttribute('tabindex', '-1'); // Update classes btnN8n.className = 'px-4 py-2 rounded-lg bg-purple-600 text-white text-sm font-bold transition shadow-lg shadow-purple-500/20'; btnTf.className = 'px-4 py-2 rounded-lg bg-slate-800 text-slate-400 text-sm font-bold border border-slate-700 hover:text-white transition'; panelN8n.classList.remove('hidden'); panelTf.classList.add('hidden'); } else { // Update ARIA states for Terraform tab btnTf.setAttribute('aria-selected', 'true'); btnTf.setAttribute('tabindex', '0'); btnN8n.setAttribute('aria-selected', 'false'); btnN8n.setAttribute('tabindex', '-1'); // Update classes btnN8n.className = 'px-4 py-2 rounded-lg bg-slate-800 text-slate-400 text-sm font-bold border border-slate-700 hover:text-white transition'; btnTf.className = 'px-4 py-2 rounded-lg bg-blue-600 text-white text-sm font-bold transition shadow-lg shadow-blue-500/20'; panelN8n.classList.add('hidden'); panelTf.classList.remove('hidden'); } } /** * 실시간 Ping 시뮬레이션 */ function updatePing() { const regions = [ { id: 'ping-kr', base: 2, variance: 2 }, { id: 'ping-jp', base: 35, variance: 5 }, { id: 'ping-hk', base: 45, variance: 8 }, { id: 'ping-sg', base: 65, variance: 10 } ]; regions.forEach(region => { const el = document.getElementById(region.id); if (el) { const jitter = Math.floor(Math.random() * region.variance) - (region.variance / 2); let val = Math.max(1, Math.floor(region.base + jitter)); el.innerText = val; } }); } // Ping 업데이트 시작 (visibility-aware) let pingInterval; function startPingUpdates() { if (!pingInterval) { pingInterval = setInterval(updatePing, 2000); } } function stopPingUpdates() { if (pingInterval) { clearInterval(pingInterval); pingInterval = null; } } // Visibility change handler document.addEventListener('visibilitychange', () => { if (document.hidden) { stopPingUpdates(); } else { startPingUpdates(); } }); // Initial start startPingUpdates(); // Tab switching with event listeners and keyboard navigation document.addEventListener('DOMContentLoaded', () => { const btnN8n = document.getElementById('btn-n8n'); const btnTf = document.getElementById('btn-tf'); if (btnN8n && btnTf) { // Click event listeners btnN8n.addEventListener('click', () => switchTab('n8n')); btnTf.addEventListener('click', () => switchTab('tf')); // Keyboard navigation (Arrow keys) [btnN8n, btnTf].forEach(btn => { btn.addEventListener('keydown', (e) => { const currentTab = btn.getAttribute('data-tab'); if (e.key === 'ArrowRight') { e.preventDefault(); if (currentTab === 'n8n') { btnTf.focus(); switchTab('tf'); } } else if (e.key === 'ArrowLeft') { e.preventDefault(); if (currentTab === 'tf') { btnN8n.focus(); switchTab('n8n'); } } }); }); } });