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>
This commit is contained in:
199
js/wizard.js
Normal file
199
js/wizard.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user