1 Commits

Author SHA1 Message Date
fde7ef8439 feat: 서버 리스트 보안 강화 및 위치 정보 포맷팅 개선, 모달 시스템 안정화
주요 변경 사항:
- 리스트 보안 강화: 서버 자산 리스트에서 IP 주소 및 원격접속 컬럼 제거 (상세 모달에서만 노출)
- 보안 배지 가독성 개선: 상세 모달 내 개별 필드 배지를 '네트워크 정보' 섹션 타이틀 옆으로 통합 이동
- 위치 정보 포맷팅: 서버 리스트 내 '서관/동관' 시작 위치에 'IDC' 접두사 자동 추가 (예: IDC(서관 204번))
- 모달 시스템 복구: 이벤트 위임 방식을 통한 전역 ESC 키 및 닫기 버튼 기능 완벽 복구
- 안정성 확보: BaseModal 초기화 로직 보완 및 동적 DOM 요소 대응 강화
2026-04-15 17:52:37 +09:00
5 changed files with 60 additions and 29 deletions

View File

@@ -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 };

View File

@@ -46,7 +46,7 @@ const HW_MODAL_HTML = `
</div>
<!-- Group 2: 네트워크 정보 -->
<div class="form-section-title server-only">네트워크 정보 (Connectivity)</div>
<div class="form-section-title server-only">네트워크 정보 (Connectivity) <span class="badge-security" style="margin-left: 0.5rem;">보안 정보</span></div>
<div class="form-group server-only">
<label for="hw-IP주소">IP 주소 1</label>
<input type="text" id="hw-IP주소" />
@@ -56,7 +56,7 @@ const HW_MODAL_HTML = `
<input type="text" id="hw-IP2" />
</div>
<div class="form-group server-only">
<label for="hw-원격접속">원격 도구 (Anydesk/Chrome 등)</label>
<label for="hw-원격접속">원격 도구</label>
<input type="text" id="hw-원격접속" />
</div>
<div class="form-group server-only">

View File

@@ -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. 전역 버튼 이벤트 바인딩

View File

@@ -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,

View File

@@ -62,7 +62,7 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont
} else {
// 서버 또는 전산비품
if (state.activeSubTab === '서버') {
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>자산번호</th><th>유형</th><th>용도</th><th>상세</th><th>설치위치</th><th>담당자</th><th>IP 주소</th><th>원격접속</th><th>모델명</th><th>OS</th><th>CPU</th><th>RAM</th><th>Storage</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
table.innerHTML = `<thead><tr><th>No</th><th>법인</th><th>자산번호</th><th>유형</th><th>용도</th><th>상세</th><th>설치위치</th><th>담당자</th><th>모델명</th><th>OS</th><th>CPU</th><th>RAM</th><th>Storage</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
} else {
table.innerHTML = `<thead><tr><th>No</th><th>법인</th>${state.activeSubTab === '전산비품' ? '<th>유형</th>' : ''}<th>자산코드</th><th>명칭</th><th>위치</th><th>관리자</th><th>구매일</th><th>금액</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
}
@@ -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 = `<tr><td colspan="${colCount}">등록된 자산이 없습니다.</td></tr>`; 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<maxLen; i++) {
let toolName = tools[i] || '접속';
let badgeColor = '#3B82F6';
if (toolName.toLowerCase().includes('any')) badgeColor = '#EF4444';
if (toolName.toLowerCase().includes('chrome')) badgeColor = '#F59E0B';
let item = `${getBadge(toolName, badgeColor)}`;
if (ids[i] || pws[i]) item += ` (${ids[i] || '-'}/${pws[i] || '-'})`;
remoteItems.push(item);
}
const remoteHtml = remoteItems.join(' / ');
const ipInfo = [asset.IP주소, asset.IP2].filter(v => v && v !== '').join(' / ');
const storageInfo = [asset.SSD1, asset.SSD2].filter(v => v && v !== '').join(' / ');
tr.innerHTML = `<td>${idx+1}</td><td class="text-nowrap">${formatInline(asset.)}</td><td class="text-nowrap">${formatInline(asset.)}</td><td class="text-nowrap">${formatInline(asset.storage유형)}</td><td class="text-nowrap">${formatInline(asset.)}</td><td class="text-nowrap">${formatInline(asset.)}</td><td class="text-nowrap">${formatInline(asset.)}</td><td class="text-nowrap">${managerHtml}</td><td class="text-nowrap">${formatInline(ipInfo)}</td><td class="text-nowrap">${remoteHtml}</td><td class="text-nowrap">${formatInline(asset.)}</td><td class="text-nowrap">${formatInline(asset.OS)}</td><td class="text-nowrap">${formatInline(asset.CPU)}</td><td class="text-nowrap">${formatInline(asset.RAM)}</td><td class="text-nowrap">${formatInline(storageInfo)}</td>`;
// 위치 정보 가공 (서관/동관 -> IDC)
let locationHtml = formatInline(asset.);
if (locationHtml.startsWith('서관') || locationHtml.startsWith('동관')) {
locationHtml = `IDC(${locationHtml})`;
}
tr.innerHTML = `
<td>${idx+1}</td>
<td class="text-nowrap">${formatInline(asset.)}</td>
<td class="text-nowrap">${formatInline(asset.)}</td>
<td class="text-nowrap">${formatInline(asset.storage유형)}</td>
<td class="text-nowrap">${formatInline(asset.)}</td>
<td class="text-nowrap">${formatInline(asset.)}</td>
<td class="text-nowrap">${locationHtml}</td>
<td class="text-nowrap">${managerHtml}</td>
<td class="text-nowrap">${formatInline(asset.)}</td>
<td class="text-nowrap">${formatInline(asset.OS)}</td>
<td class="text-nowrap">${formatInline(asset.CPU)}</td>
<td class="text-nowrap">${formatInline(asset.RAM)}</td>
<td class="text-nowrap">${formatInline(storageInfo)}</td>
`;
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); });
} else {
tr.innerHTML = `<td>${idx+1}</td><td>${asset.}</td>${state.activeSubTab === '전산비품' ? `<td>${asset.||'-'}</td>` : ''}<td>${asset.}</td><td>${asset.}</td><td>${asset.}</td><td>${asset.}</td><td>${asset.||''}</td><td>${asset.||''}</td><td><button class="btn btn-outline btn-sm btn-edit">수정</button></td>`;