## 변경사항 - 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>
133 lines
3.8 KiB
JavaScript
133 lines
3.8 KiB
JavaScript
/**
|
|
* 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');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|