From fde7ef8439b388810a21092346e61867992242b4 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 15 Apr 2026 17:52:37 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EC=95=88=20=EA=B0=95=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9C=84=EC=B9=98=20=EC=A0=95=EB=B3=B4=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85=20=EA=B0=9C=EC=84=A0,=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=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.금액||''}`; From 7158689fd00917506c05ce812ff9ee60d2027a5a Mon Sep 17 00:00:00 2001 From: Taehoon Date: Thu, 16 Apr 2026 18:07:19 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EB=AA=A8=EB=8B=AC=20=EA=B5=AC=EB=A7=A4=EC=9D=BC?= =?UTF-8?q?=EC=9E=90=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8A=B9=EC=A0=95=20=EB=B2=95=EC=9D=B8=EB=AA=85=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/HWModal.ts | 6 ++ src/core/realServerData.ts | 140 ++++++++++++++-------------- src/views/AssetTableView.ts | 160 ++++++++++++++++++++++++++------ 3 files changed, 209 insertions(+), 97 deletions(-) diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 9a9fce4..511b007 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -28,6 +28,10 @@ const HW_MODAL_HTML = `
+
+ + +
@@ -189,6 +193,7 @@ function fillHwFormData(asset: HardwareAsset) { (document.getElementById('hw-RAM') as HTMLInputElement).value = asset.RAM || ''; (document.getElementById('hw-SSD1') as HTMLInputElement).value = asset.SSD1 || ''; (document.getElementById('hw-SSD2') as HTMLInputElement).value = asset.SSD2 || ''; + (document.getElementById('hw-구매일') as HTMLInputElement).value = asset.구매일 || ''; (document.getElementById('hw-담당자_정') as HTMLInputElement).value = asset.담당자_정 || asset.관리자 || ''; (document.getElementById('hw-담당자_부') as HTMLInputElement).value = asset.담당자_부 || ''; (document.getElementById('hw-품의서명') as HTMLElement).textContent = asset.품의서명 || ''; @@ -289,6 +294,7 @@ export function initHwModal() { RAM: (document.getElementById('hw-RAM') as HTMLInputElement).value, SSD1: (document.getElementById('hw-SSD1') as HTMLInputElement).value, SSD2: (document.getElementById('hw-SSD2') as HTMLInputElement).value, + 구매일: (document.getElementById('hw-구매일') as HTMLInputElement).value, 담당자_정: (document.getElementById('hw-담당자_정') as HTMLInputElement).value, 관리자: (document.getElementById('hw-담당자_정') as HTMLInputElement).value, 담당자_부: (document.getElementById('hw-담당자_부') as HTMLInputElement).value, diff --git a/src/core/realServerData.ts b/src/core/realServerData.ts index ead448d..c071f14 100644 --- a/src/core/realServerData.ts +++ b/src/core/realServerData.ts @@ -609,7 +609,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "NAS", "용도": "GSIM NAS", @@ -629,7 +629,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "NAS", "용도": "그래픽스개발팀 데이터 백업 NAS", @@ -649,7 +649,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "공통 GIT 서버", @@ -669,7 +669,7 @@ export const realServerData = [ "SSD2": "1TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "BUILD 서버", @@ -689,7 +689,7 @@ export const realServerData = [ "SSD2": "10TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "HmEG 테스트 서버", @@ -709,7 +709,7 @@ export const realServerData = [ "SSD2": "1TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "산하 ERP 개발서버", @@ -729,7 +729,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "공간정보 신청", @@ -749,7 +749,7 @@ export const realServerData = [ "SSD2": "931GB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "AI 관련", @@ -769,7 +769,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "한종 테스트", @@ -789,7 +789,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "GSIM 언리얼 서버", @@ -809,7 +809,7 @@ export const realServerData = [ "SSD2": "8TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "AutoCAD 테스트 서버", @@ -829,7 +829,7 @@ export const realServerData = [ "SSD2": "2TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "GSIM 테스트 서버", @@ -849,7 +849,7 @@ export const realServerData = [ "SSD2": "512GB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "공간데이터 서버", @@ -869,7 +869,7 @@ export const realServerData = [ "SSD2": "8 TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "PC", "용도": "가평 VM 원격 서버", @@ -889,7 +889,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "서버", "용도": "GSIM 협업", @@ -909,7 +909,7 @@ export const realServerData = [ "SSD2": "1.88TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "스토리지", "용도": "GSIM 협업 스토리지", @@ -929,7 +929,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "서버", "용도": "GSIM META 서버", @@ -949,7 +949,7 @@ export const realServerData = [ "SSD2": "4TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "서버", "용도": "GSIM 서버", @@ -969,7 +969,7 @@ export const realServerData = [ "SSD2": "4TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "스토리지", "용도": "GSIM 스토리지", @@ -989,7 +989,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "서버", "용도": "함양-합천 서버", @@ -1009,7 +1009,7 @@ export const realServerData = [ "SSD2": "10TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "서버", "용도": "HM MapService 2.0 서버", @@ -1029,7 +1029,7 @@ export const realServerData = [ "SSD2": "40 TB" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "스토리지", "용도": "HM MapService 2.0 스토리지", @@ -1049,7 +1049,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "서버", "용도": "Gitlab Runner", @@ -1069,7 +1069,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "기술개발센터", + "법인": "", "자산코드": "", "storage유형": "서버", "용도": "전산모사", @@ -1089,7 +1089,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "1", "storage유형": "NAS", "용도": "NAS 2", @@ -1105,7 +1105,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "2", "storage유형": "NAS", "용도": "NAS 1", @@ -1121,7 +1121,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "3", "storage유형": "NAS", "용도": "NAS 4", @@ -1137,7 +1137,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "4", "storage유형": "NAS", "용도": "NAS 5", @@ -1153,7 +1153,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "5", "storage유형": "NAS", "용도": "NAS 6", @@ -1169,7 +1169,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "6", "storage유형": "NAS", "용도": "NAS7", @@ -1185,7 +1185,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "7", "storage유형": "NAS", "용도": "총괄기획실 NAS", @@ -1201,7 +1201,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "8", "storage유형": "NAS", "용도": "한맥 NAS 1", @@ -1217,7 +1217,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "9", "storage유형": "NAS", "용도": "한맥 NAS 2", @@ -1233,7 +1233,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "10", "storage유형": "NAS", "용도": "한맥 NAS 3", @@ -1249,7 +1249,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "11", "storage유형": "NAS", "용도": "NAS 13", @@ -1265,7 +1265,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "12", "storage유형": "PC", "용도": "회계", @@ -1281,7 +1281,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "13", "storage유형": "PC", "용도": "한맥CAD", @@ -1297,9 +1297,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "14", - "storage유형": "서버(타워)", + "storage유형": "PC", "용도": "Ai-Cell-Util", "상세": "깃티, 매터모스트 등 70여종", "위치": "한맥빌딩(MDF 실)", @@ -1313,7 +1313,7 @@ export const realServerData = [ "SSD2": "8 TB" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "15", "storage유형": "PC", "용도": "한라CAD", @@ -1329,7 +1329,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "16", "storage유형": "NAS", "용도": "디자인팀1 NAS", @@ -1345,7 +1345,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "17", "storage유형": "NAS", "용도": "디자인팀2 NAS", @@ -1361,9 +1361,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "18", - "storage유형": "서버(미니워크스테이션)", + "storage유형": "PC", "용도": "인사정보 서버", "상세": "인사정보 PM", "위치": "한맥빌딩(MDF 실)", @@ -1377,9 +1377,9 @@ export const realServerData = [ "SSD2": "2 TB" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "19", - "storage유형": "서버(타워)", + "storage유형": "PC", "용도": "BEPs 서버", "상세": "BEPs 개발서버, Outline 협업서비스", "위치": "한맥빌딩(MDF 실)", @@ -1393,9 +1393,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "20", - "storage유형": "서버(타워)", + "storage유형": "PC", "용도": "Ai-Cell-A100-1", "상세": "OCR, Local LLM 등 30여종", "위치": "한맥빌딩(MDF 실)", @@ -1409,9 +1409,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "21", - "storage유형": "서버(타워)", + "storage유형": "PC", "용도": "빌드서버", "상세": "인스톨 쉴드, 지라", "위치": "한맥빌딩(MDF 실)", @@ -1425,9 +1425,9 @@ export const realServerData = [ "SSD2": "4TB" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "22", - "storage유형": "PC\n서버(랙)", + "storage유형": "PC", "용도": "저장소 및 전산모사\n구)스마트건설 서버", "상세": "ParaView, CFDCore\n디지털화설문, 검색WIKI 웹서비스", "위치": "한맥빌딩(MDF 실)", @@ -1441,9 +1441,9 @@ export const realServerData = [ "SSD2": "2TB" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "23", - "storage유형": "서버(랙)", + "storage유형": "서버", "용도": "IDC 산하ERP서버", "상세": "XR 가상화 메인 서버 → IDC 산하ERP서버", "위치": "한맥빌딩(MDF 실)", @@ -1457,9 +1457,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "24", - "storage유형": "스토리지(랙)", + "storage유형": "스토리지", "용도": "WAS Storage", "상세": "", "위치": "한맥빌딩(MDF 실)", @@ -1473,9 +1473,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "25", - "storage유형": "서버(랙)", + "storage유형": "서버", "용도": "한맥 백업 서버", "상세": "가족사 인트라넷 소스 백업 서버", "위치": "한맥빌딩(MDF 실)", @@ -1489,9 +1489,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "26", - "storage유형": "서버(랙)", + "storage유형": "서버", "용도": "한라 백업 서버", "상세": "한라 웹 소스 및 Miso DB 백업 서버", "위치": "한맥빌딩(MDF 실)", @@ -1505,7 +1505,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "27", "storage유형": "NAS", "용도": "기술개발센터 NAS", @@ -1521,7 +1521,7 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "28", "storage유형": "NAS", "용도": "-", @@ -1537,9 +1537,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "29", - "storage유형": "스토리지(랙)", + "storage유형": "스토리지", "용도": "Backup Storage", "상세": "", "위치": "한맥빌딩(MDF 실)", @@ -1553,9 +1553,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "30", - "storage유형": "스토리지(랙)", + "storage유형": "스토리지", "용도": "-", "상세": "", "위치": "한맥빌딩(MDF 실)", @@ -1569,9 +1569,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "31", - "storage유형": "서버(랙)", + "storage유형": "서버", "용도": "XR WAS Server", "상세": "", "위치": "한맥빌딩(MDF 실)", @@ -1585,9 +1585,9 @@ export const realServerData = [ "SSD2": "" }, { - "법인": "한맥빌딩", + "법인": "", "자산코드": "32", - "storage유형": "서버(랙)", + "storage유형": "서버", "용도": "WAS Storage", "상세": "", "위치": "한맥빌딩(MDF 실)", diff --git a/src/views/AssetTableView.ts b/src/views/AssetTableView.ts index 5a91d63..11cb056 100644 --- a/src/views/AssetTableView.ts +++ b/src/views/AssetTableView.ts @@ -27,7 +27,7 @@ export function renderTable(mainContent: HTMLElement) { } function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) { - const list = state.masterData.hw.filter(a => a.type === state.activeSubTab); + const fullList = state.masterData.hw.filter(a => a.type === state.activeSubTab); const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; @@ -37,8 +37,8 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont container.appendChild(tableWrapper); mainContent.appendChild(container); const tbody = document.getElementById('dynamic-tbody')!; - if (list.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } - list.forEach((asset, idx) => { + if (fullList.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } + fullList.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; tr.innerHTML = `${idx+1}${asset.법인}${asset.자산코드}${asset.사용자||''}${asset.위치||''}${asset.CPU||''}${asset.GPU||''}${asset.RAM||''}${asset.SSD1||'-'}${asset.SSD2||'-'}${asset.HDD1||'-'}${asset.HDD2||'-'}${asset.구매일||''}${asset.금액||''}${asset.납품업체||''}${asset.품의서명 ? '' : '-'}`; @@ -51,42 +51,116 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont container.appendChild(tableWrapper); mainContent.appendChild(container); const tbody = document.getElementById('dynamic-tbody')!; - if (list.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } - list.forEach((asset, idx) => { + if (fullList.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } + fullList.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; tr.innerHTML = `${idx+1}${asset.법인}${asset.storage유형||''}${asset.자산코드}${asset.명칭}${asset.위치||''}${asset.모델명||''}${asset.용량||''}${asset.담당자_정||''}${asset.IP주소||''}${asset.구매일||''}${asset.금액||''}`; tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); }); tbody.appendChild(tr); }); - } else { - // 서버 또는 전산비품 - if (state.activeSubTab === '서버') { - table.innerHTML = `No법인자산번호유형용도상세설치위치담당자모델명OSCPURAMStorage`; - } else { - table.innerHTML = `No법인${state.activeSubTab === '전산비품' ? '유형' : ''}자산코드명칭위치관리자구매일금액관리`; - } + } else if (state.activeSubTab === '서버') { + // --- 서버 전용 필터 및 검색 기능 --- + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + // 법인, 유형, 위치 고유값 추출 + const corps = Array.from(new Set(fullList.map(a => a.법인))).filter(Boolean).sort(); + const types = Array.from(new Set(fullList.map(a => a.storage유형))).filter(Boolean).sort(); + const locations = Array.from(new Set(fullList.map(a => { + const loc = String(a.위치 || ''); + if (loc.startsWith('서관') || loc.startsWith('동관')) return 'IDC'; + return loc.split(' ')[0]; // 첫 단어 기준 (본사, 지사, 마천사무실 등) + }))).filter(Boolean).sort(); + + filterBar.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + `; + container.appendChild(filterBar); + + table.innerHTML = `No법인자산번호유형용도상세설치위치담당자모델명OSCPURAMStorage`; tableWrapper.appendChild(table); container.appendChild(tableWrapper); mainContent.appendChild(container); - const tbody = document.getElementById('dynamic-tbody')!; - const colCount = state.activeSubTab === '서버' ? 13 : (state.activeSubTab === '전산비품' ? 11 : 10); - if (list.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } - - list.forEach((asset, idx) => { - const tr = document.createElement('tr'); - tr.style.cursor = 'pointer'; - const formatInline = (v: any) => String(v || '').replace(/\n/g, ' / ').trim(); - const getBadge = (text: string, bgColor: string) => `${text}`; - if (state.activeSubTab === '서버') { + const tbody = document.getElementById('dynamic-tbody')!; + + const updateTable = () => { + const keyword = (document.getElementById('filter-keyword') as HTMLInputElement).value.toLowerCase().trim(); + const corp = (document.getElementById('filter-corp') as HTMLSelectElement).value; + const type = (document.getElementById('filter-type') as HTMLSelectElement).value; + const location = (document.getElementById('filter-location') as HTMLSelectElement).value; + + const filtered = fullList.filter(asset => { + const formatAsset = (v: any) => String(v || '').toLowerCase(); + const matchKeyword = !keyword || + formatAsset(asset.자산코드).includes(keyword) || + formatAsset(asset.용도).includes(keyword) || + formatAsset(asset.상세).includes(keyword) || + formatAsset(asset.모델명).includes(keyword) || + formatAsset(asset.담당자_정).includes(keyword) || + formatAsset(asset.담당자_부).includes(keyword); + + const matchCorp = !corp || asset.법인 === corp; + const matchType = !type || asset.storage유형 === type; + + let matchLocation = true; + if (location) { + const loc = String(asset.위치 || ''); + if (location === 'IDC') { + matchLocation = loc.startsWith('서관') || loc.startsWith('동관'); + } else { + matchLocation = loc.includes(location); + } + } + + return matchKeyword && matchCorp && matchType && matchLocation; + }); + + tbody.innerHTML = ''; + if (filtered.length === 0) { + tbody.innerHTML = `검색 결과가 없습니다.`; + return; + } + + filtered.forEach((asset, idx) => { + const tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + const formatInline = (v: any) => String(v || '').replace(/\n/g, ' / ').trim(); + const getBadge = (text: string, bgColor: string) => `${text}`; + const mainManager = asset.담당자_정 || ''; const subManager = asset.담당자_부 || ''; const managerHtml = [mainManager ? `${getBadge('정', '#1E5149')} ${mainManager}` : '', subManager ? `${getBadge('부', '#9CA3AF')} ${subManager}` : ''].filter(v => v !== '').join(' / '); const storageInfo = [asset.SSD1, asset.SSD2].filter(v => v && v !== '').join(' / '); - // 위치 정보 가공 (서관/동관 -> IDC) let locationHtml = formatInline(asset.위치); if (locationHtml.startsWith('서관') || locationHtml.startsWith('동관')) { locationHtml = `IDC(${locationHtml})`; @@ -108,10 +182,42 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont ${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.금액||''}`; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); }); - } + tbody.appendChild(tr); + }); + createIcons({ icons: { RefreshCcw, Paperclip } }); + }; + + const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; + const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; + const typeSelect = document.getElementById('filter-type') as HTMLSelectElement; + const locationSelect = document.getElementById('filter-location') as HTMLSelectElement; + const resetBtn = document.getElementById('btn-reset-filters') as HTMLButtonElement; + + keywordInput.addEventListener('input', updateTable); + corpSelect.addEventListener('change', updateTable); + typeSelect.addEventListener('change', updateTable); + locationSelect.addEventListener('change', updateTable); + resetBtn.addEventListener('click', () => { + keywordInput.value = ''; corpSelect.value = ''; typeSelect.value = ''; locationSelect.value = ''; + updateTable(); + }); + + updateTable(); + } else { + // 전산비품 + table.innerHTML = `No법인${state.activeSubTab === '전산비품' ? '유형' : ''}자산코드명칭위치관리자구매일금액관리`; + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); + mainContent.appendChild(container); + const tbody = document.getElementById('dynamic-tbody')!; + const colCount = state.activeSubTab === '전산비품' ? 11 : 10; + if (fullList.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } + + fullList.forEach((asset, idx) => { + const tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + tr.innerHTML = `${idx+1}${asset.법인}${state.activeSubTab === '전산비품' ? `${asset.비품유형||'-'}` : ''}${asset.자산코드}${asset.명칭}${asset.위치}${asset.관리자}${asset.구매일||''}${asset.금액||''}`; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); }); tbody.appendChild(tr); }); } From 54bfb9d48218e543964f90fb72279fff65e9bbbb Mon Sep 17 00:00:00 2001 From: Taehoon Date: Fri, 17 Apr 2026 10:34:32 +0900 Subject: [PATCH 03/14] feat: update server asset details, ui labels, and excel mapping logic --- src/components/Modal/HWModal.ts | 14 +++++++++++- src/core/excelHandler.ts | 39 +++++++++++++++++++++++---------- src/views/AssetTableView.ts | 30 +++++++++++++++++-------- 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 511b007..2ca50d1 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -21,7 +21,7 @@ const HW_MODAL_HTML = `
기본 정보 (Identity)
- +
@@ -32,6 +32,14 @@ const HW_MODAL_HTML = `
+
+ + +
+
+ + +
@@ -187,6 +195,8 @@ function fillHwFormData(asset: HardwareAsset) { (document.getElementById('hw-법인') as HTMLInputElement).value = asset.법인; (document.getElementById('hw-자산코드') as HTMLInputElement).value = asset.자산코드; (document.getElementById('hw-위치') as HTMLInputElement).value = asset.위치; + (document.getElementById('hw-현사용조직') as HTMLInputElement).value = asset.현사용조직 || ''; + (document.getElementById('hw-이전사용조직') as HTMLInputElement).value = asset.이전사용조직 || ''; (document.getElementById('hw-모델명') as HTMLInputElement).value = asset.모델명 || ''; (document.getElementById('hw-OS') as HTMLInputElement).value = asset.OS || ''; (document.getElementById('hw-CPU') as HTMLInputElement).value = asset.CPU || ''; @@ -288,6 +298,8 @@ export function initHwModal() { 법인: (document.getElementById('hw-법인') as HTMLInputElement).value, 자산코드: (document.getElementById('hw-자산코드') as HTMLInputElement).value, 위치: (document.getElementById('hw-위치') as HTMLInputElement).value, + 현사용조직: (document.getElementById('hw-현사용조직') as HTMLInputElement).value, + 이전사용조직: (document.getElementById('hw-이전사용조직') as HTMLInputElement).value, 모델명: (document.getElementById('hw-모델명') as HTMLInputElement).value, OS: (document.getElementById('hw-OS') as HTMLInputElement).value, CPU: (document.getElementById('hw-CPU') as HTMLInputElement).value, diff --git a/src/core/excelHandler.ts b/src/core/excelHandler.ts index 967b14f..af5ac68 100644 --- a/src/core/excelHandler.ts +++ b/src/core/excelHandler.ts @@ -38,6 +38,8 @@ export interface HardwareAsset { 서버PW?: string; 모니터링?: string; 비고?: string; + 현사용조직?: string; + 이전사용조직?: string; } @@ -90,7 +92,7 @@ const SW_TABS = ['구독SW', '영구SW']; const HW_HEADERS = ['법인', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명']; const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', '구매일', '금액', '납품업체', '품의서명']; -const SERVER_HEADERS = ['법인', '자산번호', '유형', '용도', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소', '원격접속', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage1', 'Storage2', 'Storage3', '모니터링', '비고']; +const SERVER_HEADERS = ['구매법인', '자산번호', '구매일자', '용도', '상세내용', '현 사용조직', '이전 사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소 1', 'IP 주소 2', '원격도구', '서버 ID', '서버 PW', '모델명', 'OS', 'CPU', 'RAM', 'SSD1', 'SSD2', '모니터링', '비고']; const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명']; const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고']; const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고']; @@ -112,7 +114,7 @@ export function downloadTemplate() { wscols = [{wch:15}, {wch:25}, {wch:15}, {wch:20}, {wch:20}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}]; } else if (tab === '서버') { hd = SERVER_HEADERS; - wscols = [{wch:15}, {wch:20}, {wch:15}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:30}]; + wscols = [{wch:15}, {wch:20}, {wch:15}, {wch:25}, {wch:30}, {wch:20}, {wch:20}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:20}, {wch:20}, {wch:20}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:30}]; } else if (tab === '스토리지') { hd = STORAGE_HEADERS; wscols = [{wch:15}, {wch:15}, {wch:25}, {wch:25}, {wch:20}, {wch:25}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:25}]; @@ -164,9 +166,13 @@ export function exportToExcel(masterData: MasterAssetData) { } else if (tab === '서버') { wsData = [ SERVER_HEADERS, - ...targetAssets.map(a => [a.법인, a.자산코드, a.storage유형 || '물리', a.용도 || '', a.위치, a.담당자_정 || '', a.담당자_부 || '', a.IP주소, a.원격접속 || '', a.모델명 || '', a.OS, a.CPU, a.RAM, a.GPU || '', a.SSD1 || '', a.SSD2 || '', a.HDD1 || '', a.모니터링 || '', a.비고 || '']) + ...targetAssets.map(a => [ + a.법인, a.자산코드, a.구매일 || '', a.용도 || '', a.상세 || '', a.현사용조직 || '', a.이전사용조직 || '', + a.위치, a.담당자_정 || '', a.담당자_부 || '', a.IP주소, (a as any).IP2 || '', a.원격접속 || '', + (a as any).서버ID || '', (a as any).서버PW || '', a.모델명 || '', a.OS, a.CPU, a.RAM, a.SSD1 || '', a.SSD2 || '', a.모니터링 || '', a.비고 || '' + ]) ]; - colsConfig = [{wch:15}, {wch:20}, {wch:15}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:30}]; + colsConfig = [{wch:15}, {wch:20}, {wch:15}, {wch:25}, {wch:30}, {wch:20}, {wch:20}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:20}, {wch:20}, {wch:20}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:30}]; } else if (tab === '스토리지') { wsData = [ STORAGE_HEADERS, @@ -262,18 +268,27 @@ export async function parseExcel(file: File): Promise { hwAssets.push({ id: Math.random().toString(36).substring(2, 9), type: sheetName, - 법인: row['법인'] || '', + 법인: row['구매법인'] || row['법인'] || '', 자산코드: row['자산번호'] || row['자산코드'] || '', 명칭: row['용도'] || row['명칭'] || '', - 용도: row['용도'] || '', 위치: row['설치위치'] || row['위치'] || '', + 구매일: row['구매일자'] || row['구매일'] || '', + 용도: row['용도'] || '', + 상세: row['상세내용'] || row['상세'] || '', + 현사용조직: row['현 사용조직'] || '', + 이전사용조직: row['이전 사용조직'] || '', + 위치: row['설치위치'] || row['위치'] || '', 관리자: row['담당자(정)'] || '', 담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '', - IP주소: row['IP 주소'] || row['IP주소'] || '', IP2: row['IP2'] || '', - 원격접속: row['원격접속'] || '', 서버ID: row['서버ID'] || '', 서버PW: row['서버PW'] || '', + IP주소: row['IP 주소 1'] || row['IP 주소'] || row['IP주소'] || '', + IP2: row['IP 주소 2'] || row['IP2'] || '', + 원격접속: row['원격도구'] || row['원격접속'] || '', + 서버ID: row['서버 ID'] || row['서버ID'] || '', + 서버PW: row['서버 PW'] || row['서버PW'] || '', 모델명: row['모델명'] || '', OS: row['OS'] || '', - CPU: row['CPU'] || '', RAM: row['RAM'] || '', GPU: row['GPU'] || '', - SSD1: row['Storage1'] || row['SSD1'] || '', SSD2: row['Storage2'] || row['SSD2'] || '', HDD1: row['Storage3'] || row['HDD1'] || '', - 모니터링: row['모니터링'] || '', 비고: row['비고'] || '', storage유형: row['유형'] || '물리', - MACaddress: '', HW사양: '', 구매일: '', 금액: '', 납품업체: '', 품의서명: '', + CPU: row['CPU'] || '', RAM: row['RAM'] || '', + SSD1: row['SSD1'] || row['Storage1'] || '', + SSD2: row['SSD2'] || row['Storage2'] || '', + 모니터링: row['모니터링'] || '', 비고: row['비고'] || '', storage유형: '물리', + MACaddress: '', HW사양: '', 금액: '', 납품업체: '', 품의서명: '', }); } else if (sheetName === '스토리지') { hwAssets.push({ diff --git a/src/views/AssetTableView.ts b/src/views/AssetTableView.ts index 11cb056..5ac1e9e 100644 --- a/src/views/AssetTableView.ts +++ b/src/views/AssetTableView.ts @@ -32,7 +32,7 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont tableWrapper.className = 'table-container'; if (state.activeSubTab === '개인PC') { - table.innerHTML = `No법인자산코드사용자위치CPUGPURAMSSD1SSD2HDD1HDD2구매일금액납품업체품의서관리`; + table.innerHTML = `No구매법인자산코드사용자위치CPUGPURAMSSD1SSD2HDD1HDD2구매일금액납품업체품의서관리`; tableWrapper.appendChild(table); container.appendChild(tableWrapper); mainContent.appendChild(container); @@ -46,7 +46,7 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont tbody.appendChild(tr); }); } else if (state.activeSubTab === '스토리지') { - table.innerHTML = `No법인유형자산코드명칭위치모델명용량담당자(정)IP주소구매일금액관리`; + table.innerHTML = `No구매법인유형자산코드명칭위치모델명용량담당자(정)IP주소구매일금액관리`; tableWrapper.appendChild(table); container.appendChild(tableWrapper); mainContent.appendChild(container); @@ -64,8 +64,9 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - // 법인, 유형, 위치 고유값 추출 + // 법인, 조직, 유형, 위치 고유값 추출 const corps = Array.from(new Set(fullList.map(a => a.법인))).filter(Boolean).sort(); + const orgUnits = Array.from(new Set(fullList.map(a => a.현사용조직))).filter(Boolean).sort(); const types = Array.from(new Set(fullList.map(a => a.storage유형))).filter(Boolean).sort(); const locations = Array.from(new Set(fullList.map(a => { const loc = String(a.위치 || ''); @@ -75,16 +76,23 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont filterBar.innerHTML = `
- +
- +
+
+ + +
- + + + + `; +function renderHwHistory(assetId: string) { + const container = document.getElementById('hw-history-list'); + if (!container) return; + const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId); + if (logs.length === 0) { + container.innerHTML = '
기록된 이력이 없습니다.
'; + return; + } + container.innerHTML = logs.map(l => ` +
+
${l.date}
+
${l.user}
+
${l.details}
+
+ `).join(''); +} + +function applyTypeSpecificUI(type: string) { + const opFields = document.querySelectorAll('.op-only'); + const serverFields = document.querySelectorAll('.server-only'); + const standardLocFields = document.querySelectorAll('.loc-standard'); + + const upperType = type.toUpperCase(); + // 1. 모바일기기 그룹: 모바일, 태블릿, 노트북 + const isMobileGroup = ['모바일', '태블릿', '노트북', '휴대폰', '핸드폰'].some(t => type.includes(t)); + // 2. 전산비품 그룹: CPU, RAM, HDD, GPU + const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || type.includes('비품'); + + const isOpType = isMobileGroup || isEquipGroup; + const isServerType = ['서버', '스토리지', 'NAS', 'DAS'].includes(type); + + opFields.forEach(el => (el as HTMLElement).style.display = isOpType ? 'flex' : 'none'); + serverFields.forEach(el => (el as HTMLElement).style.display = isServerType ? 'flex' : 'none'); + + // 전산비품/모바일 그룹은 표준 위치 선택(건물/상세) 숨김 + standardLocFields.forEach(el => (el as HTMLElement).style.display = isOpType ? 'none' : 'flex'); +} + export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') { currentAsset = asset; const modal = document.getElementById('hw-asset-modal')!; - // 1. 잠금 상태 통합 제어 (데이터 유무가 아닌 호출 mode에만 의존) setEditLock('hw-asset-form', mode, { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit', @@ -205,104 +233,12 @@ export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') }); isEditMode = (mode === 'add'); - - // 2. 데이터 바인딩 fillHwFormData(asset); + applyTypeSpecificUI(asset.type); + renderHwHistory(asset.id); modal.classList.remove('hidden'); - applyTypeSpecificUI(asset.type); - createIcons({ icons: { Paperclip } }); -} - -function applyTypeSpecificUI(type: string) { - const detailPurpose = getFieldValue('hw-상세용도'); - const form = document.getElementById('hw-asset-form') as HTMLFormElement; - if (!form) return; - - const serverOnly = document.querySelectorAll('.server-only'); - const nonServer = document.querySelectorAll('.non-server'); - const locationFields = document.querySelectorAll('.hw-location-field'); - - const groups: Record = { - detailPurpose: document.getElementById('hw-상세용도-group'), - model: document.getElementById('hw-model-group'), - ip: document.getElementById('hw-ip-group'), - ip2: document.getElementById('hw-ip2-group'), - remote: document.getElementById('hw-remote-group'), - os: document.getElementById('hw-os-group'), - cpu: document.getElementById('hw-cpu-group'), - ram: document.getElementById('hw-ram-group'), - ssd1: document.getElementById('hw-ssd1-group'), - ssd2: document.getElementById('hw-ssd2-group'), - monitoring: document.getElementById('hw-monitoring-group'), - serverId: document.getElementById('hw-server-id-group'), - serverPw: document.getElementById('hw-server-pw-group'), - hwSpec: document.getElementById('hw-hwspec-group'), - ipNonServer: document.getElementById('hw-ip-non-server-group'), - type: document.getElementById('hw-유형-group'), - networkTitle: document.getElementById('hw-network-title'), - specTitle: document.getElementById('hw-spec-title'), - opTitle: document.getElementById('hw-op-title') - }; - - // 1. 초기화 (모든 유동 섹션 숨김) - serverOnly.forEach(el => (el as HTMLElement).style.display = 'none'); - nonServer.forEach(el => (el as HTMLElement).style.display = 'none'); - locationFields.forEach(el => (el as HTMLElement).style.display = 'none'); - Object.values(groups).forEach(g => { if (g) g.style.display = 'none'; }); - - if (groups.type) groups.type.style.display = 'flex'; - if (groups.opTitle) groups.opTitle.style.display = 'flex'; - - // 2. 유형별 정밀 규칙 적용 (사용자 정의 100% 일치) - if (type === '서버') { - serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); - locationFields.forEach(el => (el as HTMLElement).style.display = 'flex'); - Object.values(groups).forEach(g => { if (g) g.style.display = 'flex'; }); - } - else if (['스토리지', 'NAS', 'DAS'].includes(type)) { - serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); - locationFields.forEach(el => (el as HTMLElement).style.display = 'flex'); - if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; - if (groups.ip) groups.ip.style.display = 'flex'; - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.model) groups.model.style.display = 'flex'; - if (groups.ssd1) groups.ssd1.style.display = 'flex'; - if (groups.ssd2) groups.ssd2.style.display = 'flex'; - } - else if (type === 'PC' || type === '노트북') { - if (type === 'PC' && groups.detailPurpose) groups.detailPurpose.style.display = 'flex'; - nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec', 'ipNonServer'].forEach(k => { - if (groups[k]) groups[k]!.style.display = 'flex'; - }); - if (type === 'PC' && detailPurpose === '서버') { - locationFields.forEach(el => (el as HTMLElement).style.display = 'flex'); - if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; - ['ip', 'ip2', 'remote', 'serverId', 'serverPw', 'monitoring'].forEach(k => { - if (groups[k]) groups[k]!.style.display = 'flex'; - }); - if (groups.ipNonServer) groups.ipNonServer.style.display = 'none'; - } - } - else if (['CPU', 'GPU', '모바일'].includes(type)) { - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.model) groups.model.style.display = 'flex'; - } - else if (type === 'RAM') { - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.ram) groups.ram.style.display = 'flex'; - } - else if (type === 'HDD') { - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.ssd1) groups.ssd1.style.display = 'flex'; - } - else if (type === '태블릿') { - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.model) groups.model.style.display = 'flex'; - if (groups.ssd1) groups.ssd1.style.display = 'flex'; - } + createIcons({ icons: { X, Save, Edit2, RotateCcw, History, Plus, Paperclip } }); } function fillHwFormData(asset: HardwareAsset) { @@ -311,41 +247,21 @@ function fillHwFormData(asset: HardwareAsset) { setFieldValue('hw-법인', asset.법인); setFieldValue('hw-자산코드', asset.자산코드); setFieldValue('hw-현사용조직', asset.현사용조직); - setFieldValue('hw-이전사용조직', asset.이전사용조직); - setFieldValue('hw-상세용도', (asset as any).상세용도); - - parseAndSetLocation(asset.위치, 'hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타'); - + setFieldValue('hw-유형', asset.type); + setFieldValue('hw-명칭', asset.명칭 || asset.모델명); + setFieldValue('hw-보관위치', asset.보관위치 || ''); + setFieldValue('hw-현재상태', asset.현재상태 || '보관중'); + setFieldValue('hw-IP주소', asset.IP주소); + setFieldValue('hw-원격접속', (asset as any).원격접속); setFieldValue('hw-모델명', asset.모델명); setFieldValue('hw-OS', asset.OS); - setFieldValue('hw-CPU', asset.CPU); - setFieldValue('hw-RAM', asset.RAM); - setFieldValue('hw-SSD1', asset.SSD1); - setFieldValue('hw-SSD2', asset.SSD2); + setFieldValue('hw-HW사양', asset.HW사양); setFieldValue('hw-담당자_정', asset.담당자_정 || asset.관리자); - setFieldValue('hw-담당자_부', asset.담당자_부); + setFieldValue('hw-구매일', asset.구매일); + setFieldValue('hw-금액', asset.금액); + setFieldValue('hw-비고', asset.비고); - const isServerGrade = asset.type === '서버' || (asset as any).상세용도 === '서버' || asset.type === '스토리지' || ['NAS', 'DAS'].includes(asset.type); - - if (isServerGrade) { - setFieldValue('hw-용도', asset.용도 || (asset as any).purpose); - setFieldValue('hw-상세', asset.상세 || (asset as any).details); - setFieldValue('hw-비고', asset.비고 || (asset as any).remarks); - setFieldValue('hw-구매일', asset.구매일 || (asset as any).purchase_date); - setFieldValue('hw-유형', asset.storage유형 || asset.type); - setFieldValue('hw-IP주소', asset.IP주소 || (asset as any).ip_address); - setFieldValue('hw-IP2', (asset as any).IP2 || (asset as any).ip_address_2); - setFieldValue('hw-원격접속', asset.원격접속 || (asset as any).remote_tool); - setFieldValue('hw-서버ID', (asset as any).서버ID || (asset as any).server_id); - setFieldValue('hw-서버PW', (asset as any).서버PW || (asset as any).server_pw); - setFieldValue('hw-모니터링', asset.모니터링 || (asset as any).monitoring); - } else { - setFieldValue('hw-명칭', asset.명칭 || asset.모델명); - setFieldValue('hw-구매일', asset.구매일 || (asset as any).purchase_date); - setFieldValue('hw-금액', asset.금액 || (asset as any).price); - setFieldValue('hw-HW사양', asset.HW사양 || asset.상세 || (asset as any).details); - setFieldValue('hw-IP주소-non-server', asset.IP주소 || (asset as any).ip_address); - } + parseAndSetLocation(asset.위치, 'hw-위치-빌딩', 'hw-위치-상세', '', ''); } export function initHwModal(onSave: () => void, closeModals: () => void) { @@ -358,110 +274,88 @@ export function initHwModal(onSave: () => void, closeModals: () => void) { const revertBtn = document.getElementById('btn-revert-hw-edit')!; const deleteBtn = document.getElementById('btn-delete-hw-asset')!; const typeSelect = document.getElementById('hw-유형') as HTMLSelectElement; - const detailPurposeSelect = document.getElementById('hw-상세용도') as HTMLSelectElement; - - [typeSelect, detailPurposeSelect].forEach(el => { - el?.addEventListener('change', () => applyTypeSpecificUI(typeSelect.value)); - }); + const logAddBtn = document.getElementById('btn-add-hw-log')!; + const logModal = document.getElementById('hw-log-modal')!; - bindLocationEvents('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타'); + typeSelect.addEventListener('change', () => applyTypeSpecificUI(typeSelect.value)); + bindLocationEvents('hw-위치-빌딩', 'hw-위치-상세', '', ''); const closeModalAction = () => { closeModals(); isEditMode = false; }; document.getElementById('btn-close-hw-modal')?.addEventListener('click', closeModalAction); document.getElementById('btn-cancel-hw-modal')?.addEventListener('click', closeModalAction); revertBtn.addEventListener('click', () => { - setEditLock('hw-asset-form', 'view', { - saveBtnId: 'btn-save-hw-asset', - revertBtnId: 'btn-revert-hw-edit', - generateBtnId: 'btn-generate-hw-code' - }); + setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' }); isEditMode = false; if (currentAsset) fillHwFormData(currentAsset); }); - document.getElementById('btn-generate-hw-code')?.addEventListener('click', async () => { - const typeValue = typeSelect.value; - const purchaseDate = getFieldValue('hw-구매일'); - const typeCode = TYPE_PREFIX_MAP[typeValue] || 'ETC'; - const dateStr = purchaseDate.replace(/[^0-9]/g, ''); - if (dateStr.length < 4) { alert('올바른 구매일(연월)을 입력해주세요.'); return; } - const prefix = `${typeCode}-${dateStr.substring(2, 6)}-`; - try { - const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`); - const data = await res.json(); - if (data.nextCode) setFieldValue('hw-자산코드', data.nextCode); - } catch (err) { alert('자산번호 생성에 실패했습니다.'); } - }); - saveBtn.addEventListener('click', () => { if (!currentAsset) return; if (!isEditMode) { - setEditLock('hw-asset-form', 'edit', { - saveBtnId: 'btn-save-hw-asset', - revertBtnId: 'btn-revert-hw-edit' - }); + setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' }); isEditMode = true; + // 수정 모드 전환 시 현재 유형에 맞춰 UI 강제 갱신 + applyTypeSpecificUI(getFieldValue('hw-유형')); return; } - const type = typeSelect.value; - const detailPurpose = detailPurposeSelect.value; + const type = getFieldValue('hw-유형'); + const isOpType = ['전산비품', '모바일기기'].includes(type); + const storageLoc = getFieldValue('hw-보관위치'); const updated: any = { ...currentAsset, 법인: getFieldValue('hw-법인'), 자산코드: getFieldValue('hw-자산코드'), 현사용조직: getFieldValue('hw-현사용조직'), - 이전사용조직: getFieldValue('hw-이전사용조직'), - 위치: getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타'), + type: type, + 명칭: getFieldValue('hw-명칭'), + 보관위치: storageLoc, + 현재상태: getFieldValue('hw-현재상태'), + IP주소: getFieldValue('hw-IP주소'), 모델명: getFieldValue('hw-모델명'), OS: getFieldValue('hw-OS'), - CPU: getFieldValue('hw-CPU'), - RAM: getFieldValue('hw-RAM'), - SSD1: getFieldValue('hw-SSD1'), - SSD2: getFieldValue('hw-SSD2'), + HW사양: getFieldValue('hw-HW사양'), 담당자_정: getFieldValue('hw-담당자_정'), - 관리자: getFieldValue('hw-담당자_정'), - 담당자_부: getFieldValue('hw-담당자_부'), - type: type, - 상세용도: detailPurpose + 구매일: getFieldValue('hw-구매일'), + 금액: getFieldValue('hw-금액'), + 비고: getFieldValue('hw-비고'), + 위치: isOpType ? storageLoc : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', '') }; - if (type === '서버' || (type === 'PC' && detailPurpose === '서버') || ['스토리지', 'NAS', 'DAS'].includes(type)) { - updated.용도 = getFieldValue('hw-용도'); - updated.상세 = getFieldValue('hw-상세'); - updated.비고 = getFieldValue('hw-비고'); - updated.storage유형 = type; - updated.IP주소 = getFieldValue('hw-IP주소'); - updated.IP2 = getFieldValue('hw-IP2'); - updated.원격접속 = getFieldValue('hw-원격접속'); - updated.서버ID = getFieldValue('hw-서버ID'); - updated.서버PW = getFieldValue('hw-서버PW'); - updated.모니터링 = getFieldValue('hw-모니터링'); - } else { - updated.명칭 = getFieldValue('hw-명칭'); - updated.구매일 = getFieldValue('hw-구매일'); - updated.금액 = getFieldValue('hw-금액'); - updated.HW사양 = getFieldValue('hw-HW사양'); - updated.IP주소 = getFieldValue('hw-IP주소-non-server'); - } - saveHardwareAsset(updated); onSave(); - setEditLock('hw-asset-form', 'view', { - saveBtnId: 'btn-save-hw-asset', - revertBtnId: 'btn-revert-hw-edit' - }); + setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' }); isEditMode = false; }); deleteBtn.addEventListener('click', () => { - if (!currentAsset) return; - if (confirm('정말로 이 자산을 삭제하시겠습니까?')) { + if (currentAsset && confirm('정말로 삭제하시겠습니까?')) { deleteHardwareAsset(currentAsset.id); onSave(); - closeModals(); + closeModalAction(); } }); + + logAddBtn.addEventListener('click', () => { + logModal.classList.remove('hidden'); + (document.getElementById('new-hw-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0]; + (document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value = ''; + }); + + document.getElementById('btn-close-hw-log')?.addEventListener('click', () => logModal.classList.add('hidden')); + document.getElementById('btn-cancel-hw-log')?.addEventListener('click', () => logModal.classList.add('hidden')); + + document.getElementById('btn-confirm-hw-log')?.addEventListener('click', () => { + if (!currentAsset) return; + const date = (document.getElementById('new-hw-log-date') as HTMLInputElement).value; + const details = (document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value; + if (!date || !details) return; + + state.masterData.logs = state.masterData.logs || []; + state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentAsset.id, date, user: '관리자', details }); + logModal.classList.add('hidden'); + renderHwHistory(currentAsset.id); + }); } diff --git a/src/core/excelHandler.ts b/src/core/excelHandler.ts index 2ffdfa6..c59d849 100644 --- a/src/core/excelHandler.ts +++ b/src/core/excelHandler.ts @@ -40,6 +40,8 @@ export interface HardwareAsset { 비고?: string; 현사용조직?: string; 이전사용조직?: string; + 보관위치?: string; + 현재상태?: string; } export interface SoftwareAsset { diff --git a/src/core/state.ts b/src/core/state.ts index 6751938..d030bf8 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -121,18 +121,25 @@ export function saveHardwareAsset(updatedAsset: HardwareAsset) { const type = updatedAsset.type || ''; const detailPurpose = (updatedAsset as any).상세용도 || updatedAsset.detail_purpose || ''; - // 1. 타겟 카테고리 결정 (유연한 검색) + // 1. 타겟 카테고리 결정 (사용자 정의 그룹 기준) let targetKey: keyof MasterAssetData = 'equip'; - if (type.includes('서버') || detailPurpose.includes('서버')) { + const upperType = type.toUpperCase(); + const isServer = type.includes('서버') || detailPurpose.includes('서버'); + const isStorage = ['NAS', 'DAS', '스토리지'].some(t => type.includes(t)); + const isMobileGroup = ['모바일', '태블릿', '노트북', '휴대폰', '핸드폰'].some(t => type.includes(t)); + const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)); + const isPc = type === 'PC' || type === '개인PC' || detailPurpose === '개인PC'; + + if (isServer) { targetKey = 'server'; - } else if (['NAS', 'DAS', '스토리지'].some(t => type.includes(t))) { + } else if (isStorage) { targetKey = 'storage'; - } else if (['모바일', '태블릿', '휴대폰', '핸드폰', '노트북'].some(t => type.includes(t))) { + } else if (isMobileGroup) { targetKey = 'mobile'; - } else if (type === 'PC' || type === '개인PC' || detailPurpose === '개인PC') { + } else if (isPc) { targetKey = 'pc'; - } else if (['CPU', 'GPU', 'RAM', 'HDD'].some(t => type.toUpperCase().includes(t))) { + } else if (isEquipGroup) { targetKey = 'equip'; } diff --git a/src/views/List/EquipmentListView.ts b/src/views/List/EquipmentListView.ts index 007bb10..812f084 100644 --- a/src/views/List/EquipmentListView.ts +++ b/src/views/List/EquipmentListView.ts @@ -28,7 +28,23 @@ export function renderEquipmentList(container: HTMLElement) { const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; const table = document.createElement('table'); - table.innerHTML = `No구매법인현 사용조직유형자산번호모델명관리자구매일금액관리`; + table.innerHTML = ` + + + No. + 상태 + 구매법인 + 유형 + 자산번호 + 모델명 + 보관위치 + 관리자 + 구매일 + 금액 + + + + `; tableWrapper.appendChild(table); container.appendChild(tableWrapper); @@ -56,19 +72,31 @@ export function renderEquipmentList(container: HTMLElement) { filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + + const statusColors: Record = { + '대여중': '#3b82f6', + '보관중': '#1E5149', + '수리중': '#ef4444', + '기타': '#6b7280' + }; + const statusColor = statusColors[asset.현재상태 || '보관중'] || '#6b7280'; + const statusBadge = `${asset.현재상태 || '보관중'}`; + tr.innerHTML = ` - ${idx+1} - ${asset.법인} - ${asset.현사용조직||''} - ${asset.type} - ${asset.자산코드} - ${formatInline(asset.모델명)} - ${formatInline(asset.담당자_정 || asset.관리자)} - ${asset.구매일||''} - ${asset.금액||''} - + ${idx + 1} + ${statusBadge} + ${asset.법인} + ${asset.type} + ${asset.자산코드 || '-'} + ${formatInline(asset.모델명 || asset.명칭)} + ${asset.보관위치 || '-'} + ${formatInline(asset.담당자_정 || asset.관리자)} + ${asset.구매일 || ''} + ${asset.금액 || '0'} `; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); }); + tr.addEventListener('click', (e) => { + if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); + }); tbody.appendChild(tr); }); }; diff --git a/src/views/List/MobileListView.ts b/src/views/List/MobileListView.ts index 77be378..c991143 100644 --- a/src/views/List/MobileListView.ts +++ b/src/views/List/MobileListView.ts @@ -28,7 +28,23 @@ export function renderMobileList(container: HTMLElement) { const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; const table = document.createElement('table'); - table.innerHTML = `No구매법인현 사용조직유형자산번호모델명관리자구매일금액관리`; + table.innerHTML = ` + + + No. + 상태 + 구매법인 + 자산코드 + 명칭 + 보관위치 + 관리자 + 구매일 + 금액 + + + + `; + tableWrapper.appendChild(table); container.appendChild(tableWrapper); @@ -56,19 +72,30 @@ export function renderMobileList(container: HTMLElement) { filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; + + const statusColors: Record = { + '대여중': '#3b82f6', + '보관중': '#1E5149', + '수리중': '#ef4444', + '기타': '#6b7280' + }; + const statusColor = statusColors[asset.현재상태 || '보관중'] || '#6b7280'; + const statusBadge = `${asset.현재상태 || '보관중'}`; + tr.innerHTML = ` - ${idx+1} - ${asset.법인} - ${asset.현사용조직||''} - ${asset.type} - ${asset.자산코드} - ${formatInline(asset.모델명)} - ${formatInline(asset.담당자_정 || asset.관리자)} - ${asset.구매일||''} - ${asset.금액||''} - + ${idx + 1} + ${statusBadge} + ${asset.법인} + ${asset.자산코드 || '-'} + ${formatInline(asset.명칭 || asset.모델명)} + ${asset.보관위치 || '-'} + ${formatInline(asset.관리자 || asset.담당자_정)} + ${asset.구매일 || ''} + ${asset.금액 || '0'} `; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); }); + tr.addEventListener('click', (e) => { + if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); + }); tbody.appendChild(tr); }); }; From e4d958b5f2698a88ac8ba3171b0b8b2bde3a7a67 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Tue, 21 Apr 2026 17:56:29 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20PC=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=9C=A0=ED=98=95(=EA=B0=9C=EC=9D=B8PC,=20=EC=84=9C=EB=B2=84)?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=20=EB=B0=8F=20UI=20=EC=A0=9C=EC=96=B4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/HWModal.ts | 60 ++++++++++++++------------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index f43b102..9ee4e25 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -245,7 +245,7 @@ function applyTypeSpecificUI(type: string) { opTitle: document.getElementById('hw-op-title') }; - // 1. 초기화 (모든 유동 섹션 숨김) + // 1. 초기화 serverOnly.forEach(el => (el as HTMLElement).style.display = 'none'); nonServer.forEach(el => (el as HTMLElement).style.display = 'none'); locationFields.forEach(el => (el as HTMLElement).style.display = 'none'); @@ -254,12 +254,31 @@ function applyTypeSpecificUI(type: string) { if (groups.type) groups.type.style.display = 'flex'; if (groups.opTitle) groups.opTitle.style.display = 'flex'; - // 2. 유형별 정밀 규칙 적용 (사용자 정의 100% 일치) - if (type === '서버') { + // 2. PC 유형일 때 상세용도 선택창 노출 (복구 핵심) + if (type === 'PC' || type === '개인PC' || type === '노트북') { + if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex'; + + // 상세용도가 '서버'인 경우 서버용 필드 노출, 아니면 일반 PC용 필드 노출 + if (detailPurpose === '서버') { + serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); + locationFields.forEach(el => (el as HTMLElement).style.display = 'flex'); + if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; + ['ip', 'ip2', 'remote', 'serverId', 'serverPw', 'monitoring', 'model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2'].forEach(k => { + if (groups[k]) groups[k]!.style.display = 'flex'; + }); + } else { + nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); + if (groups.specTitle) groups.specTitle.style.display = 'flex'; + ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec', 'ipNonServer'].forEach(k => { + if (groups[k]) groups[k]!.style.display = 'flex'; + }); + } + } + else if (type === '서버') { serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); locationFields.forEach(el => (el as HTMLElement).style.display = 'flex'); Object.values(groups).forEach(g => { if (g) g.style.display = 'flex'; }); - } + } else if (['스토리지', 'NAS', 'DAS'].includes(type)) { serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); locationFields.forEach(el => (el as HTMLElement).style.display = 'flex'); @@ -268,41 +287,12 @@ function applyTypeSpecificUI(type: string) { if (groups.specTitle) groups.specTitle.style.display = 'flex'; if (groups.model) groups.model.style.display = 'flex'; if (groups.ssd1) groups.ssd1.style.display = 'flex'; - if (groups.ssd2) groups.ssd2.style.display = 'flex'; } - else if (type === 'PC' || type === '노트북') { - if (type === 'PC' && groups.detailPurpose) groups.detailPurpose.style.display = 'flex'; - nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec', 'ipNonServer'].forEach(k => { - if (groups[k]) groups[k]!.style.display = 'flex'; - }); - if (type === 'PC' && detailPurpose === '서버') { - locationFields.forEach(el => (el as HTMLElement).style.display = 'flex'); - if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; - ['ip', 'ip2', 'remote', 'serverId', 'serverPw', 'monitoring'].forEach(k => { - if (groups[k]) groups[k]!.style.display = 'flex'; - }); - if (groups.ipNonServer) groups.ipNonServer.style.display = 'none'; - } - } - else if (['CPU', 'GPU', '모바일'].includes(type)) { + else { + // 기타 유형 (CPU, RAM, 모바일 등) if (groups.specTitle) groups.specTitle.style.display = 'flex'; if (groups.model) groups.model.style.display = 'flex'; } - else if (type === 'RAM') { - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.ram) groups.ram.style.display = 'flex'; - } - else if (type === 'HDD') { - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.ssd1) groups.ssd1.style.display = 'flex'; - } - else if (type === '태블릿') { - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.model) groups.model.style.display = 'flex'; - if (groups.ssd1) groups.ssd1.style.display = 'flex'; - } } function fillHwFormData(asset: HardwareAsset) { From 4b765aba2e0c08739397c4c9533a443d52e48422 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 22 Apr 2026 10:11:45 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20=EC=9E=90=EC=82=B0=20=EC=9C=A0?= =?UTF-8?q?=ED=98=95=EB=B3=84=20UI=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=90=EC=82=B0=EB=B2=88=ED=98=B8=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CPU/GPU/RAM/HDD 등 부품 유형별 필드 라벨 동적 변경 로직 추가\n- 유형별 불필요한 사양 필드 숨김 처리 및 UI 레이아웃 정교화\n- 서버측 자산번호 생성 API (/api/generate-asset-code) 구현\n- 모달 내 자산번호 자동 생성 버튼 이벤트 연동 및 백엔드 동기화 --- server.js | 28 ++++++++++ src/components/Modal/HWModal.ts | 94 +++++++++++++++++++++++++++++---- 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/server.js b/server.js index 22cd962..4772a4c 100644 --- a/server.js +++ b/server.js @@ -323,6 +323,34 @@ app.post('/api/sw-users/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); +// 자산번호 자동 생성 API +app.get('/api/generate-asset-code', async (req, res) => { + const { prefix } = req.query; + if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); + + try { + const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']; + let maxNum = 0; + + for (const table of tables) { + const [rows] = await pool.query( + `SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, + [`${prefix}%`] + ); + rows.forEach(r => { + const numPart = r.asset_code.replace(prefix, ''); + const num = parseInt(numPart); + if (!isNaN(num) && num > maxNum) maxNum = num; + }); + } + + const nextNum = (maxNum + 1).toString().padStart(3, '0'); + res.json({ nextCode: `${prefix}${nextNum}` }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // 초기화 및 서버 기동 ensureTables().then(() => { app.listen(PORT, () => { diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index a6029fc..4515f08 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -249,7 +249,15 @@ function applyTypeSpecificUI(type: string) { detailPurpose: document.getElementById('hw-상세용도-group'), networkTitle: document.getElementById('hw-network-title'), specTitle: document.getElementById('hw-spec-title'), - opTitle: document.getElementById('hw-op-title') + opTitle: document.getElementById('hw-op-title'), + model: document.getElementById('hw-model-group'), + os: document.getElementById('hw-os-group'), + cpu: document.getElementById('hw-cpu-group'), + ram: document.getElementById('hw-ram-group'), + ssd1: document.getElementById('hw-ssd1-group'), + ssd2: document.getElementById('hw-ssd2-group'), + hwSpec: document.getElementById('hw-hwspec-group'), + monitoring: document.getElementById('hw-monitoring-group') }; const serverOnly = document.querySelectorAll('.server-only'); @@ -257,42 +265,83 @@ function applyTypeSpecificUI(type: string) { const opOnly = document.querySelectorAll('.op-only'); const standardLoc = document.querySelectorAll('.loc-standard'); - // 1. 초기화 + // 1. 초기화 (모두 숨김 및 라벨 원복) serverOnly.forEach(el => (el as HTMLElement).style.display = 'none'); nonServer.forEach(el => (el as HTMLElement).style.display = 'none'); opOnly.forEach(el => (el as HTMLElement).style.display = 'none'); standardLoc.forEach(el => (el as HTMLElement).style.display = 'flex'); Object.values(groups).forEach(g => { if (g) g.style.display = 'none'; }); + const osLabel = document.querySelector('label[for="hw-OS"]') as HTMLElement; + const ramLabel = document.querySelector('label[for="hw-RAM"]') as HTMLElement; + const modelLabel = document.querySelector('label[for="hw-모델명"]') as HTMLElement; + if (osLabel) osLabel.innerText = '운영체제 (OS)'; + if (ramLabel) ramLabel.innerText = 'RAM 용량'; + if (modelLabel) modelLabel.innerText = '모델명'; + // 2. 분류 판별 - const isMobileGroup = ['모바일', '태블릿', '노트북', '휴대폰'].some(t => upperType.includes(t)); + const isMobileGroup = ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t)); const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품'); const isOpType = isMobileGroup || isEquipGroup; const isPcType = upperType === 'PC' || upperType === '개인PC' || upperType === '노트북'; // 3. 레이아웃 적용 + if (groups.opTitle) groups.opTitle.style.display = 'flex'; + if (isOpType) { opOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); standardLoc.forEach(el => (el as HTMLElement).style.display = 'none'); + if (groups.specTitle) groups.specTitle.style.display = 'flex'; - } + if (groups.model) groups.model.style.display = 'flex'; - if (isPcType) { + // 특정 부품 유형에 따른 라벨 및 필드 제어 + const isCpuGpu = ['CPU', 'GPU'].some(t => upperType.includes(t)); + const isRamHdd = ['RAM', 'HDD'].some(t => upperType.includes(t)); + + if (isCpuGpu) { + if (groups.os && osLabel) { + osLabel.innerText = '출시연월'; + groups.os.style.display = 'flex'; + } + } else if (isRamHdd) { + if (groups.ram && ramLabel) { + ramLabel.innerText = '용량'; + groups.ram.style.display = 'flex'; + } + // HDD인 경우 모델명 라벨을 S/N으로 변경 + if (upperType.includes('HDD') && modelLabel) { + modelLabel.innerText = 'S/N'; + } + } else { + if (groups.hwSpec) groups.hwSpec.style.display = 'flex'; + } + } + else if (isPcType) { if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex'; + if (groups.specTitle) groups.specTitle.style.display = 'flex'; + if (detailPurpose === '서버') { serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); + if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; + ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { + if (groups[k]) groups[k]!.style.display = 'flex'; + }); } else { nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); + ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { + if (groups[k]) groups[k]!.style.display = 'flex'; + }); } - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.networkTitle) groups.networkTitle.style.display = detailPurpose === '서버' ? 'flex' : 'none'; - } else if (upperType.includes('서버') || ['스토리지', 'NAS', 'DAS'].includes(upperType)) { + } + else if (upperType.includes('서버') || ['스토리지', 'NAS', 'DAS'].includes(upperType)) { serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; if (groups.specTitle) groups.specTitle.style.display = 'flex'; + ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { + if (groups[k]) groups[k]!.style.display = 'flex'; + }); } - - if (groups.opTitle) groups.opTitle.style.display = 'flex'; } export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') { @@ -377,6 +426,31 @@ export function initHwModal(onSave: () => void, closeModals: () => void) { if (currentAsset) fillHwFormData(currentAsset); }); + document.getElementById('btn-generate-hw-code')?.addEventListener('click', async () => { + const typeValue = typeSelect.value; + const purchaseDate = getFieldValue('hw-구매일'); + const typeCode = TYPE_PREFIX_MAP[typeValue] || 'ETC'; + + // 구매일에서 연월(YYMM) 추출 (예: 2026-04-21 -> 2604) + const dateStr = purchaseDate.replace(/[^0-9]/g, ''); + if (dateStr.length < 4) { + alert('올바른 구매일(연월)을 입력해주세요. (예: 2026-04-21)'); + return; + } + const prefix = `${typeCode}-${dateStr.substring(2, 6)}-`; + + try { + const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`); + const data = await res.json(); + if (data.nextCode) { + setFieldValue('hw-자산코드', data.nextCode); + } + } catch (err) { + console.error('❌ 자산번호 생성 실패:', err); + alert('자산번호 생성에 실패했습니다.'); + } + }); + saveBtn.addEventListener('click', () => { if (!currentAsset) return; if (!isEditMode) { From d52c2c42004e6efcec893bbcc52a666cdcd9ccd0 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 22 Apr 2026 11:24:15 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20=EA=B5=AC=EB=A7=A4=EC=97=B0?= =?UTF-8?q?=EC=9B=94=20=ED=91=9C=EC=A4=80=ED=99=94=20=EB=B0=8F=20=EC=9E=90?= =?UTF-8?q?=EC=82=B0=EB=B2=88=ED=98=B8=20YYYYMM=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/HWModal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 9ee4e25..29049d2 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -165,8 +165,8 @@ const HW_MODAL_HTML = `
- - + +
From e1cdcfd93a201b8ffeebcb538473ed2dc88e04fa Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 22 Apr 2026 17:15:58 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20=ED=95=98=EB=93=9C=EC=9B=A8?= =?UTF-8?q?=EC=96=B4=20=EC=9E=90=EB=8F=99=20=EB=B3=80=EA=B2=BD=20=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=9E=90=EC=82=B0?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db_fix_data.js | 49 ++ db_init.js | 8 +- server.js | 46 +- src/components/Modal/DashboardDetailModal.ts | 2 +- src/components/Modal/HWModal.ts | 765 +++++++------------ src/components/Modal/ModalUtils.ts | 90 ++- src/components/Modal/PCModal.ts | 362 --------- src/components/Modal/SWModal.ts | 475 ++++-------- src/components/Modal/SharedData.ts | 3 +- src/core/excelHandler.ts | 46 +- src/main.ts | 12 +- src/styles/modal.css | 36 + src/views/Dashboard/HwDashboard.ts | 2 +- src/views/List/EquipmentListView.ts | 5 +- src/views/List/MobileListView.ts | 12 +- src/views/List/PcListView.ts | 8 +- src/views/List/ServerListView.ts | 7 +- src/views/List/SwListView.ts | 2 +- 18 files changed, 730 insertions(+), 1200 deletions(-) create mode 100644 db_fix_data.js delete mode 100644 src/components/Modal/PCModal.ts diff --git a/db_fix_data.js b/db_fix_data.js new file mode 100644 index 0000000..92fb2b4 --- /dev/null +++ b/db_fix_data.js @@ -0,0 +1,49 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env; + +async function migrateData() { + const connection = await mysql.createConnection({ + host: DB_HOST, + user: DB_USER, + password: DB_PASS, + database: DB_NAME, + port: parseInt(DB_PORT || '3306') + }); + + console.log('🔄 기존 데이터 보정 시작 (상세유형 = 유형)...'); + + const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']; + + for (const table of tables) { + // 1. 유형(type)이 비어있는 경우 기본값 채우기 (보정 전 단계) + let defaultType = '기타'; + if (table === 'server_assets') defaultType = '서버'; + else if (table === 'pc_assets') defaultType = '개인PC'; + else if (table === 'storage_assets') defaultType = '스토리지'; + else if (table === 'equip_assets') defaultType = '전산비품'; + else if (table === 'mobile_assets') defaultType = '모바일기기'; + + await connection.query(`UPDATE ${table} SET type = ? WHERE type IS NULL OR type = ''`, [defaultType]); + + // 2. 개인PC가 아닌 데이터들에 대해 상세유형 = 유형 업데이트 + const [result] = await connection.query(` + UPDATE ${table} + SET detail_purpose = type + WHERE type NOT IN ('개인PC', 'PC') + `); + + console.log(`✅ ${table}: ${result.affectedRows}개 데이터 보정 완료`); + } + + console.log('✨ 모든 기존 데이터 보정이 완료되었습니다.'); + await connection.end(); +} + +migrateData().catch(err => { + console.error('❌ 데이터 보정 실패:', err); + process.exit(1); +}); diff --git a/db_init.js b/db_init.js index 0aa41e8..bc9daa1 100644 --- a/db_init.js +++ b/db_init.js @@ -32,7 +32,7 @@ async function initDB() { id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100) COMMENT '구매법인', asset_code VARCHAR(100) COMMENT '자산번호', - purchase_date VARCHAR(50) COMMENT '구매일자', + purchase_date VARCHAR(50) COMMENT '구매연월', type VARCHAR(50) COMMENT '유형', detail_purpose VARCHAR(50) COMMENT '상세용도', purpose VARCHAR(255) COMMENT '용도', @@ -57,6 +57,8 @@ async function initDB() { monitoring VARCHAR(100), price VARCHAR(100) COMMENT '금액', remarks TEXT, + storage_location VARCHAR(255) COMMENT '보관위치', + status VARCHAR(50) COMMENT '현재상태', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='${comment}'; `; @@ -77,7 +79,7 @@ async function initDB() { license_type VARCHAR(100) COMMENT '라이선스 유형', quantity INT COMMENT '수량', price VARCHAR(100) COMMENT '금액', - purchase_date VARCHAR(50) COMMENT '구매일', + purchase_date VARCHAR(50) COMMENT '구매연월', expiry_date VARCHAR(50) COMMENT '만료일', vendor VARCHAR(255) COMMENT '납품업체', remarks TEXT COMMENT '비고', @@ -95,7 +97,7 @@ async function initDB() { license_key VARCHAR(255) COMMENT '라이선스 키', quantity INT COMMENT '수량', price VARCHAR(100) COMMENT '금액', - purchase_date VARCHAR(50) COMMENT '구매일', + purchase_date VARCHAR(50) COMMENT '구매연월', vendor VARCHAR(255) COMMENT '납품업체', remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP diff --git a/server.js b/server.js index 4772a4c..c5d0b95 100644 --- a/server.js +++ b/server.js @@ -90,22 +90,48 @@ const hardwareInsertSQL = (table) => ` `; const getHardwareValues = (a) => [ - a.id, a.법인||'', a.자산코드||'', a.구매일||'', a.type||'', a.상세용도||'', a.용도||'', a.상세||'', + a.id, a.법인||'', a.자산코드||'', a.구매연월||'', a.type||'', a.상세용도||'', a.용도||'', a.상세||'', a.현사용조직||'', a.이전사용조직||'', a.위치||'', a.담당자_정||'', a.담당자_부||'', a.IP주소||'', a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'', a.SSD1||'', a.SSD2||'', a.HDD1||'', a.모니터링||'', a.금액||'', a.비고||'', a.보관위치||'', a.현재상태||'' ]; -const mapHardware = (r, defaultType) => ({ - id: r.id, 법인: r.corp, 자산코드: r.asset_code, 구매일: r.purchase_date, type: r.type || defaultType, - 상세용도: r.detail_purpose, 용도: r.purpose, 상세: r.details, 현사용조직: r.current_org, - 이전사용조직: r.prev_org, 위치: r.location, 담당자_정: r.manager_main, 담당자_부: r.manager_sub, - IP주소: r.ip_address, 원격접속: r.remote_tool, 서버ID: r.server_id, 서버PW: r.server_pw, - 모델명: r.model_name, OS: r.os, CPU: r.cpu, RAM: r.ram, GPU: r.gpu, SSD1: r.storage1, - SSD2: r.storage2, HDD1: r.storage3, 모니터링: r.monitoring, 금액: r.price, 비고: r.remarks, - 보관위치: r.storage_location, 현재상태: r.status -}); +const mapHardware = (r, defaultType) => { + const type = r.type || defaultType; + return { + id: r.id, + 법인: r.corp, + 자산코드: r.asset_code, + 구매연월: r.purchase_date, + type: type, + 상세용도: (type !== '개인PC' && !r.detail_purpose) ? type : r.detail_purpose, + 용도: r.purpose, + 상세: r.details, + 현사용조직: r.current_org, + 이전사용조직: r.prev_org, + 위치: r.location, + 담당자_정: r.manager_main, + 담당자_부: r.manager_sub, + IP주소: r.ip_address, + 원격접속: r.remote_tool, + 서버ID: r.server_id, + 서버PW: r.server_pw, + 모델명: r.model_name, + OS: r.os, + CPU: r.cpu, + RAM: r.ram, + GPU: r.gpu, + SSD1: r.storage1, + SSD2: r.storage2, + HDD1: r.storage3, + 모니터링: r.monitoring, + 금액: r.price, + 비고: r.remarks, + 보관위치: r.storage_location, + 현재상태: r.status + }; +}; // --- API 라우트 정의 --- diff --git a/src/components/Modal/DashboardDetailModal.ts b/src/components/Modal/DashboardDetailModal.ts index 4ea6f96..7c5fe94 100644 --- a/src/components/Modal/DashboardDetailModal.ts +++ b/src/components/Modal/DashboardDetailModal.ts @@ -49,7 +49,7 @@ export function openDashboardDetail(title: string, list: HardwareAsset[]) { if (!thead) return; titleEl.textContent = title; - thead.innerHTML = `No유형자산코드명칭/모델위치담당/사용자구매일금액`; + thead.innerHTML = `No유형자산코드명칭/모델위치담당/사용자구매연월금액`; tbody.innerHTML = ''; if (list.length === 0) { tbody.innerHTML = `해당 조건의 자산이 없습니다.`; diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index dcc94f2..4247dd0 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,7 +1,7 @@ import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state'; -import { HardwareAsset, MasterAssetData, HardwareLog } from '../../core/excelHandler'; -import { openModal, closeModals } from './BaseModal'; -import { createIcons, Paperclip, History, Plus, X, Save, Edit2, RotateCcw } from 'lucide'; +import { HardwareAsset, HardwareLog } from '../../core/excelHandler'; +import { closeModals } from './BaseModal'; +import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Paperclip } from 'lucide'; import { CORP_LIST, ORG_LIST, HW_TYPE_LIST, LOCATION_DATA, TYPE_PREFIX_MAP } from './SharedData'; import { generateOptionsHTML, @@ -10,7 +10,10 @@ import { parseAndSetLocation, bindLocationEvents, getCombinedLocation, - setEditLock + setEditLock, + createModalFrameHTML, + autoFillForm, + autoExtractForm } from './ModalUtils'; let currentAsset: HardwareAsset | null = null; @@ -18,350 +21,119 @@ let isEditMode = false; const STATUS_LIST = ['대여중', '보관중', '수리중', '기타']; -const HW_MODAL_HTML = ` -