From fde7ef8439b388810a21092346e61867992242b4 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 15 Apr 2026 17:52:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EC=95=88=20=EA=B0=95=ED=99=94=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EC=A0=95=EB=B3=B4=20=ED=8F=AC=EB=A7=B7?= =?UTF-8?q?=ED=8C=85=20=EA=B0=9C=EC=84=A0,=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=95=88=EC=A0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주요 변경 사항: - 리스트 보안 강화: 서버 자산 리스트에서 IP 주소 및 원격접속 컬럼 제거 (상세 모달에서만 노출) - 보안 배지 가독성 개선: 상세 모달 내 개별 필드 배지를 '네트워크 정보' 섹션 타이틀 옆으로 통합 이동 - 위치 정보 포맷팅: 서버 리스트 내 '서관/동관' 시작 위치에 'IDC' 접두사 자동 추가 (예: IDC(서관 204번)) - 모달 시스템 복구: 이벤트 위임 방식을 통한 전역 ESC 키 및 닫기 버튼 기능 완벽 복구 - 안정성 확보: BaseModal 초기화 로직 보완 및 동적 DOM 요소 대응 강화 --- src/components/Modal/BaseModal.ts | 15 +++++++++-- src/components/Modal/HWModal.ts | 4 +-- src/main.ts | 15 ++++++----- src/styles/modal.css | 13 ++++++++++ src/views/AssetTableView.ts | 42 +++++++++++++++++-------------- 5 files changed, 60 insertions(+), 29 deletions(-) diff --git a/src/components/Modal/BaseModal.ts b/src/components/Modal/BaseModal.ts index 7e1c518..e8c895d 100644 --- a/src/components/Modal/BaseModal.ts +++ b/src/components/Modal/BaseModal.ts @@ -4,7 +4,9 @@ export function initBaseModal() { const closeAllModals = () => { const modals = document.querySelectorAll('.modal-overlay'); - modals.forEach(modal => modal.classList.add('hidden')); + modals.forEach(modal => { + modal.classList.add('hidden'); + }); }; // ESC 키로 닫기 @@ -12,12 +14,21 @@ export function initBaseModal() { if (e.key === 'Escape') closeAllModals(); }); - // 배경(Overlay) 클릭 시 닫기 (동적 생성된 모달 대응을 위해 이벤트 위임 고려 가능하나 일단 단순 구현) + // 배경(Overlay) 및 닫기 버튼 클릭 시 닫기 (이벤트 위임) document.addEventListener('click', (e) => { const target = e.target as HTMLElement; + + // 1. 오버레이 클릭 시 닫기 if (target.classList.contains('modal-overlay')) { closeAllModals(); } + + // 2. 닫기 아이콘(data-lucide="x") 또는 닫기/취소 버튼 클릭 시 닫기 + // 버튼 ID가 btn-close- 또는 btn-cancel-로 시작하는 경우 대응 + const btn = target.closest('button'); + if (btn && (btn.id.startsWith('btn-close-') || btn.id.startsWith('btn-cancel-'))) { + closeAllModals(); + } }); return { closeAllModals }; diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index a330711..9a9fce4 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -46,7 +46,7 @@ const HW_MODAL_HTML = ` -
네트워크 정보 (Connectivity)
+
네트워크 정보 (Connectivity) 보안 정보
@@ -56,7 +56,7 @@ const HW_MODAL_HTML = `
- +
diff --git a/src/main.ts b/src/main.ts index 98fd064..40dc02a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { renderSidebar } from './components/Sidebar'; import { renderDashboard } from './views/DashboardView'; import { renderTable } from './views/AssetTableView'; import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset } from './core/excelHandler'; +import { initBaseModal } from './components/Modal/BaseModal'; import { initPcModal } from './components/Modal/PCModal'; import { initHwModal, openHwModal } from './components/Modal/HWModal'; import { initStorageModal } from './components/Modal/StorageModal'; @@ -36,12 +37,14 @@ function initApp() { } }); - // 3. 모달 초기화 (HTML 주입 및 이벤트 바인딩) - initPcModal(() => renderTable(mainContent), () => {}); - initHwModal(); - initStorageModal(() => renderTable(mainContent), () => {}); - initSwModal(() => renderTable(mainContent), () => {}); - initSwUserModal(() => renderTable(mainContent), () => {}); + // 3. 모달 초기화 (HTML 주입 및 개별 로직 바인딩) + const { closeAllModals } = initBaseModal(); + + initPcModal(() => renderTable(mainContent), closeAllModals); + initHwModal(); // HW 모달은 내부에서 자체 닫기 로직 포함 중이나 추후 통일 가능 + initStorageModal(() => renderTable(mainContent), closeAllModals); + initSwModal(() => renderTable(mainContent), closeAllModals); + initSwUserModal(() => renderTable(mainContent), closeAllModals); initDashboardDetailModal(); // 4. 전역 버튼 이벤트 바인딩 diff --git a/src/styles/modal.css b/src/styles/modal.css index 25ed49b..cd3fb31 100644 --- a/src/styles/modal.css +++ b/src/styles/modal.css @@ -126,6 +126,19 @@ font-size: 0.8125rem; font-weight: 600; color: var(--text-muted); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.badge-security { + font-size: 10px; + background-color: #fef2f2; + color: #ef4444; + padding: 1px 4px; + border-radius: 3px; + border: 1px solid #fee2e2; + font-weight: 700; } .form-group input, diff --git a/src/views/AssetTableView.ts b/src/views/AssetTableView.ts index 961d38f..5a91d63 100644 --- a/src/views/AssetTableView.ts +++ b/src/views/AssetTableView.ts @@ -62,7 +62,7 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont } else { // 서버 또는 전산비품 if (state.activeSubTab === '서버') { - table.innerHTML = `No법인자산번호유형용도상세설치위치담당자IP 주소원격접속모델명OSCPURAMStorage`; + table.innerHTML = `No법인자산번호유형용도상세설치위치담당자모델명OSCPURAMStorage`; } else { table.innerHTML = `No법인${state.activeSubTab === '전산비품' ? '유형' : ''}자산코드명칭위치관리자구매일금액관리`; } @@ -71,7 +71,7 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont container.appendChild(tableWrapper); mainContent.appendChild(container); const tbody = document.getElementById('dynamic-tbody')!; - const colCount = state.activeSubTab === '서버' ? 15 : (state.activeSubTab === '전산비품' ? 11 : 10); + const colCount = state.activeSubTab === '서버' ? 13 : (state.activeSubTab === '전산비품' ? 11 : 10); if (list.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } list.forEach((asset, idx) => { @@ -84,25 +84,29 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont const mainManager = asset.담당자_정 || ''; const subManager = asset.담당자_부 || ''; const managerHtml = [mainManager ? `${getBadge('정', '#1E5149')} ${mainManager}` : '', subManager ? `${getBadge('부', '#9CA3AF')} ${subManager}` : ''].filter(v => v !== '').join(' / '); - const tools = (asset.원격접속 || '').split('\n'); - const ids = (asset.서버ID || '').split('\n'); - const pws = (asset.서버PW || '').split('\n'); - const maxLen = Math.max(tools.length, ids.length, pws.length); - let remoteItems = []; - for(let i=0; i v && v !== '').join(' / '); const storageInfo = [asset.SSD1, asset.SSD2].filter(v => v && v !== '').join(' / '); - tr.innerHTML = `${idx+1}${formatInline(asset.법인)}${formatInline(asset.자산코드)}${formatInline(asset.storage유형)}${formatInline(asset.용도)}${formatInline(asset.상세)}${formatInline(asset.위치)}${managerHtml}${formatInline(ipInfo)}${remoteHtml}${formatInline(asset.모델명)}${formatInline(asset.OS)}${formatInline(asset.CPU)}${formatInline(asset.RAM)}${formatInline(storageInfo)}`; + // 위치 정보 가공 (서관/동관 -> IDC) + let locationHtml = formatInline(asset.위치); + if (locationHtml.startsWith('서관') || locationHtml.startsWith('동관')) { + locationHtml = `IDC(${locationHtml})`; + } + + tr.innerHTML = ` + ${idx+1} + ${formatInline(asset.법인)} + ${formatInline(asset.자산코드)} + ${formatInline(asset.storage유형)} + ${formatInline(asset.용도)} + ${formatInline(asset.상세)} + ${locationHtml} + ${managerHtml} + ${formatInline(asset.모델명)} + ${formatInline(asset.OS)} + ${formatInline(asset.CPU)} + ${formatInline(asset.RAM)} + ${formatInline(storageInfo)} + `; tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); }); } else { tr.innerHTML = `${idx+1}${asset.법인}${state.activeSubTab === '전산비품' ? `${asset.비품유형||'-'}` : ''}${asset.자산코드}${asset.명칭}${asset.위치}${asset.관리자}${asset.구매일||''}${asset.금액||''}`;