Files
ITAM/src/main.ts
SDI 04a53fc899
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 12s
ITAM Docker Build Check / docker-build-check (push) Successful in 33s
BARON-SSO 로그인 연동
2026-06-30 15:35:51 +09:00

475 lines
17 KiB
TypeScript

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 = `
<div class="map-editor-page-wrapper" style="display: flex; flex: 1; height: calc(100vh - var(--header-height) - 48px); overflow: hidden; width: 100%;">
<!-- Left: File Selector -->
<div class="file-sidebar" id="file-sidebar"></div>
<!-- Center: Main Editor -->
<div class="editor-container" id="container">
<div class="img-wrapper" id="wrapper">
<img src="" id="target-img" alt="Map Image">
</div>
</div>
<!-- Right: Control Panel -->
<div class="sidebar">
<h2>Map Editor <small class="editor-version">v3.0</small></h2>
<div class="current-path" id="current-path">파일을 선택하세요</div>
<p>
드래그하여 구역을 정의하세요. 저장 버튼을 누르면 즉시 시스템에 반영됩니다.
</p>
<div class="box-list" id="box-list"></div>
<div class="actions" style="display: flex; flex-direction: column; gap: 0.5rem;">
<button id="btn-clear-all" class="btn btn-outline">전체 삭제</button>
<button id="btn-print-map-qrs" class="btn btn-outline btn-primary">이 도면 QR 일괄인쇄</button>
<button id="btn-save-server" class="btn btn-primary">서버에 즉시 저장</button>
<div id="save-status"></div>
</div>
</div>
</div>
`;
// 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 = `
<div id="view-body" class="view-container"></div>
`;
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<HTMLButtonElement>('.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);