feat: add server recommendation wizard
- Add 4-step wizard (purpose → stack → scale → recommendation) - Rule-based recommendation engine (WIZARD_CONFIG) - 6 purpose categories: web, game, AI/ML, dev, database, other - Multiple stack selection with category grouping - 4 scale options with RAM/CPU multipliers - 3-tier recommendations: economy, recommended, performance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
218
index.html
218
index.html
@@ -213,9 +213,9 @@
|
||||
|
||||
<!-- 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">
|
||||
<button @click="openWizard()" :aria-expanded="wizardOpen" 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>
|
||||
<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>
|
||||
|
||||
@@ -1253,6 +1253,220 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Server Recommendation Wizard Modal -->
|
||||
<div x-show="wizardOpen"
|
||||
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"
|
||||
@keydown.escape.window="closeWizard()"
|
||||
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="closeWizard()"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="wizard-title"
|
||||
class="bg-slate-900/95 backdrop-blur-xl border border-white/10 rounded-3xl shadow-2xl shadow-black/50 overflow-hidden w-full max-w-lg">
|
||||
|
||||
<!-- Wizard Header -->
|
||||
<div class="px-6 py-4 border-b border-white/5 flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-brand-500 to-purple-500 flex items-center justify-center text-lg">🧙</div>
|
||||
<div>
|
||||
<h2 id="wizard-title" class="text-lg font-bold text-white">서버 추천 마법사</h2>
|
||||
<p class="text-xs text-slate-400">기술 스택에 맞는 최적의 서버를 찾아드립니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="closeWizard()"
|
||||
aria-label="마법사 닫기"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-full bg-white/5 text-slate-400 hover:bg-white/10 hover:text-white transition">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="px-6 pt-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<template x-for="step in 4" :key="step">
|
||||
<div class="flex-1 h-1.5 rounded-full transition-all duration-300"
|
||||
:class="wizardCurrentStep >= step - 1 ? 'bg-brand-500' : 'bg-slate-700'"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-slate-500">
|
||||
<span :class="wizardCurrentStep === 0 && 'text-brand-400 font-medium'">목적</span>
|
||||
<span :class="wizardCurrentStep === 1 && 'text-brand-400 font-medium'">기술스택</span>
|
||||
<span :class="wizardCurrentStep === 2 && 'text-brand-400 font-medium'">규모</span>
|
||||
<span :class="wizardCurrentStep === 3 && 'text-brand-400 font-medium'">추천</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard Content -->
|
||||
<div class="p-6 max-h-[60vh] overflow-y-auto">
|
||||
|
||||
<!-- Step 0: Purpose Selection -->
|
||||
<div x-show="wizardCurrentStep === 0" x-transition>
|
||||
<h3 class="text-base font-semibold text-white mb-4">어떤 용도로 서버를 사용하시나요?</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<template x-for="purpose in Object.keys(WIZARD_CONFIG.purposes)" :key="purpose">
|
||||
<button @click="selectWizardPurpose(purpose)"
|
||||
class="p-4 rounded-xl border-2 transition-all text-left hover:border-brand-500/50 hover:bg-brand-500/5"
|
||||
:class="wizardPurpose === purpose ? 'border-brand-500 bg-brand-500/10' : 'border-slate-700/50 bg-slate-800/50'">
|
||||
<div class="text-2xl mb-2" x-text="WIZARD_CONFIG.purposes[purpose].icon"></div>
|
||||
<div class="font-medium text-white text-sm" x-text="WIZARD_CONFIG.purposes[purpose].name"></div>
|
||||
<div class="text-xs text-slate-400 mt-1" x-text="WIZARD_CONFIG.purposes[purpose].desc"></div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Stack Selection -->
|
||||
<div x-show="wizardCurrentStep === 1" x-transition>
|
||||
<h3 class="text-base font-semibold text-white mb-2">사용할 기술 스택을 선택하세요</h3>
|
||||
<p class="text-xs text-slate-400 mb-4">복수 선택 가능합니다</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<template x-for="(stacks, category) in getWizardStacksByPurpose()" :key="category">
|
||||
<div>
|
||||
<h4 class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-2" x-text="category"></h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="stack in stacks" :key="stack.id">
|
||||
<button @click="toggleWizardStack(stack.id)"
|
||||
class="px-3 py-2 rounded-lg border transition-all text-sm flex items-center gap-2"
|
||||
:class="wizardStacks.includes(stack.id)
|
||||
? 'border-brand-500 bg-brand-500/20 text-brand-300'
|
||||
: 'border-slate-700 bg-slate-800/50 text-slate-300 hover:border-slate-600'">
|
||||
<span x-text="stack.icon"></span>
|
||||
<span x-text="stack.name"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button @click="wizardGoBack()" class="flex-1 py-3 bg-slate-800 hover:bg-slate-700 text-white rounded-xl transition text-sm">
|
||||
← 이전
|
||||
</button>
|
||||
<button @click="confirmWizardStacks()"
|
||||
:disabled="wizardStacks.length === 0"
|
||||
class="flex-1 py-3 bg-brand-500 hover:bg-brand-600 disabled:bg-slate-700 disabled:text-slate-500 text-white font-medium rounded-xl transition text-sm">
|
||||
다음 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Scale Selection -->
|
||||
<div x-show="wizardCurrentStep === 2" x-transition>
|
||||
<h3 class="text-base font-semibold text-white mb-4">예상 사용 규모를 선택하세요</h3>
|
||||
<div class="space-y-3">
|
||||
<template x-for="(scale, key) in WIZARD_CONFIG.scales" :key="key">
|
||||
<button @click="selectWizardScale(key)"
|
||||
class="w-full p-4 rounded-xl border-2 transition-all text-left hover:border-brand-500/50"
|
||||
:class="wizardScale === key ? 'border-brand-500 bg-brand-500/10' : 'border-slate-700/50 bg-slate-800/50'">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xl" x-text="scale.icon"></span>
|
||||
<span class="font-medium text-white" x-text="scale.name"></span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-400 mt-1" x-text="scale.desc"></div>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500" x-text="scale.users"></div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<button @click="wizardGoBack()" class="w-full py-3 bg-slate-800 hover:bg-slate-700 text-white rounded-xl transition text-sm">
|
||||
← 이전
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Recommendations -->
|
||||
<div x-show="wizardCurrentStep === 3" x-transition>
|
||||
<h3 class="text-base font-semibold text-white mb-2">추천 서버 사양</h3>
|
||||
<p class="text-xs text-slate-400 mb-4">선택하신 조건에 맞는 최적의 서버입니다</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Economy -->
|
||||
<div x-show="wizardRecommendations?.economy"
|
||||
class="p-4 rounded-xl border border-slate-700/50 bg-slate-800/50 hover:border-slate-600 transition cursor-pointer"
|
||||
@click="selectWizardPlan('economy')">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium text-slate-400 uppercase">Economy</span>
|
||||
<span class="text-xs text-slate-500">최소 사양</span>
|
||||
</div>
|
||||
<div class="flex items-baseline gap-2 mb-2">
|
||||
<span class="text-2xl font-bold text-white" x-text="wizardRecommendations?.economy?.cpu + ' vCPU'"></span>
|
||||
<span class="text-slate-400">/</span>
|
||||
<span class="text-lg text-slate-300" x-text="formatWizardRam(wizardRecommendations?.economy?.ram)"></span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500" x-text="'$' + wizardRecommendations?.economy?.price?.toFixed(2) + '/월 ~'"></div>
|
||||
</div>
|
||||
|
||||
<!-- Recommended -->
|
||||
<div x-show="wizardRecommendations?.recommended"
|
||||
class="p-4 rounded-xl border-2 border-brand-500/50 bg-brand-500/5 hover:border-brand-500 transition cursor-pointer relative"
|
||||
@click="selectWizardPlan('recommended')">
|
||||
<div class="absolute -top-2.5 left-4 px-2 py-0.5 bg-brand-500 text-white text-xs font-medium rounded">추천</div>
|
||||
<div class="flex items-center justify-between mb-2 pt-1">
|
||||
<span class="text-xs font-medium text-brand-400 uppercase">Recommended</span>
|
||||
<span class="text-xs text-slate-500">최적 사양</span>
|
||||
</div>
|
||||
<div class="flex items-baseline gap-2 mb-2">
|
||||
<span class="text-2xl font-bold text-white" x-text="wizardRecommendations?.recommended?.cpu + ' vCPU'"></span>
|
||||
<span class="text-slate-400">/</span>
|
||||
<span class="text-lg text-slate-300" x-text="formatWizardRam(wizardRecommendations?.recommended?.ram)"></span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500" x-text="'$' + wizardRecommendations?.recommended?.price?.toFixed(2) + '/월 ~'"></div>
|
||||
</div>
|
||||
|
||||
<!-- Performance -->
|
||||
<div x-show="wizardRecommendations?.performance"
|
||||
class="p-4 rounded-xl border border-purple-500/30 bg-purple-500/5 hover:border-purple-500/50 transition cursor-pointer"
|
||||
@click="selectWizardPlan('performance')">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium text-purple-400 uppercase">Performance</span>
|
||||
<span class="text-xs text-slate-500">고성능</span>
|
||||
</div>
|
||||
<div class="flex items-baseline gap-2 mb-2">
|
||||
<span class="text-2xl font-bold text-white" x-text="wizardRecommendations?.performance?.cpu + ' vCPU'"></span>
|
||||
<span class="text-slate-400">/</span>
|
||||
<span class="text-lg text-slate-300" x-text="formatWizardRam(wizardRecommendations?.performance?.ram)"></span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500" x-text="'$' + wizardRecommendations?.performance?.price?.toFixed(2) + '/월 ~'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="mt-4 p-3 bg-slate-800/50 rounded-xl">
|
||||
<div class="text-xs text-slate-400 mb-2">선택 요약</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="px-2 py-1 bg-slate-700/50 rounded text-xs text-slate-300" x-text="WIZARD_CONFIG.purposes[wizardPurpose]?.name"></span>
|
||||
<template x-for="stackId in wizardStacks" :key="stackId">
|
||||
<span class="px-2 py-1 bg-slate-700/50 rounded text-xs text-slate-300" x-text="getWizardStackName(stackId)"></span>
|
||||
</template>
|
||||
<span class="px-2 py-1 bg-slate-700/50 rounded text-xs text-slate-300" x-text="WIZARD_CONFIG.scales[wizardScale]?.name"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<button @click="wizardGoBack()" class="w-full py-3 bg-slate-800 hover:bg-slate-700 text-white rounded-xl transition text-sm">
|
||||
← 다시 선택하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll Animation Script -->
|
||||
<script>
|
||||
// Intersection Observer for scroll animations
|
||||
|
||||
Reference in New Issue
Block a user