feat: SaaS Hero Template 스타일 적용
- 그리드 배경 패턴 추가 (.hero-grid-bg) - 타이틀 그라디언트 텍스트 효과 (.hero-title-gradient) - Telegram 데모에 펄스 글로우 효과 (.hero-glow) - CTA 버튼 그라디언트 + 호버 스케일 효과 (.btn-gradient) - 순차 페이드인 애니메이션 (.animate-fade-in-*) - Ping 위젯 스타일 개선 (두꺼운 바, backdrop-blur-md) - 배지 호버 효과 및 화살표 아이콘 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
241
index.html
241
index.html
@@ -56,6 +56,66 @@
|
||||
linear-gradient(180deg,#0a0f1a 0%,#0f172a 50%,#0a0f1a 100%);
|
||||
background-size:300px 300px,300px 300px,100% 100%,100% 100%,100% 100%;
|
||||
}
|
||||
/* SaaS Hero Styles */
|
||||
.hero-grid-bg {
|
||||
background-image: linear-gradient(to right, rgba(56,189,248,0.05) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(56,189,248,0.05) 1px, transparent 1px);
|
||||
background-size: 4rem 4rem;
|
||||
mask-image: radial-gradient(ellipse 80% 50% at 50% 0%, #000 70%, transparent 110%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 80% 50% at 50% 0%, #000 70%, transparent 110%);
|
||||
}
|
||||
.hero-title-gradient {
|
||||
background: linear-gradient(to bottom, #ffffff 0%, #ffffff 50%, rgba(255,255,255,0.6) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.hero-glow {
|
||||
position: relative;
|
||||
}
|
||||
.hero-glow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -20%;
|
||||
background: radial-gradient(circle at center, rgba(56,189,248,0.3) 0%, transparent 70%);
|
||||
filter: blur(40px);
|
||||
z-index: -1;
|
||||
animation: glow-pulse 4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% { opacity: 0.5; transform: scale(1); }
|
||||
50% { opacity: 0.8; transform: scale(1.05); }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.8s ease-out forwards;
|
||||
}
|
||||
.animate-fade-in-delay {
|
||||
animation: fadeIn 0.8s ease-out 0.2s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.animate-fade-in-delay-2 {
|
||||
animation: fadeIn 0.8s ease-out 0.4s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.btn-gradient {
|
||||
background: linear-gradient(to bottom, #ffffff 0%, rgba(255,255,255,0.95) 50%, rgba(255,255,255,0.8) 100%);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-gradient:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 20px 40px -10px rgba(56,189,248,0.3);
|
||||
}
|
||||
.btn-gradient:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- App JavaScript (must load before Alpine.js) -->
|
||||
@@ -107,20 +167,27 @@
|
||||
<main id="main-content">
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative pt-24 pb-20 overflow-hidden grid-bg gradient-bg noise-bg">
|
||||
<!-- Animated Blobs -->
|
||||
<div class="blob blob-1 w-[600px] h-[600px] bg-brand-500/20 top-[-200px] left-[-100px] -z-10"></div>
|
||||
<div class="blob blob-2 w-[500px] h-[500px] bg-purple-500/15 top-[100px] right-[-150px] -z-10"></div>
|
||||
<div class="blob blob-3 w-[400px] h-[400px] bg-brand-400/10 bottom-[-100px] left-[30%] -z-10"></div>
|
||||
<section class="relative pt-32 pb-24 overflow-hidden">
|
||||
<!-- Grid Background -->
|
||||
<div class="absolute inset-0 hero-grid-bg opacity-60 h-[600px]"></div>
|
||||
|
||||
<!-- Radial Accent -->
|
||||
<div class="absolute left-1/2 top-[80%] h-[500px] w-[90%] -translate-x-1/2 rounded-[100%] bg-gradient-to-t from-brand-500/20 to-transparent blur-3xl"></div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-6 grid lg:grid-cols-2 gap-16 items-center relative z-10">
|
||||
<!-- Left: Text -->
|
||||
<div>
|
||||
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full glass-card-static text-xs font-mono text-brand-400 mb-6">
|
||||
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
|
||||
System Operational: v2.4.0 Live
|
||||
</div>
|
||||
<h1 class="text-5xl md:text-6xl font-bold leading-tight mb-6"
|
||||
<div class="text-center lg:text-left">
|
||||
<!-- Badge -->
|
||||
<a href="#features" class="group inline-flex items-center gap-2 px-4 py-2 rounded-full border border-slate-700/50 bg-slate-800/50 backdrop-blur-sm text-sm text-slate-300 mb-8 animate-fade-in hover:border-brand-500/50 transition-colors">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
|
||||
System Operational: v2.4.0 Live
|
||||
</span>
|
||||
<svg class="w-4 h-4 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="M9 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.1] tracking-tight mb-6 animate-fade-in-delay"
|
||||
x-data="{
|
||||
phrases: [
|
||||
{ highlight: 'Incus/LXD', text: '기반 초경량 컨테이너 호스팅' },
|
||||
@@ -133,52 +200,55 @@
|
||||
}"
|
||||
x-init="setInterval(() => { show = false; setTimeout(() => { current = (current + 1) % phrases.length; show = true; }, 300); }, 4000)">
|
||||
<span class="inline-block transition-all duration-500" :class="show ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-4'">
|
||||
<span class="gradient-text-animated" x-text="phrases[current].highlight"></span><br>
|
||||
<span x-text="phrases[current].text"></span>
|
||||
<span class="hero-title-gradient" x-text="phrases[current].highlight"></span><br>
|
||||
<span class="text-slate-300" x-text="phrases[current].text"></span>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-lg text-slate-400 mb-8 leading-relaxed">
|
||||
|
||||
<!-- Subtitle -->
|
||||
<p class="text-base md:text-lg text-slate-400 mb-10 leading-relaxed max-w-xl mx-auto lg:mx-0 animate-fade-in-delay-2">
|
||||
VM의 오버헤드는 버리세요. OS 수준의 완벽한 격리와 네이티브 성능을 제공합니다.
|
||||
<span class="text-white font-medium">n8n, Ansible</span> 자동화 파이프라인과 즉시 연동됩니다.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="relative">
|
||||
<button @click="openLauncher()" :aria-expanded="launcherOpen" aria-haspopup="dialog" aria-label="인스턴스 즉시 배포 - 서버 런처 열기" class="btn-glow relative w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-white to-slate-100 text-dark-900 font-bold rounded-xl flex items-center justify-center gap-3 transition-all active:scale-95 group">
|
||||
<span class="text-xl">🚀</span>
|
||||
<span>인스턴스 즉시 배포</span>
|
||||
<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" aria-hidden="true"><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">
|
||||
<!-- CTA & Ping Widget -->
|
||||
<div class="flex flex-col sm:flex-row gap-6 items-center justify-center lg:justify-start animate-fade-in-delay-2">
|
||||
<button @click="openLauncher()" :aria-expanded="launcherOpen" aria-haspopup="dialog" aria-label="인스턴스 즉시 배포 - 서버 런처 열기" class="btn-gradient px-8 py-4 text-dark-900 font-bold rounded-xl flex items-center justify-center gap-3 group">
|
||||
<span class="text-xl">🚀</span>
|
||||
<span>인스턴스 즉시 배포</span>
|
||||
<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" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- Ping Widget -->
|
||||
<div id="ping-widget" class="flex flex-col gap-1.5 text-xs font-mono text-slate-400 bg-slate-900/60 p-4 rounded-xl border border-slate-700/50 backdrop-blur-md">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-24 whitespace-nowrap">🇰🇷 Seoul</span>
|
||||
<div class="flex-1 h-1 bg-slate-700 rounded-full overflow-hidden"><div class="h-full bg-green-400 w-[95%]"></div></div>
|
||||
<span class="text-green-400 w-12 text-right"><span id="ping-kr">2</span>ms</span>
|
||||
<div class="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden w-20"><div class="h-full bg-green-400 w-[95%] rounded-full"></div></div>
|
||||
<span class="text-green-400 w-12 text-right font-semibold"><span id="ping-kr">2</span>ms</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-24 whitespace-nowrap">🇯🇵 Tokyo</span>
|
||||
<div class="flex-1 h-1 bg-slate-700 rounded-full overflow-hidden"><div class="h-full bg-brand-400 w-[80%]"></div></div>
|
||||
<span class="text-brand-400 w-12 text-right"><span id="ping-jp">35</span>ms</span>
|
||||
<div class="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden w-20"><div class="h-full bg-brand-400 w-[80%] rounded-full"></div></div>
|
||||
<span class="text-brand-400 w-12 text-right font-semibold"><span id="ping-jp">35</span>ms</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-24 whitespace-nowrap">🇭🇰 HongKong</span>
|
||||
<div class="flex-1 h-1 bg-slate-700 rounded-full overflow-hidden"><div class="h-full bg-yellow-400 w-[70%]"></div></div>
|
||||
<span class="text-yellow-400 w-12 text-right"><span id="ping-hk">45</span>ms</span>
|
||||
<div class="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden w-20"><div class="h-full bg-yellow-400 w-[70%] rounded-full"></div></div>
|
||||
<span class="text-yellow-400 w-12 text-right font-semibold"><span id="ping-hk">45</span>ms</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-24 whitespace-nowrap">🇸🇬 Singapore</span>
|
||||
<div class="flex-1 h-1 bg-slate-700 rounded-full overflow-hidden"><div class="h-full bg-yellow-400 w-[60%]"></div></div>
|
||||
<span class="text-yellow-400 w-12 text-right"><span id="ping-sg">65</span>ms</span>
|
||||
<div class="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden w-20"><div class="h-full bg-yellow-400 w-[60%] rounded-full"></div></div>
|
||||
<span class="text-yellow-400 w-12 text-right font-semibold"><span id="ping-sg">65</span>ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Telegram Chat Demo -->
|
||||
<div class="relative max-w-md mx-auto w-full">
|
||||
<div class="relative max-w-md mx-auto w-full hero-glow">
|
||||
<!-- Telegram Window -->
|
||||
<div class="rounded-2xl shadow-2xl overflow-hidden relative border border-slate-700">
|
||||
<div class="rounded-2xl shadow-2xl overflow-hidden relative border border-slate-700/50 backdrop-blur-sm">
|
||||
<!-- Telegram Header -->
|
||||
<div class="bg-[#232e3c] px-4 py-3 flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white text-lg font-bold">A</div>
|
||||
@@ -303,8 +373,7 @@
|
||||
:class="selectedSeoulType === type.id
|
||||
? 'bg-brand-500 text-white border-brand-500'
|
||||
: 'bg-transparent text-slate-400 border-slate-600 hover:border-slate-500 hover:text-white'"
|
||||
class="px-4 py-2 rounded-lg border text-sm font-medium transition-all flex items-center gap-2"
|
||||
:title="type.desc">
|
||||
class="px-4 py-2 rounded-lg border text-sm font-medium transition-all flex items-center gap-2">
|
||||
<span x-text="type.name"></span>
|
||||
<span class="text-xs opacity-70" x-text="'(' + getSeoulTypeCount(type.id) + ')'"></span>
|
||||
</button>
|
||||
@@ -321,8 +390,7 @@
|
||||
:class="selectedGlobalType === type.id
|
||||
? 'bg-brand-500 text-white border-brand-500'
|
||||
: 'bg-transparent text-slate-400 border-slate-600 hover:border-slate-500 hover:text-white'"
|
||||
class="px-4 py-2 rounded-lg border text-sm font-medium transition-all flex items-center gap-2"
|
||||
:title="type.desc">
|
||||
class="px-4 py-2 rounded-lg border text-sm font-medium transition-all flex items-center gap-2">
|
||||
<span x-text="type.name"></span>
|
||||
<span class="text-xs opacity-70" x-text="'(' + getGlobalTypeCount(type.id) + ')'"></span>
|
||||
</button>
|
||||
@@ -333,6 +401,18 @@
|
||||
<!-- GPU 탭에서 간격 맞추기 -->
|
||||
<div x-show="selectedCity.startsWith('gpu-')" class="mb-4"></div>
|
||||
|
||||
<!-- Selected Instance Type Info -->
|
||||
<div x-show="selectedCity === 'seoul'" x-transition class="mb-4 glass-panel rounded-xl p-4 text-sm text-slate-300">
|
||||
<span class="text-brand-400 font-bold" x-text="seoulTypes.find(t => t.id === selectedSeoulType)?.name"></span>
|
||||
<span class="mx-2 text-slate-600">|</span>
|
||||
<span class="whitespace-pre-line" x-text="seoulTypes.find(t => t.id === selectedSeoulType)?.tooltip"></span>
|
||||
</div>
|
||||
<div x-show="selectedCity === 'global'" x-transition class="mb-4 glass-panel rounded-xl p-4 text-sm text-slate-300">
|
||||
<span class="text-brand-400 font-bold" x-text="globalTypes.find(t => t.id === selectedGlobalType)?.name"></span>
|
||||
<span class="mx-2 text-slate-600">|</span>
|
||||
<span class="whitespace-pre-line" x-text="globalTypes.find(t => t.id === selectedGlobalType)?.tooltip"></span>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="flex justify-between items-center mb-4 text-xs text-slate-500">
|
||||
<span x-show="!loading && !error">
|
||||
@@ -394,7 +474,7 @@
|
||||
|
||||
<!-- Instance Rows -->
|
||||
<template x-for="(inst, idx) in filteredInstances" :key="inst.id + '-' + (inst.region?.region_code || idx)">
|
||||
<tr class="border-b border-slate-700/50 hover:bg-slate-800/50 transition" :class="idx === 0 && 'bg-brand-500/5 border-l-4 border-l-brand-500'">
|
||||
<tr @click="selectInstance(inst)" class="border-b border-slate-700/50 hover:bg-slate-800/50 transition cursor-pointer" :class="selectedInstance?.id === inst.id && selectedInstance?.region?.region_code === inst.region?.region_code && 'bg-brand-500/5 border-l-4 border-l-brand-500'">
|
||||
<!-- GPU (GPU 탭에서만 표시) -->
|
||||
<td x-show="selectedCity.startsWith('gpu-')" class="py-4 pl-4 text-left">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
@@ -422,7 +502,7 @@
|
||||
</td>
|
||||
<!-- 월 요금 (DB에서 가져온 한화 금액) -->
|
||||
<td class="py-4 pr-4 text-right">
|
||||
<div class="font-bold text-lg" :class="idx === 0 ? 'text-brand-400' : 'text-white'" x-text="formatKrw(inst.pricing?.monthly_price_krw)"></div>
|
||||
<div class="font-bold text-lg" :class="selectedInstance?.id === inst.id && selectedInstance?.region?.region_code === inst.region?.region_code ? 'text-brand-400' : 'text-white'" x-text="formatKrw(inst.pricing?.monthly_price_krw)"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -443,6 +523,89 @@
|
||||
<div class="mt-6 text-xs text-slate-500">
|
||||
<p>* 실시간 API 데이터 기준. 실제 요금은 변동될 수 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Instance Detail Modal -->
|
||||
<div x-show="showInstanceModal"
|
||||
x-transition.opacity
|
||||
@click.self="closeInstanceModal()"
|
||||
@keydown.escape.window="closeInstanceModal()"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/80 backdrop-blur-sm p-4"
|
||||
role="dialog" aria-modal="true">
|
||||
|
||||
<!-- Modal Panel -->
|
||||
<div x-show="showInstanceModal"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
@click.stop
|
||||
class="glass-panel rounded-2xl p-6 max-w-md w-full shadow-2xl">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-white" x-text="selectedInstanceDetail?.vcpu + ' Cores · ' + formatMemory(selectedInstanceDetail?.memory_mb)"></h3>
|
||||
<p class="text-sm text-slate-400 mt-1" x-text="selectedInstanceDetail?.region?.region_name || 'Unknown Region'"></p>
|
||||
</div>
|
||||
<button @click="closeInstanceModal()"
|
||||
class="text-slate-400 hover:text-white transition p-1"
|
||||
aria-label="닫기">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Specs Grid -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="glass-panel rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-brand-400" x-text="selectedInstanceDetail?.vcpu"></div>
|
||||
<div class="text-xs text-slate-400 mt-1">vCPU</div>
|
||||
</div>
|
||||
<div class="glass-panel rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-purple-400" x-text="formatMemory(selectedInstanceDetail?.memory_mb)"></div>
|
||||
<div class="text-xs text-slate-400 mt-1">RAM</div>
|
||||
</div>
|
||||
<div class="glass-panel rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-green-400" x-text="formatStorage(selectedInstanceDetail?.storage_gb)"></div>
|
||||
<div class="text-xs text-slate-400 mt-1">Storage</div>
|
||||
</div>
|
||||
<div class="glass-panel rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-amber-400" x-text="formatTransfer(selectedInstanceDetail?.transfer_tb)"></div>
|
||||
<div class="text-xs text-slate-400 mt-1">Transfer</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="glass-panel rounded-xl p-4 mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<div class="text-sm text-slate-400">시간당</div>
|
||||
<div class="text-lg font-medium text-white" x-text="formatKrwHourly(selectedInstanceDetail?.pricing?.hourly_price_krw)"></div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm text-slate-400">월</div>
|
||||
<div class="text-2xl font-bold text-brand-400" x-text="formatKrw(selectedInstanceDetail?.pricing?.monthly_price_krw)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3">
|
||||
<button @click="copyInstanceSpec()"
|
||||
class="flex-1 px-4 py-3 rounded-xl border border-slate-600 hover:border-slate-500 text-white font-medium transition flex items-center justify-center gap-2">
|
||||
<span x-show="!copiedToClipboard">📋 사양 복사</span>
|
||||
<span x-show="copiedToClipboard" class="text-green-400">✓ 복사됨</span>
|
||||
</button>
|
||||
<a :href="getInstanceTelegramLink()" target="_blank"
|
||||
class="flex-1 px-4 py-3 rounded-xl bg-[#0088cc] hover:bg-[#0099dd] text-white font-medium transition text-center">
|
||||
텔레그램에서 주문
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature Highlight -->
|
||||
|
||||
Reference in New Issue
Block a user