Refactor: Extract JS to app.js and add footer legal links

- Extract inline Alpine.js code to separate app.js file
- Unify pricing data in single PRICING_DATA source
- Convert static pricing tables to dynamic Alpine.js templates
- Add footer links for Terms, Privacy Policy, SLA (Telegram bot deep links)
- Add ESC key handler for modal close
- Add aria-label to pricing table for accessibility
- Reduce index.html from 1023 to 862 lines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-20 20:11:42 +09:00
parent a613a07eb5
commit 1d052deb8d
2 changed files with 494 additions and 215 deletions

197
app.js Normal file
View File

@@ -0,0 +1,197 @@
/**
* Anvil Hosting - Main Application JavaScript
* 가격 데이터 및 Alpine.js 컴포넌트 정의
*/
// 단일 가격 데이터 소스 (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 }
]
};
// 런처 모달용 가격 (plan명 기준)
const LAUNCHER_PRICES = {
'Micro': { base: 8500, seoul: 8500 },
'Starter': { base: 20400, seoul: 17000 },
'Pro': { base: 40700, seoul: 33900 },
'Business': { base: 67800, seoul: 67800 }
};
// 플랜별 스펙 정보
const PLAN_SPECS = {
'Micro': '1vCPU / 1GB RAM',
'Starter': '1vCPU / 2GB RAM',
'Pro': '2vCPU / 4GB RAM',
'Business': '4vCPU / 8GB RAM'
};
/**
* 가격 포맷팅 (한국 원화)
*/
function formatPrice(price) {
return '₩' + price.toLocaleString('ko-KR');
}
/**
* Alpine.js 메인 앱 데이터
*/
function anvilApp() {
return {
// 모달 상태
launcherOpen: false,
launching: false,
step: 0,
// 서버 구성
config: {
region: 'Tokyo',
os: 'Debian 12',
plan: 'Pro'
},
// 배포 로그
logs: [],
// 가격 조회
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');
},
// 플랜 스펙 조회
getPlanSpec(plan) {
return PLAN_SPECS[plan] || '';
},
// 서버 배포 시작
startLaunch() {
this.launching = true;
this.step = 1;
this.logs = [
'[INFO] Initializing deployment...',
'[INFO] Selecting node in ' + this.config.region + '...',
'[INFO] Pulling ' + this.config.os + ' image...'
];
setTimeout(() => {
this.logs.push('[SUCCESS] Image pulled.');
this.logs.push('[INFO] Creating container instance...');
this.step = 2;
}, 1500);
setTimeout(() => {
this.logs.push('[SUCCESS] Container created.');
this.logs.push('[INFO] Configuring network & firewall...');
this.step = 3;
}, 3000);
setTimeout(() => {
const randomIP = Math.floor(Math.random() * 254 + 1);
this.logs.push('[SUCCESS] Network ready. IP: 45.12.89.' + randomIP);
this.logs.push('[INFO] Starting system services...');
this.step = 4;
}, 4500);
setTimeout(() => {
this.launching = false;
this.step = 5;
this.logs.push('[COMPLETE] Server is now live!');
}, 6000);
},
// 런처 초기화
resetLauncher() {
this.launcherOpen = false;
this.launching = false;
this.step = 0;
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');
if (tab === 'n8n') {
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 {
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 업데이트 시작
setInterval(updatePing, 2000);

View File

@@ -4,75 +4,47 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anvil Hosting - 개발자를 위한 컨테이너 클라우드</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- SEO Meta Tags -->
<meta name="description" content="Incus/LXD 기반 초경량 컨테이너 호스팅. VM 오버헤드 없이 네이티브 성능을 제공하며, n8n, Ansible 자동화 파이프라인과 즉시 연동됩니다. 도쿄, 서울, 싱가포르, 홍콩 리전 지원.">
<meta name="keywords" content="컨테이너 호스팅, LXD, Incus, 클라우드 서버, VPS, 도쿄 서버, 서울 서버, 개발자 호스팅, Docker, 자동화">
<meta name="author" content="Anvil Hosting">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://hosting.anvil.it.com">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://hosting.anvil.it.com">
<meta property="og:title" content="Anvil Hosting - 개발자를 위한 컨테이너 클라우드">
<meta property="og:description" content="Incus/LXD 기반 초경량 컨테이너 호스팅. 네이티브 성능과 자동화 파이프라인을 경험하세요.">
<meta property="og:image" content="https://hosting.anvil.it.com/og-image.png">
<meta property="og:locale" content="ko_KR">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="https://hosting.anvil.it.com">
<meta name="twitter:title" content="Anvil Hosting - 개발자를 위한 컨테이너 클라우드">
<meta name="twitter:description" content="Incus/LXD 기반 초경량 컨테이너 호스팅. 네이티브 성능과 자동화 파이프라인을 경험하세요.">
<meta name="twitter:image" content="https://hosting.anvil.it.com/og-image.png">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect fill='%230ea5e9' rx='20' width='100' height='100'/><text x='50' y='70' font-size='60' text-anchor='middle' fill='white' font-family='sans-serif' font-weight='bold'>A</text></svg>">
<!-- Tailwind CSS (Production Build) -->
<link rel="stylesheet" href="style.css">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Pretendard:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Pretendard', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
colors: {
brand: {
400: '#38bdf8', // Sky blue
500: '#0ea5e9',
600: '#0284c7',
900: '#0c4a6e',
},
dark: {
900: '#0b1120', // Almost black
800: '#1e293b',
700: '#334155',
}
},
animation: {
'float': 'float 6s ease-in-out infinite',
'typewriter': 'typewriter 2s steps(40) 1s 1 normal both',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-20px)' },
}
}
},
},
}
</script>
<style>
body { background-color: #0b1120; color: #e2e8f0; }
.glass-panel {
background: rgba(30, 41, 59, 0.4);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.grid-bg {
background-image: linear-gradient(rgba(56, 189, 248, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(56, 189, 248, 0.05) 1px, transparent 1px);
background-size: 30px 30px;
}
/* Chat Animation */
.chat-bubble {
opacity: 0;
transform: translateY(10px);
animation: chat-enter 0.5s forwards;
}
@keyframes chat-enter {
to { opacity: 1; transform: translateY(0); }
}
.delay-1 { animation-delay: 0.5s; }
.delay-2 { animation-delay: 1.5s; }
.delay-3 { animation-delay: 2.5s; }
.delay-4 { animation-delay: 3.5s; }
</style>
<!-- App JavaScript (must load before Alpine.js) -->
<script src="app.js"></script>
<!-- Alpine.js (pinned version with SRI) -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js" integrity="sha384-iZD2X8o1Zdq0HR5H/7oa8W30WS4No+zWCKUPD7fHRay9I1Gf+C4F8sVmw7zec1wW" crossorigin="anonymous"></script>
</head>
<body class="font-sans antialiased selection:bg-brand-500/30">
<body x-data="anvilApp()" @keydown.window="handleKeydown($event)" class="font-sans antialiased selection:bg-brand-500/30">
<!-- Navbar -->
<nav class="fixed w-full z-50 top-0 border-b border-white/5 bg-dark-900/80 backdrop-blur-md">
@@ -88,7 +60,7 @@
<a href="#automation" class="hover:text-white transition">자동화</a>
</div>
</div>
<a href="https://t.me/AnvilForgeBot" target="_blank" class="px-4 py-2 bg-brand-600 hover:bg-brand-500 text-white text-sm font-bold rounded-lg transition shadow-lg shadow-brand-500/20 flex items-center gap-2">
<a href="https://t.me/AnvilForgeBot" target="_blank" rel="noopener noreferrer" aria-label="텔레그램 봇으로 콘솔 시작" class="px-4 py-2 bg-brand-600 hover:bg-brand-500 text-white text-sm font-bold rounded-lg transition shadow-lg shadow-brand-500/20 flex items-center gap-2">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.62-.2-1.12-.31-1.15-.63.03-.37.59-.75 1.5-.95 6.07-2.64 10.12-4.38 12.15-5.21 2.91-1.2 3.51-1.4 3.91-1.41.09 0 .28.02.41.09.11.06.23.14.3.24.08.12.12.33.09.57z"/></svg>
Console 시작
</a>
@@ -117,13 +89,12 @@
</p>
<div class="flex flex-col sm:flex-row gap-4">
<div class="relative group">
<div class="absolute -inset-0.5 bg-gradient-to-r from-brand-400 to-purple-600 rounded-lg blur opacity-50 group-hover:opacity-100 transition duration-200"></div>
<a href="#pricing" class="relative w-full sm:w-auto px-8 py-4 bg-white text-dark-900 font-bold rounded-lg flex items-center justify-center gap-3 transition-transform group-hover:-translate-y-0.5">
<div class="relative">
<button @click="launcherOpen = true" :aria-expanded="launcherOpen" aria-haspopup="dialog" aria-label="서버 런처 열기" class="relative w-full sm:w-auto px-8 py-4 bg-white/95 text-dark-900 font-bold rounded-lg flex items-center justify-center gap-3 transition-all hover:ring-4 hover:ring-brand-500/30 hover:shadow-[0_0_20px_rgba(56,189,248,0.4)] active:scale-95 group">
<span class="text-xl">🚀</span>
<span>인스턴스 즉시 배포</span>
<svg class="w-4 h-4 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
</a>
<svg class="w-4 h-4 text-brand-600 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
</button>
</div>
<div id="ping-widget" class="flex flex-col gap-1.5 text-xs font-mono text-slate-400 bg-slate-900/50 p-3 rounded-lg border border-slate-700/50 backdrop-blur-sm mt-8 sm:mt-0 sm:ml-4 max-w-sm">
<div class="flex items-center gap-3">
@@ -370,26 +341,6 @@
</div>
</div>
<script>
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');
if (tab === 'n8n') {
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 {
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');
}
}
</script>
</div>
</section>
@@ -538,7 +489,7 @@
</div>
<!-- Pricing Tabs -->
<div x-data="{ region: 'global' }" class="glass-panel p-8 rounded-2xl border border-slate-700 mb-12">
<div x-data="pricingTable()" class="glass-panel p-8 rounded-2xl border border-slate-700 mb-12">
<!-- Tab Buttons -->
<div class="flex justify-center mb-8">
<div class="bg-slate-900/80 p-1 rounded-xl inline-flex border border-slate-700">
@@ -553,126 +504,54 @@
</div>
</div>
<!-- Global Pricing Table -->
<div x-show="region === 'global'" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform scale-95" x-transition:enter-end="opacity-100 transform scale-100">
<!-- Dynamic Pricing Table -->
<div x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform scale-95" x-transition:enter-end="opacity-100 transform scale-100">
<div class="overflow-x-auto">
<table class="w-full text-left font-sans">
<table class="w-full text-left font-sans" aria-label="서버 요금표">
<thead>
<tr class="text-slate-500 text-sm border-b border-slate-700">
<th class="pb-4 font-medium pl-4">Plan</th>
<th class="pb-4 font-medium text-center">vCPU</th>
<th class="pb-4 font-medium text-center">RAM</th>
<th class="pb-4 font-medium text-center">SSD</th>
<th class="pb-4 font-medium text-center" :class="isSeoul() ? 'text-green-400' : ''">
<span x-text="isSeoul() ? 'SSD (High)' : 'SSD'"></span>
</th>
<th class="pb-4 font-medium text-center">Transfer</th>
<th class="pb-4 font-medium text-right pr-4">Price / Month</th>
</tr>
</thead>
<tbody class="text-slate-300 text-sm">
<tr class="border-b border-slate-700/50 hover:bg-slate-800/50 transition">
<td class="py-4 pl-4 font-bold text-white">Micro</td>
<td class="text-center text-brand-400">1 Core</td>
<td class="text-center">1 GB</td>
<td class="text-center">25 GB</td>
<td class="text-center">1 TB</td>
<td class="text-right pr-4 font-bold text-lg">₩8,500</td>
</tr>
<tr class="border-b border-slate-700/50 hover:bg-slate-800/50 transition">
<td class="py-4 pl-4 font-bold text-white">Starter</td>
<td class="text-center text-brand-400">1 Core</td>
<td class="text-center">2 GB</td>
<td class="text-center">50 GB</td>
<td class="text-center">2 TB</td>
<td class="text-right pr-4 font-bold text-lg">₩20,400</td>
</tr>
<tr class="border-b border-slate-700/50 bg-brand-500/5 hover:bg-brand-500/10 transition border-l-4 border-l-brand-500">
<td class="py-4 pl-3 font-bold text-brand-400">Pro ⭐</td>
<td class="text-center text-brand-400 font-bold">2 Cores</td>
<td class="text-center font-bold">4 GB</td>
<td class="text-center">80 GB</td>
<td class="text-center">4 TB</td>
<td class="text-right pr-4 font-bold text-lg text-brand-400">₩40,700</td>
</tr>
<tr class="hover:bg-slate-800/50 transition">
<td class="py-4 pl-4 font-bold text-white">Business</td>
<td class="text-center text-brand-400 font-bold">4 Cores</td>
<td class="text-center">8 GB</td>
<td class="text-center">160 GB</td>
<td class="text-center">5 TB</td>
<td class="text-right pr-4 font-bold text-lg">₩67,800</td>
</tr>
<template x-for="(plan, index) in plans" :key="plan.plan">
<tr :class="plan.featured
? 'border-b border-slate-700/50 bg-brand-500/5 hover:bg-brand-500/10 transition border-l-4 border-l-brand-500'
: (index < plans.length - 1 ? 'border-b border-slate-700/50 hover:bg-slate-800/50 transition' : 'hover:bg-slate-800/50 transition')">
<td :class="plan.featured ? 'py-4 pl-3 font-bold text-brand-400' : 'py-4 pl-4 font-bold text-white'">
<span x-text="plan.plan"></span>
<span x-show="plan.featured"></span>
</td>
<td :class="plan.featured ? 'text-center text-brand-400 font-bold' : 'text-center text-brand-400'" x-text="plan.vcpu"></td>
<td :class="plan.featured ? 'text-center font-bold' : 'text-center'" x-text="plan.ram"></td>
<td :class="isSeoul() ? 'text-center text-green-400 font-bold' : 'text-center'" x-text="plan.ssd"></td>
<td class="text-center" x-text="plan.transfer"></td>
<td :class="plan.featured ? 'text-right pr-4 font-bold text-lg text-brand-400' : 'text-right pr-4 font-bold text-lg'" x-text="formatPrice(plan.price)"></td>
</tr>
</template>
</tbody>
</table>
</div>
<p class="mt-4 text-center text-xs text-slate-500">
<!-- Global Note -->
<p x-show="!isSeoul()" class="mt-4 text-center text-xs text-slate-500">
* 🇯🇵 도쿄, 🇸🇬 싱가포르 기준 요금입니다. 🇭🇰 홍콩 리전은 약 10%의 추가 요금이 발생할 수 있습니다.
</p>
</div>
<!-- Seoul Pricing Table -->
<div x-show="region === 'seoul'" style="display: none;" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform scale-95" x-transition:enter-end="opacity-100 transform scale-100">
<div class="overflow-x-auto">
<table class="w-full text-left font-sans">
<thead>
<tr class="text-slate-500 text-sm border-b border-slate-700">
<th class="pb-4 font-medium pl-4">Plan</th>
<th class="pb-4 font-medium text-center">vCPU</th>
<th class="pb-4 font-medium text-center">RAM</th>
<th class="pb-4 font-medium text-center text-green-400">SSD (High)</th>
<th class="pb-4 font-medium text-center">Transfer</th>
<th class="pb-4 font-medium text-right pr-4">Price / Month</th>
</tr>
</thead>
<tbody class="text-slate-300 text-sm">
<!-- Nano Plan -->
<tr class="border-b border-slate-700/50 hover:bg-slate-800/50 transition">
<td class="py-4 pl-4 font-bold text-white">Nano</td>
<td class="text-center text-brand-400">1 Core</td>
<td class="text-center">512 MB</td>
<td class="text-center text-green-400 font-bold">20 GB</td>
<td class="text-center">1 TB</td>
<td class="text-right pr-4 font-bold text-lg">₩6,000</td>
</tr>
<tr class="border-b border-slate-700/50 hover:bg-slate-800/50 transition">
<td class="py-4 pl-4 font-bold text-white">Micro</td>
<td class="text-center text-brand-400">1 Core</td>
<td class="text-center">1 GB</td>
<td class="text-center text-green-400 font-bold">40 GB</td>
<td class="text-center">2 TB</td>
<td class="text-right pr-4 font-bold text-lg">₩8,500</td>
</tr>
<tr class="border-b border-slate-700/50 hover:bg-slate-800/50 transition">
<td class="py-4 pl-4 font-bold text-white">Starter</td>
<td class="text-center text-brand-400">1 Core</td>
<td class="text-center">2 GB</td>
<td class="text-center text-green-400 font-bold">60 GB</td>
<td class="text-center">3 TB</td>
<td class="text-right pr-4 font-bold text-lg">₩17,000</td>
</tr>
<tr class="border-b border-slate-700/50 bg-brand-500/5 hover:bg-brand-500/10 transition border-l-4 border-l-brand-500">
<td class="py-4 pl-3 font-bold text-brand-400">Pro ⭐</td>
<td class="text-center text-brand-400 font-bold">2 Cores</td>
<td class="text-center font-bold">4 GB</td>
<td class="text-center text-green-400 font-bold">80 GB</td>
<td class="text-center">4 TB</td>
<td class="text-right pr-4 font-bold text-lg text-brand-400">₩33,900</td>
</tr>
<tr class="hover:bg-slate-800/50 transition">
<td class="py-4 pl-4 font-bold text-white">Business</td>
<td class="text-center text-brand-400">2 Cores</td>
<td class="text-center">8 GB</td>
<td class="text-center text-green-400 font-bold">160 GB</td>
<td class="text-center">5 TB</td>
<td class="text-right pr-4 font-bold text-lg">₩67,800</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-6 p-4 bg-slate-800/50 rounded-lg border border-slate-700 flex flex-col md:flex-row justify-between items-center gap-4">
<!-- Seoul Enterprise Note -->
<div x-show="isSeoul()" class="mt-6 p-4 bg-slate-800/50 rounded-lg border border-slate-700 flex flex-col md:flex-row justify-between items-center gap-4">
<div class="text-xs text-slate-400">
<span class="text-white font-bold block mb-1">더 강력한 성능이 필요하신가요?</span>
AWS EC2 기반의 고성능(4 vCPU+) 인스턴스는 별도 문의 바랍니다. (VPC Peering 지원)
</div>
<a href="https://t.me/AnvilForgeBot" target="_blank" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white text-xs font-bold rounded transition">
<a href="https://t.me/AnvilForgeBot" target="_blank" rel="noopener noreferrer" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white text-xs font-bold rounded transition">
Enterprise 문의 →
</a>
</div>
@@ -743,9 +622,9 @@
<span class="font-bold text-slate-300">Anvil Hosting</span> | Global Cloud Infrastructure
</div>
<div class="flex gap-6">
<a href="#" class="hover:text-slate-300">이용약관</a>
<a href="#" class="hover:text-slate-300">개인정보처리방침</a>
<a href="#" class="hover:text-slate-300">SLA</a>
<a href="https://t.me/AnvilForgeBot?start=terms" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-white transition">이용약관</a>
<a href="https://t.me/AnvilForgeBot?start=privacy" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-white transition">개인정보처리방침</a>
<a href="https://t.me/AnvilForgeBot?start=sla" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-white transition">SLA</a>
</div>
<div class="text-right">
<div>© 2026 LIBEHAIM Inc. | Taro Tanaka</div>
@@ -754,27 +633,230 @@
</div>
</footer>
<script>
// Real-time Ping Simulation
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 }
];
<!-- Server Launcher Modal -->
<div x-show="launcherOpen"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-dark-900/90 backdrop-blur-sm"
style="display: none;">
<div @click.away="if(!launching) launcherOpen = false"
role="dialog"
aria-modal="true"
aria-labelledby="launcher-title"
class="bg-slate-900 border border-white/10 w-full max-w-xl rounded-2xl shadow-2xl overflow-hidden relative">
<!-- Modal Header -->
<div class="px-6 py-4 border-b border-white/5 flex justify-between items-center bg-slate-800/50">
<h3 id="launcher-title" class="text-xl font-bold flex items-center gap-2">
<span class="text-brand-400">🚀</span> Server Launcher
</h3>
<button @click="resetLauncher" aria-label="닫기" class="text-slate-400 hover:text-white" x-show="!launching">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="p-8">
<!-- Step 0: Configuration -->
<div x-show="step === 0" class="space-y-8">
<div>
<label class="block text-xs font-mono text-slate-500 uppercase tracking-widest mb-4">1. Select Region</label>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<template x-for="r in ['Seoul', 'Tokyo', 'Singapore', 'Hong Kong']">
<button @click="config.region = r"
:class="config.region === r ? 'border-brand-500 bg-brand-500/10 text-white shadow-[0_0_15px_rgba(56,189,248,0.2)]' : 'border-slate-700 bg-slate-800 text-slate-400 hover:border-slate-500 hover:bg-slate-700/50'"
class="px-4 py-3 rounded-xl border text-sm font-bold transition-all text-center">
<span x-text="r"></span>
<div x-show="r === 'Seoul'" class="text-[9px] text-brand-400 mt-1">Premium</div>
</button>
</template>
</div>
</div>
<div>
<label class="block text-xs font-mono text-slate-500 uppercase tracking-widest mb-4">2. Choose OS</label>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<template x-for="o in ['Debian 12', 'Ubuntu 24.04', 'CentOS 9', 'Alpine']">
<button @click="config.os = o"
:class="config.os === o ? 'border-purple-500 bg-purple-500/10 text-white shadow-[0_0_15px_rgba(168,85,247,0.2)]' : 'border-slate-700 bg-slate-800 text-slate-400 hover:border-slate-500 hover:bg-slate-700/50'"
class="px-4 py-3 rounded-xl border text-sm font-bold transition-all text-center">
<span x-text="o"></span>
</button>
</template>
</div>
</div>
<div>
<label class="block text-xs font-mono text-slate-500 uppercase tracking-widest mb-4">3. Instance Plan & Pricing</label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<template x-for="p in ['Micro', 'Starter', 'Pro', 'Business']">
<button @click="config.plan = p"
:class="config.plan === p ? 'border-brand-500 ring-2 ring-brand-500/20 bg-brand-500/5' : 'border-slate-700 bg-slate-800 hover:border-brand-400/50'"
class="p-4 rounded-xl border transition-all text-left group flex justify-between items-center">
<div>
<div class="font-bold text-white text-base mb-1" x-text="p"></div>
<div class="text-xs text-slate-500" x-text="getPlanSpec(p)"></div>
</div>
<div class="text-right">
<div class="text-brand-400 font-bold text-lg" x-text="'₩' + getPrice(p)"></div>
<div class="text-[10px] text-slate-600">/ month</div>
</div>
</button>
</template>
</div>
</div>
<button @click="startLaunch" class="w-full py-4 bg-brand-600 hover:bg-brand-500 text-white font-bold rounded-xl transition-all shadow-xl shadow-brand-500/20 flex items-center justify-center gap-3 text-lg">
<span>🚀 Launch Instance in</span>
<span class="bg-black/20 px-2 py-0.5 rounded text-brand-300" x-text="config.region"></span>
</button>
</div>
<!-- Launching State -->
<div x-show="step > 0" class="min-h-[400px] flex flex-col">
<div class="mb-8">
<div class="flex justify-between text-xs font-mono text-slate-500 mb-2">
<span x-text="step < 5 ? 'Deploying...' : 'Ready'"></span>
<span x-text="(step * 20) + '%'"></span>
</div>
<div class="h-2 bg-slate-800 rounded-full overflow-hidden">
<div class="h-full bg-brand-500 transition-all duration-500" :style="'width: ' + (step * 20) + '%'"></div>
</div>
</div>
<div class="flex-1 bg-black/40 rounded-xl p-6 font-mono text-xs text-slate-400 overflow-y-auto border border-white/5 space-y-2">
<template x-for="log in logs">
<div class="flex gap-4">
<span class="text-slate-600" x-text="new Date().toLocaleTimeString()"></span>
<span :class="log.includes('[SUCCESS]') ? 'text-green-400' : log.includes('[COMPLETE]') ? 'text-brand-400 font-bold' : 'text-slate-300'" x-text="log"></span>
</div>
</template>
<div x-show="launching" class="animate-pulse">_</div>
</div>
<div x-show="step === 5" x-transition class="mt-8 p-6 bg-brand-500/10 border border-brand-500/20 rounded-xl flex flex-col items-center text-center">
<div class="w-12 h-12 rounded-full bg-brand-500 flex items-center justify-center text-white text-2xl mb-4"></div>
<h4 class="text-xl font-bold text-white mb-2">서버가 생성되었습니다!</h4>
<p class="text-slate-400 text-sm mb-6">설정한 구성대로 인스턴스가 활성화되었습니다.</p>
<div class="flex gap-4 w-full">
<button @click="resetLauncher" class="flex-1 py-3 bg-slate-800 hover:bg-slate-700 text-white font-bold rounded-lg transition">닫기</button>
<a href="https://t.me/AnvilForgeBot" target="_blank" class="flex-1 py-3 bg-brand-600 hover:bg-brand-500 text-white font-bold rounded-lg transition text-center">Console 이동</a>
</div>
</div>
</div>
</div>
</div>
</div>
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.floor(region.base + jitter);
if (val < 1) val = 1;
el.innerText = val;
}
});
}
setInterval(updatePing, 2000);
</script>
</body>
</html>