import './styles/common.css'; import './styles/login.css'; import { state, loadMasterDataFromDB, saveAsset } from './core/state'; import { renderNavigation } from './components/Navigation'; import { renderDashboard } from './views/DashboardView'; import { renderSWTable } from './views/SW_Table'; import { renderLocationView } from './views/LocationView'; import { renderAuditApprovalView } from './views/AuditApprovalView'; import { MapEditor } from './views/MapEditor'; import { initBaseModal } from './components/Modal/BaseModal'; import { initHwModal, openHwModal } from './components/Modal/HWModal'; import { initSwModal, openSwModal } from './components/Modal/SWModal'; import { initSwUserModal } from './components/Modal/SWUserModal'; import { initDomainModal, openDomainModal } from './components/Modal/DomainModal'; import { initPartsMasterModal, openPartsMasterModal } from './components/Modal/PartsMasterModal'; import { initJobSpecModal, openJobSpecModal } from './components/Modal/JobSpecModal'; import { initUserModal, openUserModal } from './components/Modal/UserModal'; import { activePartsMasterSubTab } from './views/List/PartsMasterListView'; import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal'; import { initGuide } from './components/Guide'; import { pcFlowModal } from './components/Modal/PCFlowModal'; import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } from 'lucide'; interface AuthSessionResponse { authenticated: boolean; user: unknown; } let phoneLoginPollTimer: number | undefined; let activeMapEditorInstance: MapEditor | null = null; // 화면 갱신 통합 핸들러 async function refreshView(tab?: string) { const mainContent = document.getElementById('main-content')!; if (!mainContent) return; // Clean up any active MapEditor instance when navigating away if (activeMapEditorInstance) { activeMapEditorInstance.destroy(); activeMapEditorInstance = null; } const activeTab = tab || state.activeSubTab; if (activeTab === '대시보드') { renderDashboard(mainContent); return; } if (activeTab === '실사 승인') { await renderAuditApprovalView(mainContent); return; } if (activeTab === '위치지정') { // Render Map Editor directly into main content to maximize working area mainContent.innerHTML = `
Map Image
`; // Initialize MapEditor instance const editor = new MapEditor(); await editor.init(); activeMapEditorInstance = editor; return; } // 서버 탭이 아닐 경우에는 state.viewMode가 location이더라도 강제로 목록(list) 뷰를 그리도록 함 // (state.viewMode의 원래 상태는 보존하여, 서버 탭 복귀 시 최근 보던 모드를 유지함) const isServerTab = activeTab === '서버'; const effectiveViewMode = isServerTab ? state.viewMode : 'list'; mainContent.innerHTML = `
`; const viewBody = document.getElementById('view-body')!; if (effectiveViewMode === 'location') { renderLocationView(viewBody); } else { renderSWTable(viewBody); // 리스트 형식 } } // 통합 갱신 (저장은 이미 개별 모달에서 처리됨) async function refreshAllData() { await loadMasterDataFromDB(); refreshView(); } // --- App Initialization --- function initApp() { const mainContent = document.getElementById('main-content')!; if (!mainContent) return; const { closeAllModals } = initBaseModal(); try { renderNavigation((tab) => { refreshView(); }); initHwModal(() => refreshAllData(), closeAllModals); initSwModal(() => refreshAllData(), closeAllModals); initSwUserModal(() => { loadMasterDataFromDB().then(() => refreshView()); }, closeAllModals); initDomainModal(() => refreshAllData(), closeAllModals); initPartsMasterModal(() => refreshAllData(), closeAllModals); initJobSpecModal(() => refreshAllData(), closeAllModals); initUserModal(() => refreshAllData(), closeAllModals); initDashboardDetailModal(); initGuide(); pcFlowModal.init(() => { loadMasterDataFromDB().then(() => refreshView()); }); loadMasterDataFromDB().then((success) => { if (success) { refreshView(); initRoleSwitcher(); // [추가] 역할 전환 토글 초기화 } }); } catch (e) { console.error('❌ Initialization failed:', e); } console.log('🚀 ITAM App Multi-Table Optimized'); // --- 통합 이벤트 위임 (Dynamic Elements 지원) --- document.addEventListener('click', (e) => { const target = e.target as HTMLElement; // 자산 추가 if (target.closest('#btn-add-asset')) { const tab = state.activeSubTab; const cat = state.activeCategory; const newId = Math.random().toString(36).substring(2, 9); if (cat === 'hw') { if (tab === '부품 마스터') { if (activePartsMasterSubTab === 'job-spec') { openJobSpecModal({ id: '' } as any, 'add'); } else { openPartsMasterModal({ id: '' } as any, 'add'); } } else { openHwModal({ id: newId, asset_code: '', category: tab } as any, 'add'); } } else if (cat === 'sw') { const swType = tab === '외부SW' ? '외부SW' : (tab === '내부SW' ? '내부SW' : '외부SW'); openSwModal({ id: newId, asset_type: swType } as any, 'add'); } else if (cat === 'ops') { if (tab === '도메인') openDomainModal(null); else if (tab === '사용자') openUserModal({ id: '' }, 'add'); } return; } // 부품 마스터 탭으로 바로가기 연동 if (target.closest('#btn-goto-parts-master')) { state.activeCategory = 'hw'; state.activeSubTab = '부품 마스터'; renderNavigation((tab) => { refreshView(); }); refreshView(); return; } // PC 이동/반납 모달 열기 if (target.closest('#btn-pc-flow')) { pcFlowModal.open(); return; } }); createIcons({ icons: { Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } }); window.addEventListener('refresh-view', () => refreshView()); } /** * 헤더 역할 전환 토글 로직 */ function initRoleSwitcher() { const checkbox = document.getElementById('role-toggle-checkbox') as HTMLInputElement; const userLabel = document.querySelector('.role-label.user'); const adminLabel = document.querySelector('.role-label.admin'); if (!checkbox || !userLabel || !adminLabel) return; checkbox.addEventListener('change', () => { if (checkbox.checked) { state.currentUserRole = 'admin'; userLabel.classList.remove('active'); adminLabel.classList.add('active'); document.body.classList.add('admin-mode'); // 관리자 모드 전환 시 대시보드로 이동 state.activeCategory = 'hw'; state.activeSubTab = '대시보드'; } else { state.currentUserRole = 'user'; adminLabel.classList.remove('active'); userLabel.classList.add('active'); document.body.classList.remove('admin-mode'); // 실무자 모드 전환 시 서버 목록으로 이동 state.activeCategory = 'hw'; state.activeSubTab = '서버'; } // 모든 렌더링을 refreshView 하나로 통합하여 규격 유지 renderNavigation(() => refreshView()); refreshView(); }); } /** * 앱 초기화 (로그인 과정 없이 즉시 시작) */ function initializeAppDirectly() { const loginContainer = document.getElementById('login-container'); const appLayout = document.getElementById('app-layout'); // 기본 권한 설정: 실무자 (User) state.currentUserRole = 'user'; state.activeCategory = 'hw'; state.activeSubTab = '서버'; // 실무자 기본 탭 // 화면 전환 if (loginContainer) loginContainer.style.display = 'none'; if (appLayout) appLayout.style.display = 'flex'; // 앱 초기화 및 내비게이션(헤더 포함) 렌더링 initApp(); renderNavigation((tab) => refreshView(tab)); } function showLoginScreen(errorMessage?: string) { const loginContainer = document.getElementById('login-container'); const appLayout = document.getElementById('app-layout'); const loginError = document.getElementById('login-error'); const phoneLoginError = document.getElementById('phone-login-error'); const phoneLoginStatus = document.getElementById('phone-login-status'); const loginForm = document.getElementById('login-form') as HTMLFormElement | null; const phoneLoginForm = document.getElementById('phone-login-form') as HTMLFormElement | null; const loginModeTabs = document.querySelectorAll('.login-mode-tab'); if (appLayout) appLayout.style.display = 'none'; if (loginContainer) loginContainer.style.display = 'flex'; const setMessage = (element: HTMLElement | null, message?: string) => { if (!element) return; if (message) { element.textContent = message; element.removeAttribute('hidden'); } else { element.textContent = ''; element.setAttribute('hidden', 'true'); } }; setMessage(loginError, errorMessage); setMessage(phoneLoginError, undefined); setMessage(phoneLoginStatus, undefined); const switchLoginMode = (mode: 'password' | 'phone') => { if (loginForm) loginForm.hidden = mode !== 'password'; if (phoneLoginForm) phoneLoginForm.hidden = mode !== 'phone'; loginModeTabs.forEach((tab) => tab.classList.toggle('active', tab.dataset.mode === mode)); setMessage(loginError, mode === 'password' ? errorMessage : undefined); setMessage(phoneLoginError, mode === 'phone' ? errorMessage : undefined); }; loginModeTabs.forEach((tab) => { if (!tab.dataset.bound) { tab.dataset.bound = 'true'; tab.addEventListener('click', () => switchLoginMode((tab.dataset.mode as 'password' | 'phone') || 'password')); } }); const clearPhonePollTimer = () => { if (phoneLoginPollTimer) { window.clearTimeout(phoneLoginPollTimer); phoneLoginPollTimer = undefined; } }; const pollPhoneLogin = async (pendingRef: string, intervalMs: number) => { clearPhonePollTimer(); phoneLoginPollTimer = window.setTimeout(async () => { try { const response = await fetch('/api/auth/headless/phone/poll', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pendingRef }) }); const payload = await response.json(); if (!response.ok) { const message = payload.redirectTo ? '접근 권한이 없는 테넌트입니다. 관리자에게 문의하세요.' : (payload.error || '전화번호 로그인 확인에 실패했습니다.'); clearPhonePollTimer(); setMessage(phoneLoginStatus, undefined); setMessage(phoneLoginError, message); return; } if (payload.status === 'authenticated') { clearPhonePollTimer(); initializeAppDirectly(); return; } setMessage(phoneLoginStatus, '모바일에서 인증 링크를 승인하는 중입니다. 승인 후 자동으로 로그인됩니다.'); pollPhoneLogin(payload.pendingRef || pendingRef, payload.intervalMs || intervalMs); } catch (error) { console.error('Phone SSO poll failed:', error); clearPhonePollTimer(); setMessage(phoneLoginStatus, undefined); setMessage(phoneLoginError, '전화번호 로그인 확인 중 오류가 발생했습니다.'); } }, intervalMs); }; if (loginForm && !loginForm.dataset.bound) { loginForm.dataset.bound = 'true'; loginForm.addEventListener('submit', async (event) => { event.preventDefault(); const submitButton = document.getElementById('login-submit') as HTMLButtonElement | null; const loginId = (document.getElementById('login-id') as HTMLInputElement | null)?.value.trim() || ''; const password = (document.getElementById('login-password') as HTMLInputElement | null)?.value || ''; if (!loginId || !password) { showLoginScreen('사번과 비밀번호를 입력하세요.'); return; } if (submitButton) { submitButton.disabled = true; submitButton.textContent = '로그인 중...'; } try { const response = await fetch('/api/auth/headless/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ loginId, password }) }); const payload = await response.json(); if (!response.ok) { const message = payload.redirectTo ? '접근 권한이 없는 테넌트입니다. 관리자에게 문의하세요.' : (payload.error || '로그인에 실패했습니다.'); showLoginScreen(message); return; } initializeAppDirectly(); } catch (error) { console.error('SSO login failed:', error); showLoginScreen('로그인 요청 처리 중 오류가 발생했습니다.'); } finally { if (submitButton) { submitButton.disabled = false; submitButton.textContent = '로그인'; } } }); } if (phoneLoginForm && !phoneLoginForm.dataset.bound) { phoneLoginForm.dataset.bound = 'true'; phoneLoginForm.addEventListener('submit', async (event) => { event.preventDefault(); const submitButton = document.getElementById('phone-login-submit') as HTMLButtonElement | null; const loginId = (document.getElementById('phone-login-id') as HTMLInputElement | null)?.value.trim() || ''; if (!loginId) { setMessage(phoneLoginError, '전화번호를 입력하세요.'); return; } clearPhonePollTimer(); setMessage(phoneLoginError, undefined); setMessage(phoneLoginStatus, '인증 링크를 요청하는 중입니다...'); if (submitButton) { submitButton.disabled = true; submitButton.textContent = '링크 전송 중...'; } try { const response = await fetch('/api/auth/headless/phone/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ loginId }) }); const payload = await response.json(); if (!response.ok) { setMessage(phoneLoginStatus, undefined); setMessage(phoneLoginError, payload.error || '전화번호 로그인 시작에 실패했습니다.'); return; } setMessage(phoneLoginStatus, payload.message || '인증 링크를 발송했습니다. 모바일에서 승인해 주세요.'); pollPhoneLogin(payload.pendingRef, payload.intervalMs || 3000); } catch (error) { console.error('Phone SSO init failed:', error); setMessage(phoneLoginStatus, undefined); setMessage(phoneLoginError, '전화번호 로그인 요청 중 오류가 발생했습니다.'); } finally { if (submitButton) { submitButton.disabled = false; submitButton.textContent = '인증 링크 보내기'; } } }); } switchLoginMode('password'); } async function bootstrapApp() { const params = new URLSearchParams(window.location.search); const authError = params.get('auth_error_description') || params.get('auth_error'); try { const response = await fetch('/api/auth/session'); const sessionInfo = await response.json() as AuthSessionResponse; if (response.ok && sessionInfo.authenticated) { initializeAppDirectly(); return; } } catch (error) { console.error('Failed to load auth session:', error); } showLoginScreen(authError || undefined); } document.addEventListener('DOMContentLoaded', bootstrapApp);