Files
anvil-hosting/js/wizard.js
kappa 758266d8cb refactor: app.js를 ES6 모듈로 분리
## 변경사항
- app.js (1370줄) → 7개 모듈 (1427줄)
- ES6 import/export 문법 사용
- Alpine.js 호환성 유지 (window 전역 노출)

## 모듈 구조
- js/config.js: 상수/설정 (WIZARD_CONFIG, PRICING_DATA, MOCK_*)
- js/api.js: ApiService
- js/utils.js: formatPrice, switchTab, ping 시뮬레이션
- js/wizard.js: 서버 추천 마법사 로직
- js/pricing.js: 가격표 컴포넌트
- js/dashboard.js: 대시보드 및 텔레그램 연동
- js/app.js: 메인 통합 (모든 모듈 import)

## HTML 변경
- <script type="module" src="js/app.js">로 변경
- 기존 기능 모두 정상 작동

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 12:59:54 +09:00

200 lines
5.7 KiB
JavaScript

/**
* Server Recommendation Wizard
* 서버 추천 마법사 로직
*/
import { WIZARD_CONFIG } from './config.js';
/**
* 서버 추천 로직 (룰 기반)
* @param {string[]} selectedStacks - 선택된 스택 ID 목록
* @param {string} scale - 규모 키 (personal, small, medium, large)
* @returns {Object} 추천 결과 { economy, recommended, performance }
*/
export 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
};
}
/**
* 마법사 앱 상태 및 메서드
* anvilApp에서 사용할 마법사 관련 로직
*/
export function createWizardMethods() {
return {
// 마법사 상태
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;
// 텔레그램 봇으로 연결 (선택한 사양 정보 포함)
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;
}
};
}