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:
132
js/utils.js
Normal file
132
js/utils.js
Normal 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user