From a805d9ce065698a5d02284cd73294e82c3e59cac Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 15 Apr 2026 13:56:04 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=AA=A8=EB=8B=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주요 정리 내용: - 핵심 엔진 분리: state, excelHandler 등을 src/core/ 디렉토리로 격리 - 모달 컴포넌트화: index.html의 거대 HTML 구조를 각 모달 TS 파일로 내장 및 동적 주입 - index.html 최적화: 수백 줄의 중복 코드를 제거하여 슬림한 Shell 구조로 변환 - 전역 복구: 병합 과정에서 발생한 한글 인코딩 깨짐 전수 복구 및 빌드 오류 해결 - 경로 정합성: 파일 구조 변경에 따른 모든 import 경로 일괄 업데이트 --- index.html | 413 +------------------ src/components/Modal/BaseModal.ts | 25 +- src/components/Modal/DashboardDetailModal.ts | 109 +++++ src/components/Modal/HWModal.ts | 185 ++++++++- src/components/Modal/PCModal.ts | 177 ++++++-- src/components/Modal/SWModal.ts | 114 ++++- src/components/Modal/SWUserModal.ts | 200 ++++++--- src/components/Modal/StorageModal.ts | 89 ++-- src/components/Sidebar.ts | 2 +- src/{ => core}/dummyDataGenerator.ts | 2 +- src/{ => core}/excelHandler.ts | 0 src/{ => core}/realServerData.ts | 63 +-- src/{ => core}/state.ts | 0 src/main.ts | 34 +- src/views/AssetTableView.ts | 138 +------ src/views/DashboardView.ts | 73 +--- 16 files changed, 762 insertions(+), 862 deletions(-) create mode 100644 src/components/Modal/DashboardDetailModal.ts rename src/{ => core}/dummyDataGenerator.ts (99%) rename src/{ => core}/excelHandler.ts (100%) rename src/{ => core}/realServerData.ts (94%) rename src/{ => core}/state.ts (100%) diff --git a/index.html b/index.html index 25d5cd3..a5c94b1 100644 --- a/index.html +++ b/index.html @@ -64,421 +64,12 @@
+
- - - - - - - - - - - - - - - - - - + diff --git a/src/components/Modal/BaseModal.ts b/src/components/Modal/BaseModal.ts index 8830c5f..7e1c518 100644 --- a/src/components/Modal/BaseModal.ts +++ b/src/components/Modal/BaseModal.ts @@ -2,33 +2,22 @@ * 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다. */ export function initBaseModal() { - const modals = document.querySelectorAll('.modal-overlay'); - const closeButtons = document.querySelectorAll('.btn-icon, [id^="btn-cancel-"]'); - - // 모든 모달 닫기 함수 const closeAllModals = () => { + const modals = document.querySelectorAll('.modal-overlay'); modals.forEach(modal => modal.classList.add('hidden')); - // SW 관련 추가 모달 처리 - document.getElementById('sw-user-modal')?.classList.add('hidden'); - document.getElementById('sw-user-edit-modal')?.classList.add('hidden'); - document.getElementById('dashboard-detail-modal')?.classList.add('hidden'); }; - // 닫기 버튼 이벤트 바인딩 - closeButtons.forEach(btn => { - btn.addEventListener('click', closeAllModals); - }); - // ESC 키로 닫기 window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeAllModals(); }); - // 배경(Overlay) 클릭 시 닫기 - modals.forEach(modal => { - modal.addEventListener('click', (e) => { - if (e.target === modal) closeAllModals(); - }); + // 배경(Overlay) 클릭 시 닫기 (동적 생성된 모달 대응을 위해 이벤트 위임 고려 가능하나 일단 단순 구현) + document.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + if (target.classList.contains('modal-overlay')) { + closeAllModals(); + } }); return { closeAllModals }; diff --git a/src/components/Modal/DashboardDetailModal.ts b/src/components/Modal/DashboardDetailModal.ts new file mode 100644 index 0000000..16c1273 --- /dev/null +++ b/src/components/Modal/DashboardDetailModal.ts @@ -0,0 +1,109 @@ +import { HardwareAsset, SoftwareAsset } from '../../core/excelHandler'; +import { state } from '../../core/state'; + +const DASHBOARD_DETAIL_MODAL_HTML = ` + +`; + +export function initDashboardDetailModal() { + if (!document.getElementById('dashboard-detail-modal')) { + document.body.insertAdjacentHTML('beforeend', DASHBOARD_DETAIL_MODAL_HTML); + } + + const modal = document.getElementById('dashboard-detail-modal')!; + const closeBtn = document.getElementById('btn-close-dashboard-detail-modal')!; + const cancelBtn = document.getElementById('btn-cancel-dashboard-detail-modal')!; + + const closeModal = () => modal.classList.add('hidden'); + closeBtn.addEventListener('click', closeModal); + cancelBtn.addEventListener('click', closeModal); + modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); +} + +export function openDashboardDetail(title: string, list: HardwareAsset[]) { + const modal = document.getElementById('dashboard-detail-modal'); + if (!modal) return; + const titleEl = document.getElementById('dashboard-detail-modal-title'); + const tbody = document.getElementById('dashboard-detail-tbody'); + if (!titleEl || !tbody) return; + const thead = tbody.closest('table')?.querySelector('thead'); + if (!thead) return; + + titleEl.textContent = title; + thead.innerHTML = `No유형자산코드명칭/모델위치담당/사용자구매일금액`; + tbody.innerHTML = ''; + if (list.length === 0) { + tbody.innerHTML = `해당 조건의 자산이 없습니다.`; + } else { + list.forEach((asset, idx) => { + let manager = asset.관리자 || asset.사용자 || asset.담당자_정 || '-'; + let name = asset.명칭 || asset.모델명 || '-'; + const tr = document.createElement('tr'); + tr.innerHTML = `${idx+1}${asset.type}${asset.자산코드}${name}${asset.위치||'-'}${manager}${asset.구매일||'-'}${asset.금액||'-'}`; + tbody.appendChild(tr); + }); + } + modal.classList.remove('hidden'); +} + +export function openSwDashboardDetail(title: string, list: SoftwareAsset[]) { + const modal = document.getElementById('dashboard-detail-modal'); + if (!modal) return; + const titleEl = document.getElementById('dashboard-detail-modal-title'); + const tbody = document.getElementById('dashboard-detail-tbody'); + if (!titleEl || !tbody) return; + const thead = tbody.closest('table')?.querySelector('thead'); + if (!thead) return; + + titleEl.textContent = title; + thead.innerHTML = `No유형법인제품명수량금액`; + tbody.innerHTML = ''; + list.forEach((sw, idx) => { + const tr = document.createElement('tr'); + tr.innerHTML = `${idx+1}${sw.type}${sw.법인}${sw.제품명}${sw.수량}${sw.금액}`; + tbody.appendChild(tr); + }); + modal.classList.remove('hidden'); +} + +export function openSwUsageDetail(title: string, list: SoftwareAsset[]) { + const modal = document.getElementById('dashboard-detail-modal'); + if (!modal) return; + const titleEl = document.getElementById('dashboard-detail-modal-title'); + const tbody = document.getElementById('dashboard-detail-tbody'); + if (!titleEl || !tbody) return; + const thead = tbody.closest('table')?.querySelector('thead'); + if (!thead) return; + + titleEl.textContent = title; + thead.innerHTML = `No법인제품명수량사용중사용가능`; + tbody.innerHTML = ''; + list.forEach((sw, idx) => { + const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; + const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10); + const avail = qty - assigned; + const tr = document.createElement('tr'); + tr.innerHTML = `${idx+1}${sw.법인}${sw.제품명}${qty}${assigned}${avail}`; + tbody.appendChild(tr); + }); + modal.classList.remove('hidden'); +} diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 05e971a..a330711 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,14 +1,164 @@ -import { state } from '../../state'; -import { HardwareAsset } from '../../excelHandler'; +import { state } from '../../core/state'; +import { HardwareAsset } from '../../core/excelHandler'; import { renderTable } from '../../views/AssetTableView'; import { createIcons, Paperclip } from 'lucide'; let currentAsset: HardwareAsset | null = null; let isEditMode = false; +const HW_MODAL_HTML = ` + +`; + export function openHwModal(asset: HardwareAsset) { currentAsset = asset; - isEditMode = false; // 항상 조회 모드로 시작 + isEditMode = false; const modal = document.getElementById('hw-asset-modal')!; const form = document.getElementById('hw-asset-form') as HTMLFormElement; @@ -19,18 +169,14 @@ export function openHwModal(asset: HardwareAsset) { form.classList.remove('is-edit-mode'); form.classList.add('is-view-mode'); saveBtn.textContent = '수정'; - revertBtn.classList.add('hidden'); // 조회 모드에서는 숨김 + revertBtn.classList.add('hidden'); - // 데이터 채우기 함수 호출 (수정 취소 시에도 재사용) fillHwFormData(asset); modal.classList.remove('hidden'); createIcons({ icons: { Paperclip } }); } -/** - * 폼 필드에 자산 데이터 채우기 - */ function fillHwFormData(asset: HardwareAsset) { (document.getElementById('hw-asset-id') as HTMLInputElement).value = asset.id; (document.getElementById('hw-asset-type') as HTMLInputElement).value = asset.type; @@ -85,6 +231,11 @@ function fillHwFormData(asset: HardwareAsset) { } export function initHwModal() { + // HTML 주입 + if (!document.getElementById('hw-asset-modal')) { + document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML); + } + const modal = document.getElementById('hw-asset-modal')!; const form = document.getElementById('hw-asset-form') as HTMLFormElement; const closeBtn = document.getElementById('btn-close-hw-modal')!; @@ -104,20 +255,13 @@ export function initHwModal() { form.classList.add('is-view-mode'); saveBtn.textContent = '수정'; revertBtn.classList.add('hidden'); - if (currentAsset) fillHwFormData(currentAsset); // 원래 데이터로 복구 + if (currentAsset) fillHwFormData(currentAsset); }; closeBtn.addEventListener('click', closeModal); cancelBtn.addEventListener('click', closeModal); - - modal.addEventListener('click', (e) => { - if (e.target === modal) closeModal(); - }); - - // 수정 취소 버튼 이벤트 - revertBtn.addEventListener('click', () => { - switchToViewMode(); - }); + modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); + revertBtn.addEventListener('click', () => { switchToViewMode(); }); saveBtn.addEventListener('click', () => { if (!currentAsset) return; @@ -127,11 +271,10 @@ export function initHwModal() { form.classList.remove('is-view-mode'); form.classList.add('is-edit-mode'); saveBtn.textContent = '저장'; - revertBtn.classList.remove('hidden'); // 수정 모드에서 취소 버튼 표시 + revertBtn.classList.remove('hidden'); return; } - // 실제 저장 로직 const assetId = (document.getElementById('hw-asset-id') as HTMLInputElement).value; const type = (document.getElementById('hw-asset-type') as HTMLInputElement).value; @@ -177,7 +320,7 @@ export function initHwModal() { if (idx > -1) { state.masterData.hw[idx] = updated; renderTable(document.getElementById('main-content')!); - switchToViewMode(); // 저장 후 다시 조회 모드로 + switchToViewMode(); } }); diff --git a/src/components/Modal/PCModal.ts b/src/components/Modal/PCModal.ts index bc7197d..cce66ee 100644 --- a/src/components/Modal/PCModal.ts +++ b/src/components/Modal/PCModal.ts @@ -1,23 +1,143 @@ -import { state } from '../../state'; -import { HardwareAsset, HardwareLog } from '../../excelHandler'; +import { state } from '../../core/state'; +import { HardwareAsset, HardwareLog } from '../../core/excelHandler'; import { openModal } from './BaseModal'; -/** - * 개인PC 모달 초기화 및 로직 제어 - */ +const PC_MODAL_HTML = ` + +`; + export function initPcModal(renderContent: () => void, closeModals: () => void) { + if (!document.getElementById('pc-asset-modal')) { + document.body.insertAdjacentHTML('beforeend', PC_MODAL_HTML); + } + const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement; const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement; const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement; + const btnCancelPc = document.getElementById('btn-cancel-pc-modal') as HTMLButtonElement; + const btnClosePc = document.getElementById('btn-close-pc-modal') as HTMLButtonElement; + + btnCancelPc?.addEventListener('click', closeModals); + btnClosePc?.addEventListener('click', closeModals); - // 저장 버튼 이벤트 btnSavePc?.addEventListener('click', (e) => { e.preventDefault(); if (!pcForm.checkValidity()) { pcForm.reportValidity(); return; } const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value; const fileInput = document.getElementById('pc-품의서') as HTMLInputElement; - const 품의서명 = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('pc-품의서명') as HTMLElement).innerText.replace('📎', ''); + const 품의서명 = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('pc-품의서명') as HTMLElement).innerText.replace('첨부: ', ''); const newAsset: HardwareAsset = { id: id || Math.random().toString(36).substring(2, 9), @@ -26,11 +146,7 @@ export function initPcModal(renderContent: () => void, closeModals: () => void) 자산코드: (document.getElementById('pc-자산코드') as HTMLInputElement).value, 명칭: '', 위치: (document.getElementById('pc-위치') as HTMLInputElement).value, - 관리자: '', - IP주소: '', - MACaddress: '', - HW사양: '', - OS: '', + 관리자: '', IP주소: '', MACaddress: '', HW사양: '', OS: '', 납품업체: (document.getElementById('pc-납품업체') as HTMLInputElement).value, 사용자: (document.getElementById('pc-사용자') as HTMLInputElement).value, CPU: (document.getElementById('pc-CPU') as HTMLInputElement).value, GPU: (document.getElementById('pc-GPU') as HTMLInputElement).value, @@ -41,7 +157,6 @@ export function initPcModal(renderContent: () => void, closeModals: () => void) HDD2: (document.getElementById('pc-HDD2') as HTMLInputElement).value, 구매일: (document.getElementById('pc-구매일') as HTMLInputElement).value, 금액: (document.getElementById('pc-금액') as HTMLInputElement).value, - 납품업체: (document.getElementById('pc-납품업체') as HTMLInputElement).value, 품의서명 }; @@ -50,22 +165,15 @@ export function initPcModal(renderContent: () => void, closeModals: () => void) if(idx !== -1) { const oldAsset = state.masterData.hw[idx]; const changes = getChangeDetails(oldAsset, newAsset); - if (changes) { - // 로그인 기능이 없으므로 '관리자'가 로그인한 것으로 가정 - const modifier = '관리자'; - - const log: HardwareLog = { + state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: id, date: new Date().toLocaleString(), details: changes, - user: modifier - }; - - state.masterData.logs.push(log); + user: '관리자' + }); } - state.masterData.hw[idx] = newAsset; } } else { @@ -76,22 +184,17 @@ export function initPcModal(renderContent: () => void, closeModals: () => void) renderContent(); }); - // 삭제 버튼 이벤트 btnDeletePc?.addEventListener('click', (e) => { e.preventDefault(); const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value; if (confirm('삭제하시겠습니까?')) { state.masterData.hw = state.masterData.hw.filter(a => a.id !== id); - // 관련 로그도 삭제할지 여부는 정책에 따라 (여기서는 유지) closeModals(); renderContent(); } }); } -/** - * 개인PC 상세 모달 열기 - */ export function openPcModal(asset?: HardwareAsset) { const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement; const deleteBtn = document.getElementById('btn-delete-pc-asset')!; @@ -118,15 +221,15 @@ export function openPcModal(asset?: HardwareAsset) { (document.getElementById('pc-HDD1') as HTMLInputElement).value = asset.HDD1 || ''; (document.getElementById('pc-HDD2') as HTMLInputElement).value = asset.HDD2 || ''; (document.getElementById('pc-구매일') as HTMLInputElement).value = asset.구매일 || ''; - (document.getElementById('pc-금액') as HTMLInputElement).value = asset.금액 ? asset.금액.replace(/,/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ',') : ''; + (document.getElementById('pc-금액') as HTMLInputElement).value = asset.금액 || ''; (document.getElementById('pc-납품업체') as HTMLInputElement).value = asset.납품업체 || ''; - (document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : ''; + (document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `첨부: ${asset.품의서명}` : ''; renderHistory(asset.id); } else { - document.getElementById('pc-modal-title')!.textContent = '새 개인PC 자산 추가'; + document.getElementById('pc-modal-title')!.textContent = '신규 개인PC 자산 추가'; deleteBtn.style.display = 'none'; - if (historyArea) historyArea.style.display = 'none'; // 신규 시 이력 숨김 + if (historyArea) historyArea.style.display = 'none'; (document.getElementById('pc-asset-id') as HTMLInputElement).value = ''; (document.getElementById('pc-법인') as HTMLSelectElement).value = '한맥'; @@ -134,9 +237,6 @@ export function openPcModal(asset?: HardwareAsset) { } } -/** - * 변경 사항 감지 및 문자열 생성 - */ function getChangeDetails(oldAsset: HardwareAsset, newAsset: HardwareAsset): string { const changes: string[] = []; const fields = [ @@ -160,18 +260,13 @@ function getChangeDetails(oldAsset: HardwareAsset, newAsset: HardwareAsset): str fields.forEach(field => { const oldVal = (oldAsset as any)[field.key] || ''; const newVal = (newAsset as any)[field.key] || ''; - if (oldVal !== newVal) { changes.push(`${field.label}: ${oldVal || '없음'} → ${newVal || '없음'}`); } }); - return changes.join('\n'); } -/** - * 이력 리스트 렌더링 - */ function renderHistory(assetId: string) { const historyList = document.getElementById('pc-history-list'); if (!historyList) return; @@ -189,7 +284,7 @@ function renderHistory(assetId: string) {
${log.date}
수정자: ${log.user}
-
${log.details.replace(/\n/g, '
')}
+
${log.details.replace(/\\n/g, '
')}
`).join(''); } diff --git a/src/components/Modal/SWModal.ts b/src/components/Modal/SWModal.ts index deb3fbe..5ae0625 100644 --- a/src/components/Modal/SWModal.ts +++ b/src/components/Modal/SWModal.ts @@ -1,16 +1,104 @@ -import { state } from '../../state'; -import { SoftwareAsset } from '../../excelHandler'; +import { state } from '../../core/state'; +import { SoftwareAsset } from '../../core/excelHandler'; import { openModal } from './BaseModal'; -/** - * 소프트웨어 모달 초기화 및 로직 제어 - */ +const SW_MODAL_HTML = ` + +`; + export function initSwModal(renderContent: () => void, closeModals: () => void) { + if (!document.getElementById('sw-asset-modal')) { + document.body.insertAdjacentHTML('beforeend', SW_MODAL_HTML); + } + const swForm = document.getElementById('sw-asset-form') as HTMLFormElement; const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement; const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement; + const btnCancelSw = document.getElementById('btn-cancel-sw-modal') as HTMLButtonElement; + const btnCloseSw = document.getElementById('btn-close-sw-modal') as HTMLButtonElement; + + btnCancelSw?.addEventListener('click', closeModals); + btnCloseSw?.addEventListener('click', closeModals); - // 저장 버튼 이벤트 btnSaveSw?.addEventListener('click', (e) => { e.preventDefault(); if (!swForm.checkValidity()) { swForm.reportValidity(); return; } @@ -44,7 +132,6 @@ export function initSwModal(renderContent: () => void, closeModals: () => void) renderContent(); }); - // 삭제 버튼 이벤트 btnDeleteSw?.addEventListener('click', (e) => { e.preventDefault(); const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value; @@ -56,12 +143,7 @@ export function initSwModal(renderContent: () => void, closeModals: () => void) }); } -/** - * 소프트웨어 상세 모달 열기 - * @param asset 수정 시 자산 데이터, 신규 시 undefined - */ export function openSwModal(asset?: SoftwareAsset) { - const swModal = document.getElementById('sw-asset-modal') as HTMLDivElement; const swForm = document.getElementById('sw-asset-form') as HTMLFormElement; const deleteBtn = document.getElementById('btn-delete-sw-asset')!; @@ -71,11 +153,11 @@ export function openSwModal(asset?: SoftwareAsset) { const subGroup = document.getElementById('sw-구독일-group')!; const permGroup = document.getElementById('sw-유지보수-group')!; if (state.activeSubTab === '구독SW') { - subGroup.style.display = 'block'; + subGroup.style.display = 'flex'; permGroup.style.display = 'none'; } else { subGroup.style.display = 'none'; - permGroup.style.display = 'block'; + permGroup.style.display = 'flex'; } if (asset) { @@ -91,13 +173,13 @@ export function openSwModal(asset?: SoftwareAsset) { (document.getElementById('sw-구매일') as HTMLInputElement).value = asset.구매일 || ''; (document.getElementById('sw-구독일') as HTMLInputElement).value = asset.구독일 || ''; (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked = !!asset.유지보수여부; - (document.getElementById('sw-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : ''; + (document.getElementById('sw-금액') as HTMLInputElement).value = asset.금액 || ''; (document.getElementById('sw-수량') as HTMLInputElement).value = String(asset.수량); (document.getElementById('sw-계정명') as HTMLInputElement).value = asset.계정명 || ''; (document.getElementById('sw-납품업체') as HTMLInputElement).value = asset.납품업체 || ''; (document.getElementById('sw-비고') as HTMLInputElement).value = asset.비고 || ''; } else { - document.getElementById('sw-modal-title')!.textContent = `새 ${state.activeSubTab} 자산 추가`; + document.getElementById('sw-modal-title')!.textContent = `신규 ${state.activeSubTab} 자산 추가`; deleteBtn.style.display = 'none'; (document.getElementById('sw-asset-id') as HTMLInputElement).value = ''; (document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab; diff --git a/src/components/Modal/SWUserModal.ts b/src/components/Modal/SWUserModal.ts index dcad5f2..f4f0668 100644 --- a/src/components/Modal/SWUserModal.ts +++ b/src/components/Modal/SWUserModal.ts @@ -1,73 +1,154 @@ -import { state } from '../../state'; -import { SoftwareAsset, SWUser } from '../../excelHandler'; +import { state } from '../../core/state'; +import { SoftwareAsset, SWUser } from '../../core/excelHandler'; import { openModal } from './BaseModal'; import { createIcons, Edit2, X, Paperclip } from 'lucide'; let currentSwUserAssetId: string = ''; let tempSwUsers: SWUser[] = []; -/** - * 소프트웨어 사용자 할당 모달 초기화 - */ +const SW_USER_MODAL_HTML = ` + + + + + +`; + export function initSwUserModal(renderContent: () => void, closeModals: () => void) { + if (!document.getElementById('sw-user-modal')) { + document.body.insertAdjacentHTML('beforeend', SW_USER_MODAL_HTML); + } + const btnOpenAddUser = document.getElementById('btn-open-add-user'); const btnSaveEditUser = document.getElementById('btn-save-edit-user'); const btnSaveSwUserMapping = document.getElementById('btn-save-sw-user-mapping'); + const btnCancelUserEdit = document.getElementById('btn-cancel-sw-user-edit'); + const btnCloseUserEdit = document.getElementById('btn-close-sw-user-edit'); + const btnCancelUserModal = document.getElementById('btn-cancel-sw-user-modal'); + const btnCloseUserModal = document.getElementById('btn-close-sw-user-modal'); - btnOpenAddUser?.addEventListener('click', () => { - openUserEditModal(-1); - }); - - btnSaveEditUser?.addEventListener('click', () => { - saveUserEdit(); - }); - + btnOpenAddUser?.addEventListener('click', () => openUserEditModal(-1)); + btnSaveEditUser?.addEventListener('click', () => saveUserEdit()); + btnSaveSwUserMapping?.addEventListener('click', () => { - // 변경사항 전역 상태에 반영 state.masterData.swUsers = state.masterData.swUsers.filter(u => u.swId !== currentSwUserAssetId); state.masterData.swUsers.push(...tempSwUsers); - document.getElementById('sw-user-modal')?.classList.add('hidden'); renderContent(); }); - // 취소 버튼들 - document.getElementById('btn-cancel-sw-user-edit')?.addEventListener('click', () => { - document.getElementById('sw-user-edit-modal')?.classList.add('hidden'); - }); - document.getElementById('btn-cancel-sw-user-modal')?.addEventListener('click', () => { - document.getElementById('sw-user-modal')?.classList.add('hidden'); - }); + btnCancelUserEdit?.addEventListener('click', () => document.getElementById('sw-user-edit-modal')?.classList.add('hidden')); + btnCloseUserEdit?.addEventListener('click', () => document.getElementById('sw-user-edit-modal')?.classList.add('hidden')); + btnCancelUserModal?.addEventListener('click', () => document.getElementById('sw-user-modal')?.classList.add('hidden')); + btnCloseUserModal?.addEventListener('click', () => document.getElementById('sw-user-modal')?.classList.add('hidden')); } -/** - * 소프트웨어 사용자 목록 렌더링 - */ function renderUserList() { const tbody = document.getElementById('user-list-body')!; tbody.innerHTML = ''; if (tempSwUsers.length === 0) { - tbody.innerHTML = '할당된 사용자가 없습니다.'; + tbody.innerHTML = '할당된 사용자가 없습니다.'; return; } tempSwUsers.forEach((user, idx) => { const tr = document.createElement('tr'); - tr.style.cssText = 'border-bottom: 1px solid var(--border); transition: background-color 0.2s;'; - const deptTeam = [user.부서, user.팀].filter(Boolean).join(' / ') || '-'; const attachIcon = user.신청서명 ? `` : '-'; tr.innerHTML = ` - ${user.법인} - ${deptTeam} - ${user.직위 || '-'} - ${user.이름} - ${user.사용기간 || '-'} - ${attachIcon} - - - + ${user.법인} + ${deptTeam} + ${user.직위 || '-'} + ${user.이름} + ${user.사용기간 || '-'} + ${attachIcon} + + + `; tbody.appendChild(tr); @@ -84,7 +165,6 @@ function renderUserList() { tbody.querySelectorAll('.btn-remove-user').forEach(btn => { btn.addEventListener('click', (e) => { - e.stopPropagation(); const idx = parseInt((e.currentTarget as HTMLButtonElement).getAttribute('data-idx')!); tempSwUsers.splice(idx, 1); renderUserList(); @@ -92,9 +172,6 @@ function renderUserList() { }); } -/** - * 사용자 할당 모달 열기 - */ export function openSwUserModal(asset: SoftwareAsset) { openModal('sw-user-modal'); currentSwUserAssetId = asset.id; @@ -102,9 +179,6 @@ export function openSwUserModal(asset: SoftwareAsset) { renderUserList(); } -/** - * 사용자 추가/수정 모달 열기 - */ function openUserEditModal(idx: number) { const editModal = document.getElementById('sw-user-edit-modal')!; editModal.classList.remove('hidden'); @@ -121,7 +195,7 @@ function openUserEditModal(idx: number) { (document.getElementById('new-user-신청서') as HTMLInputElement).value = ''; document.getElementById('new-user-신청서명')!.innerText = ''; } else { - document.getElementById('sw-user-edit-modal-title')!.innerText = '사용자 수정'; + document.getElementById('sw-user-edit-modal-title')!.innerText = '사용자 정보 수정'; const u = tempSwUsers[idx]; (document.getElementById('new-user-법인') as HTMLSelectElement).value = u.법인; (document.getElementById('new-user-부서') as HTMLInputElement).value = u.부서; @@ -130,22 +204,15 @@ function openUserEditModal(idx: number) { (document.getElementById('new-user-이름') as HTMLInputElement).value = u.이름; (document.getElementById('new-user-사용기간') as HTMLInputElement).value = u.사용기간; (document.getElementById('new-user-신청서') as HTMLInputElement).value = ''; - document.getElementById('new-user-신청서명')!.innerText = u.신청서명 ? `📎${u.신청서명}` : ''; + document.getElementById('new-user-신청서명')!.innerText = u.신청서명 ? `첨부: ${u.신청서명}` : ''; } } -/** - * 사용자 추가/수정 내용 저장 (임시 목록에 반영) - */ function saveUserEdit() { const idx = parseInt((document.getElementById('edit-user-idx') as HTMLInputElement).value); - const 법인 = (document.getElementById('new-user-법인') as HTMLSelectElement).value; - const 부서 = (document.getElementById('new-user-부서') as HTMLInputElement).value; - const 팀 = (document.getElementById('new-user-팀') as HTMLInputElement).value; - const 직위 = (document.getElementById('new-user-직위') as HTMLInputElement).value; const 이름 = (document.getElementById('new-user-이름') as HTMLInputElement).value.trim(); - const 사용기간 = (document.getElementById('new-user-사용기간') as HTMLInputElement).value; - + if (!이름) { alert('이름을 입력해주세요.'); return; } + const fileInput = document.getElementById('new-user-신청서') as HTMLInputElement; let 신청서명 = ''; if (fileInput.files && fileInput.files.length > 0) { @@ -154,17 +221,20 @@ function saveUserEdit() { 신청서명 = tempSwUsers[idx].신청서명; } - if (!이름) { alert('이름을 입력해주세요.'); return; } + const userData: SWUser = { + id: idx === -1 ? Math.random().toString(36).substring(2, 9) : tempSwUsers[idx].id, + swId: currentSwUserAssetId, + 법인: (document.getElementById('new-user-법인') as HTMLSelectElement).value, + 부서: (document.getElementById('new-user-부서') as HTMLInputElement).value, + 팀: (document.getElementById('new-user-팀') as HTMLInputElement).value, + 직위: (document.getElementById('new-user-직위') as HTMLInputElement).value, + 이름, + 사용기간: (document.getElementById('new-user-사용기간') as HTMLInputElement).value, + 신청서명 + }; - if (idx === -1) { - tempSwUsers.push({ - id: Math.random().toString(36).substring(2, 9), - swId: currentSwUserAssetId, - 법인, 부서, 팀, 직위, 이름, 사용기간, 신청서명 - }); - } else { - tempSwUsers[idx] = { ...tempSwUsers[idx], 법인, 부서, 팀, 직위, 이름, 사용기간, 신청서명 }; - } + if (idx === -1) tempSwUsers.push(userData); + else tempSwUsers[idx] = userData; document.getElementById('sw-user-edit-modal')?.classList.add('hidden'); renderUserList(); diff --git a/src/components/Modal/StorageModal.ts b/src/components/Modal/StorageModal.ts index 1c3a8d9..f2e45e4 100644 --- a/src/components/Modal/StorageModal.ts +++ b/src/components/Modal/StorageModal.ts @@ -1,45 +1,77 @@ -import { state } from '../../state'; -import { HardwareAsset } from '../../excelHandler'; +import { state } from '../../core/state'; +import { HardwareAsset } from '../../core/excelHandler'; import { openModal } from './BaseModal'; -/** - * 스토리지 모달 초기화 및 로직 제어 - */ +const STORAGE_MODAL_HTML = ` + +`; + export function initStorageModal(renderContent: () => void, closeModals: () => void) { + if (!document.getElementById('storage-asset-modal')) { + document.body.insertAdjacentHTML('beforeend', STORAGE_MODAL_HTML); + } + const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement; const btnSaveStorage = document.getElementById('btn-save-storage-asset') as HTMLButtonElement; const btnDeleteStorage = document.getElementById('btn-delete-storage-asset') as HTMLButtonElement; + const btnCancelStorage = document.getElementById('btn-cancel-storage-modal') as HTMLButtonElement; + const btnCloseStorage = document.getElementById('btn-close-storage-modal') as HTMLButtonElement; + + btnCancelStorage?.addEventListener('click', closeModals); + btnCloseStorage?.addEventListener('click', closeModals); - // 저장 버튼 이벤트 btnSaveStorage?.addEventListener('click', (e) => { e.preventDefault(); if (!storageForm.checkValidity()) { storageForm.reportValidity(); return; } const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value; - const fileInput = document.getElementById('storage-품의서') as HTMLInputElement; - const 품의서명 = fileInput.files && fileInput.files.length > 0 ? fileInput.files[0].name : (document.getElementById('storage-품의서명') as HTMLElement).innerText.replace('📎', ''); const newAsset: HardwareAsset = { id: id || Math.random().toString(36).substring(2, 9), type: '스토리지', - 법인: (document.getElementById('storage-법인') as HTMLSelectElement).value, - storage유형: (document.getElementById('storage-유형') as HTMLSelectElement).value, + 법인: (document.getElementById('storage-법인') as HTMLInputElement).value, + storage유형: (document.getElementById('storage-유형') as HTMLInputElement).value, 자산코드: (document.getElementById('storage-자산코드') as HTMLInputElement).value, 명칭: (document.getElementById('storage-명칭') as HTMLInputElement).value, 위치: (document.getElementById('storage-위치') as HTMLInputElement).value, - 관리자: '', - IP주소: (document.getElementById('storage-IP주소') as HTMLInputElement).value, - MACaddress: (document.getElementById('storage-MAC주소') as HTMLInputElement).value, - HW사양: '', - OS: '', 모델명: (document.getElementById('storage-모델명') as HTMLInputElement).value, 용량: (document.getElementById('storage-용량') as HTMLInputElement).value, 담당자_정: (document.getElementById('storage-담당자_정') as HTMLInputElement).value, - 담당자_부: (document.getElementById('storage-담당자_부') as HTMLInputElement).value, + IP주소: (document.getElementById('storage-IP주소') as HTMLInputElement).value, 구매일: (document.getElementById('storage-구매일') as HTMLInputElement).value, 금액: (document.getElementById('storage-금액') as HTMLInputElement).value, - 납품업체: (document.getElementById('storage-납품업체') as HTMLInputElement).value, - 품의서명 + 관리자: '', MACaddress: '', HW사양: '', OS: '', 납품업체: '', 품의서명: '' }; if (id) { @@ -53,7 +85,6 @@ export function initStorageModal(renderContent: () => void, closeModals: () => v renderContent(); }); - // 삭제 버튼 이벤트 btnDeleteStorage?.addEventListener('click', (e) => { e.preventDefault(); const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value; @@ -65,12 +96,7 @@ export function initStorageModal(renderContent: () => void, closeModals: () => v }); } -/** - * 스토리지 상세 모달 열기 - * @param asset 수정 시 자산 데이터, 신규 시 undefined - */ export function openStorageModal(asset?: HardwareAsset) { - const storageModal = document.getElementById('storage-asset-modal') as HTMLDivElement; const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement; const deleteBtn = document.getElementById('btn-delete-storage-asset')!; @@ -82,27 +108,20 @@ export function openStorageModal(asset?: HardwareAsset) { deleteBtn.style.display = 'block'; (document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id; - (document.getElementById('storage-법인') as HTMLSelectElement).value = asset.법인; - (document.getElementById('storage-유형') as HTMLSelectElement).value = asset.storage유형 || 'NAS'; + (document.getElementById('storage-법인') as HTMLInputElement).value = asset.법인; + (document.getElementById('storage-유형') as HTMLInputElement).value = asset.storage유형 || 'NAS'; (document.getElementById('storage-자산코드') as HTMLInputElement).value = asset.자산코드; (document.getElementById('storage-명칭') as HTMLInputElement).value = asset.명칭; (document.getElementById('storage-위치') as HTMLInputElement).value = asset.위치 || ''; (document.getElementById('storage-모델명') as HTMLInputElement).value = asset.모델명 || ''; (document.getElementById('storage-용량') as HTMLInputElement).value = asset.용량 || ''; (document.getElementById('storage-담당자_정') as HTMLInputElement).value = asset.담당자_정 || ''; - (document.getElementById('storage-담당자_부') as HTMLInputElement).value = asset.담당자_부 || ''; (document.getElementById('storage-IP주소') as HTMLInputElement).value = asset.IP주소 || ''; - (document.getElementById('storage-MAC주소') as HTMLInputElement).value = asset.MACaddress || ''; (document.getElementById('storage-구매일') as HTMLInputElement).value = asset.구매일 || ''; - (document.getElementById('storage-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : ''; - (document.getElementById('storage-납품업체') as HTMLInputElement).value = asset.납품업체 || ''; - (document.getElementById('storage-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : ''; + (document.getElementById('storage-금액') as HTMLInputElement).value = asset.금액 || ''; } else { - document.getElementById('storage-modal-title')!.textContent = '새 스토리지 자산 추가'; + document.getElementById('storage-modal-title')!.textContent = '신규 스토리지 자산 추가'; deleteBtn.style.display = 'none'; (document.getElementById('storage-asset-id') as HTMLInputElement).value = ''; - (document.getElementById('storage-법인') as HTMLSelectElement).value = '한맥'; - (document.getElementById('storage-유형') as HTMLSelectElement).value = 'NAS'; - (document.getElementById('storage-품의서명') as HTMLElement).innerText = ''; } } diff --git a/src/components/Sidebar.ts b/src/components/Sidebar.ts index bc1180b..44453f2 100644 --- a/src/components/Sidebar.ts +++ b/src/components/Sidebar.ts @@ -1,4 +1,4 @@ -import { state } from '../state'; +import { state } from '../core/state'; export function renderSidebar(onTabChange: (tab: string) => void) { const navItems = document.querySelectorAll('.nav-list li'); diff --git a/src/dummyDataGenerator.ts b/src/core/dummyDataGenerator.ts similarity index 99% rename from src/dummyDataGenerator.ts rename to src/core/dummyDataGenerator.ts index 8eac636..55f3b43 100644 --- a/src/dummyDataGenerator.ts +++ b/src/core/dummyDataGenerator.ts @@ -167,7 +167,7 @@ export function generateDummyData(): MasterAssetData { 납품업체: '총판', 비고: '연간구독' }); - // ... rest unchanged + const assignCount = Math.floor(Math.random() * 2) + 1; for (let j=0; j renderTable(mainContent), () => {}); initHwModal(); - initStorageModal(); - initSwModal(); + initStorageModal(() => renderTable(mainContent), () => {}); + initSwModal(() => renderTable(mainContent), () => {}); initSwUserModal(() => renderTable(mainContent), () => {}); initDashboardDetailModal(); @@ -55,7 +60,7 @@ function initApp() { document.getElementById('btn-add-asset')?.addEventListener('click', () => { if (state.activeSubTab === '서버' || state.activeSubTab === '전산비품' || state.activeSubTab === '스토리지') { - const newAsset = { + const newAsset: HardwareAsset = { id: Math.random().toString(36).substring(2, 9), type: state.activeSubTab, 법인: '한맥', @@ -80,16 +85,5 @@ function initApp() { }); } -function initDashboardDetailModal() { - const modal = document.getElementById('dashboard-detail-modal')!; - const closeBtn = document.getElementById('btn-close-dashboard-detail-modal')!; - const cancelBtn = document.getElementById('btn-cancel-dashboard-detail-modal')!; - - const closeModal = () => modal.classList.add('hidden'); - closeBtn.addEventListener('click', closeModal); - cancelBtn.addEventListener('click', closeModal); - modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); -} - // Start the app document.addEventListener('DOMContentLoaded', initApp); diff --git a/src/views/AssetTableView.ts b/src/views/AssetTableView.ts index b1963ad..961d38f 100644 --- a/src/views/AssetTableView.ts +++ b/src/views/AssetTableView.ts @@ -1,4 +1,4 @@ -import { state } from '../state'; +import { state } from '../core/state'; import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide'; import { openPcModal } from '../components/Modal/PCModal'; import { openHwModal } from '../components/Modal/HWModal'; @@ -10,7 +10,7 @@ import { openSwUserModal } from '../components/Modal/SWUserModal'; * 자산 목록 테이블 렌더링 메인 함수 */ export function renderTable(mainContent: HTMLElement) { - mainContent.innerHTML = ''; // 기존 내용 삭제 (중요) + mainContent.innerHTML = ''; const container = document.createElement('div'); container.className = 'view-container'; const table = document.createElement('table'); @@ -21,7 +21,6 @@ export function renderTable(mainContent: HTMLElement) { renderSwTable(table, container, mainContent); } - // 테이블 내 아이콘 초기화 createIcons({ icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 } }); @@ -44,7 +43,6 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont 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.품의서명 ? '' : '-'}`; tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset); }); - tr.querySelector('.btn-edit')?.addEventListener('click', () => openPcModal(asset)); tbody.appendChild(tr); }); } else if (state.activeSubTab === '스토리지') { @@ -59,33 +57,12 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont 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); }); - tr.querySelector('.btn-edit')?.addEventListener('click', () => openStorageModal(asset)); tbody.appendChild(tr); }); } else { // 서버 또는 전산비품 if (state.activeSubTab === '서버') { - table.innerHTML = ` - - - No - 법인 - 자산번호 - 유형 - 용도 - 상세 - 설치위치 - 담당자 - IP 주소 - 원격접속 - 모델명 - OS - CPU - RAM - Storage - - - `; + table.innerHTML = `No법인자산번호유형용도상세설치위치담당자IP 주소원격접속모델명OSCPURAMStorage`; } else { table.innerHTML = `No법인${state.activeSubTab === '전산비품' ? '유형' : ''}자산코드명칭위치관리자구매일금액관리`; } @@ -100,63 +77,32 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont 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}`; + const getBadge = (text: string, bgColor: string) => `${text}`; if (state.activeSubTab === '서버') { const mainManager = asset.담당자_정 || ''; const subManager = asset.담당자_부 || ''; - - // 담당자 배지화 - const managerHtml = [ - mainManager ? `${getBadge('정', '#1E5149')} ${mainManager}` : '', - subManager ? `${getBadge('부', '#9CA3AF')} ${subManager}` : '' - ].filter(v => v !== '').join(' / '); - - // 원격접속 배지화 + 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)} - `; + 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)}`; 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.금액||''}`; @@ -170,111 +116,61 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) { const fullList = state.masterData.sw.filter(a => a.type === state.activeSubTab); const isSub = state.activeSubTab === '구독SW'; - - // 0. Container 준비 (조회 바 + 테이블) container.innerHTML = ''; - - // 1. 조회 바 (Filter Bar) 생성 const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - filterBar.innerHTML = ` -
- - -
-
- - -
-
- - -
- - `; + filterBar.innerHTML = `
`; container.appendChild(filterBar); - // 2. 테이블 기본 구조 생성 const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; table.classList.add('sw-table'); table.innerHTML = `No.분야법인부서제품명구매일${isSub ? '구독일' : ''}금액수량사용가능관리`; - tableWrapper.appendChild(table); container.appendChild(tableWrapper); mainContent.appendChild(container); const tbody = document.getElementById('dynamic-tbody')!; - - // 3. 필터링 및 테이블 업데이트 로직 const updateTable = () => { const keyword = (document.getElementById('filter-keyword') as HTMLInputElement).value.toLowerCase().trim(); const field = (document.getElementById('filter-field') as HTMLSelectElement).value; const corp = (document.getElementById('filter-corp') as HTMLSelectElement).value; - const filtered = fullList.filter(asset => { - const matchKeyword = !keyword || - (asset.제품명 || '').toLowerCase().includes(keyword) || - (asset.부서 || '').toLowerCase().includes(keyword); + const matchKeyword = !keyword || (asset.제품명 || '').toLowerCase().includes(keyword) || (asset.부서 || '').toLowerCase().includes(keyword); const matchField = !field || asset.분야 === field; const matchCorp = !corp || asset.법인 === corp; return matchKeyword && matchField && matchCorp; }); - tbody.innerHTML = ''; if (filtered.length === 0) { tbody.innerHTML = `검색 결과가 없습니다.`; return; } - filtered.forEach((asset, idx) => { const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length; - const avail = (typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10)) - assigned; + const qty = typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10); + const avail = qty - assigned; const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - tr.innerHTML = `${idx+1}${asset.분야||''}${asset.법인}${asset.부서||''}${asset.제품명}${asset.구매일||''}${isSub ? `${asset.구독일||''}` : ''}${asset.금액||'0'}${asset.수량}${avail}`; + tr.innerHTML = `${idx+1}${asset.분야||''}${asset.법인}${asset.부서||''}${asset.제품명}${asset.구매일||''}${isSub ? `${asset.구독일||''}` : ''}${asset.금액||'0'}${qty}${avail}`; tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); }); tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset)); tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset)); tbody.appendChild(tr); }); - - // 버튼 내 아이콘 다시 그리기 - createIcons({ - icons: { Edit2, Users, RefreshCcw } - }); + createIcons({ icons: { Edit2, Users, RefreshCcw } }); }; - // 4. 이벤트 바인딩 const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; const fieldSelect = document.getElementById('filter-field') as HTMLSelectElement; const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; const resetBtn = document.getElementById('btn-reset-filters') as HTMLButtonElement; - keywordInput.addEventListener('input', updateTable); fieldSelect.addEventListener('change', updateTable); corpSelect.addEventListener('change', updateTable); - resetBtn.addEventListener('click', () => { - keywordInput.value = ''; - fieldSelect.value = ''; - corpSelect.value = ''; + keywordInput.value = ''; fieldSelect.value = ''; corpSelect.value = ''; updateTable(); }); - - // 초기 실행 updateTable(); } diff --git a/src/views/DashboardView.ts b/src/views/DashboardView.ts index ebec674..1955583 100644 --- a/src/views/DashboardView.ts +++ b/src/views/DashboardView.ts @@ -1,5 +1,6 @@ -import { state } from '../state'; -import { HardwareAsset, SoftwareAsset } from '../excelHandler'; +import { state } from '../core/state'; +import { HardwareAsset, SoftwareAsset } from '../core/excelHandler'; +import { openDashboardDetail, openSwDashboardDetail, openSwUsageDetail } from '../components/Modal/DashboardDetailModal'; declare var Chart: any; @@ -344,71 +345,3 @@ function isSWExpiring(sw: SoftwareAsset) { } return false; } - -function openDashboardDetail(title: string, list: HardwareAsset[]) { - const modal = document.getElementById('dashboard-detail-modal'); - if (!modal) return; - const titleEl = document.getElementById('dashboard-detail-modal-title'); - const tbody = document.getElementById('dashboard-detail-tbody'); - if (!titleEl || !tbody) return; - const thead = tbody.closest('table')?.querySelector('thead'); - if (!thead) return; - - titleEl.textContent = title; - thead.innerHTML = `No유형자산코드명칭/모델위치담당/사용자구매일금액`; - tbody.innerHTML = ''; - if (list.length === 0) { - tbody.innerHTML = `해당 조건의 자산이 없습니다.`; - } else { - list.forEach((asset, idx) => { - let manager = asset.관리자 || asset.사용자 || asset.담당자_정 || '-'; - let name = asset.명칭 || asset.모델명 || '-'; - const tr = document.createElement('tr'); - tr.innerHTML = `${idx+1}${asset.type}${asset.자산코드}${name}${asset.위치||'-'}${manager}${asset.구매일||'-'}${asset.금액||'-'}`; - tbody.appendChild(tr); - }); - } - modal.classList.remove('hidden'); -} - -function openSwDashboardDetail(title: string, list: SoftwareAsset[]) { - const modal = document.getElementById('dashboard-detail-modal'); - if (!modal) return; - const titleEl = document.getElementById('dashboard-detail-modal-title'); - const tbody = document.getElementById('dashboard-detail-tbody'); - if (!titleEl || !tbody) return; - const thead = tbody.closest('table')?.querySelector('thead'); - if (!thead) return; - - titleEl.textContent = title; - thead.innerHTML = `No유형법인제품명수량금액`; - tbody.innerHTML = ''; - list.forEach((sw, idx) => { - const tr = document.createElement('tr'); - tr.innerHTML = `${idx+1}${sw.type}${sw.법인}${sw.제품명}${sw.수량}${sw.금액}`; - tbody.appendChild(tr); - }); - modal.classList.remove('hidden'); -} - -function openSwUsageDetail(title: string, list: SoftwareAsset[]) { - const modal = document.getElementById('dashboard-detail-modal'); - if (!modal) return; - const titleEl = document.getElementById('dashboard-detail-modal-title'); - const tbody = document.getElementById('dashboard-detail-tbody'); - if (!titleEl || !tbody) return; - const thead = tbody.closest('table')?.querySelector('thead'); - if (!thead) return; - - titleEl.textContent = title; - thead.innerHTML = `No법인제품명수량사용중사용가능`; - tbody.innerHTML = ''; - list.forEach((sw, idx) => { - const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; - const avail = (typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10)) - assigned; - const tr = document.createElement('tr'); - tr.innerHTML = `${idx+1}${sw.법인}${sw.제품명}${sw.수량}${assigned}${avail}`; - tbody.appendChild(tr); - }); - modal.classList.remove('hidden'); -}