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:
kappa
2026-01-23 12:59:54 +09:00
parent 347a5cc598
commit 758266d8cb
21 changed files with 2193 additions and 194 deletions

132
js/utils.js Normal file
View File

@@ -0,0 +1,132 @@
/**
* Utility Functions
* 유틸리티 함수 모음
*/
/**
* 가격 포맷팅 (한국 원화)
*/
export function formatPrice(price) {
return '₩' + price.toLocaleString('ko-KR');
}
/**
* 탭 전환 (n8n/Terraform)
*/
export 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');
// Null checks for DOM elements
if (!btnN8n || !btnTf || !panelN8n || !panelTf) return;
if (tab === 'n8n') {
// Update ARIA states for n8n tab
btnN8n.setAttribute('aria-selected', 'true');
btnN8n.setAttribute('tabindex', '0');
btnTf.setAttribute('aria-selected', 'false');
btnTf.setAttribute('tabindex', '-1');
// Update classes
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 {
// Update ARIA states for Terraform tab
btnTf.setAttribute('aria-selected', 'true');
btnTf.setAttribute('tabindex', '0');
btnN8n.setAttribute('aria-selected', 'false');
btnN8n.setAttribute('tabindex', '-1');
// Update classes
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 시뮬레이션
*/
export 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 업데이트 시작 (visibility-aware)
let pingInterval;
export function startPingUpdates() {
if (!pingInterval) {
pingInterval = setInterval(updatePing, 2000);
}
}
export function stopPingUpdates() {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
}
// Visibility change handler
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPingUpdates();
} else {
startPingUpdates();
}
});
// Initial start
startPingUpdates();
// Tab switching with event listeners and keyboard navigation
document.addEventListener('DOMContentLoaded', () => {
const btnN8n = document.getElementById('btn-n8n');
const btnTf = document.getElementById('btn-tf');
if (btnN8n && btnTf) {
// Click event listeners
btnN8n.addEventListener('click', () => switchTab('n8n'));
btnTf.addEventListener('click', () => switchTab('tf'));
// Keyboard navigation (Arrow keys)
[btnN8n, btnTf].forEach(btn => {
btn.addEventListener('keydown', (e) => {
const currentTab = btn.getAttribute('data-tab');
if (e.key === 'ArrowRight') {
e.preventDefault();
if (currentTab === 'n8n') {
btnTf.focus();
switchTab('tf');
}
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
if (currentTab === 'tf') {
btnN8n.focus();
switchTab('n8n');
}
}
});
});
}
});