diff --git a/backup_refactor/index.html b/backup_refactor/index.html new file mode 100644 index 0000000..5208b03 --- /dev/null +++ b/backup_refactor/index.html @@ -0,0 +1,55 @@ + + + + + + + ITAM 자산관리 ERP + + + + + + + +
+ +
+ +
+ + +
+ +
+
+ + + + + diff --git a/backup_refactor/package.json b/backup_refactor/package.json new file mode 100644 index 0000000..8b0150a --- /dev/null +++ b/backup_refactor/package.json @@ -0,0 +1,25 @@ +{ + "name": "hm-itam", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "server": "node server.js", + "db-init": "node db_init.js" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.2.0" + }, + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "lucide": "^0.364.0", + "mysql2": "^3.22.1", + "xlsx": "^0.18.5" + } +} diff --git a/backup_refactor/src/components/Modal/BaseModal.ts b/backup_refactor/src/components/Modal/BaseModal.ts new file mode 100644 index 0000000..7e1c518 --- /dev/null +++ b/backup_refactor/src/components/Modal/BaseModal.ts @@ -0,0 +1,35 @@ +/** + * 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다. + */ +export function initBaseModal() { + const closeAllModals = () => { + const modals = document.querySelectorAll('.modal-overlay'); + modals.forEach(modal => modal.classList.add('hidden')); + }; + + // ESC 키로 닫기 + window.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeAllModals(); + }); + + // 배경(Overlay) 클릭 시 닫기 (동적 생성된 모달 대응을 위해 이벤트 위임 고려 가능하나 일단 단순 구현) + document.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + if (target.classList.contains('modal-overlay')) { + closeAllModals(); + } + }); + + return { closeAllModals }; +} + +/** + * 특정 모달을 엽니다. + * @param modalId 모달 엘리먼트의 ID + */ +export function openModal(modalId: string) { + const modal = document.getElementById(modalId); + if (modal) { + modal.classList.remove('hidden'); + } +} diff --git a/backup_refactor/src/components/Modal/DashboardDetailModal.ts b/backup_refactor/src/components/Modal/DashboardDetailModal.ts new file mode 100644 index 0000000..16c1273 --- /dev/null +++ b/backup_refactor/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/backup_refactor/src/components/Modal/HWModal.ts b/backup_refactor/src/components/Modal/HWModal.ts new file mode 100644 index 0000000..a330711 --- /dev/null +++ b/backup_refactor/src/components/Modal/HWModal.ts @@ -0,0 +1,335 @@ +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; + + const modal = document.getElementById('hw-asset-modal')!; + const form = document.getElementById('hw-asset-form') as HTMLFormElement; + const saveBtn = document.getElementById('btn-save-hw-asset')!; + const revertBtn = document.getElementById('btn-revert-hw-edit')!; + + form.reset(); + form.classList.remove('is-edit-mode'); + form.classList.add('is-view-mode'); + saveBtn.textContent = '수정'; + 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; + (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 || ''; + (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.담당자_정 || asset.관리자 || ''; + (document.getElementById('hw-담당자_부') as HTMLInputElement).value = asset.담당자_부 || ''; + (document.getElementById('hw-품의서명') as HTMLElement).textContent = asset.품의서명 || ''; + + const serverOnly = document.querySelectorAll('.server-only'); + const nonServer = document.querySelectorAll('.non-server'); + const equipGroup = document.getElementById('hw-비품유형-group')!; + + if (asset.type === '서버') { + serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); + nonServer.forEach(el => (el as HTMLElement).style.display = 'none'); + equipGroup.style.display = 'none'; + + (document.getElementById('hw-용도') as HTMLInputElement).value = asset.용도 || ''; + (document.getElementById('hw-상세') as HTMLInputElement).value = asset.상세 || ''; + (document.getElementById('hw-비고') as HTMLInputElement).value = asset.비고 || ''; + (document.getElementById('hw-IP주소') as HTMLInputElement).value = asset.IP주소 || ''; + (document.getElementById('hw-IP2') as HTMLInputElement).value = (asset as any).IP2 || ''; + (document.getElementById('hw-원격접속') as HTMLInputElement).value = asset.원격접속 || ''; + (document.getElementById('hw-서버ID') as HTMLInputElement).value = (asset as any).서버ID || ''; + (document.getElementById('hw-서버PW') as HTMLInputElement).value = (asset as any).서버PW || ''; + (document.getElementById('hw-모니터링') as HTMLInputElement).value = asset.모니터링 || ''; + } else { + serverOnly.forEach(el => (el as HTMLElement).style.display = 'none'); + nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); + + (document.getElementById('hw-명칭') as HTMLInputElement).value = asset.명칭 || ''; + (document.getElementById('hw-구매일') as HTMLInputElement).value = asset.구매일 || ''; + (document.getElementById('hw-금액') as HTMLInputElement).value = asset.금액 || ''; + (document.getElementById('hw-HW사양') as HTMLTextAreaElement).value = asset.HW사양 || ''; + (document.getElementById('hw-IP주소-non-server') as HTMLInputElement).value = asset.IP주소 || ''; + + if (asset.type === '전산비품') { + equipGroup.style.display = 'flex'; + (document.getElementById('hw-비품유형') as HTMLSelectElement).value = asset.비품유형 || '노트북'; + } else { + equipGroup.style.display = 'none'; + } + } +} + +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')!; + const cancelBtn = document.getElementById('btn-cancel-hw-modal')!; + const saveBtn = document.getElementById('btn-save-hw-asset')!; + const revertBtn = document.getElementById('btn-revert-hw-edit')!; + const deleteBtn = document.getElementById('btn-delete-hw-asset')!; + + const closeModal = () => { + modal.classList.add('hidden'); + isEditMode = false; + }; + + const switchToViewMode = () => { + isEditMode = false; + form.classList.remove('is-edit-mode'); + form.classList.add('is-view-mode'); + saveBtn.textContent = '수정'; + revertBtn.classList.add('hidden'); + 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(); }); + + saveBtn.addEventListener('click', () => { + if (!currentAsset) return; + + if (!isEditMode) { + isEditMode = true; + form.classList.remove('is-view-mode'); + form.classList.add('is-edit-mode'); + saveBtn.textContent = '저장'; + 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; + + const updated: HardwareAsset = { + ...currentAsset, + 법인: (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, + 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, + }; + + if (type === '서버') { + updated.용도 = (document.getElementById('hw-용도') as HTMLInputElement).value; + updated.상세 = (document.getElementById('hw-상세') as HTMLInputElement).value; + updated.비고 = (document.getElementById('hw-비고') as HTMLInputElement).value; + updated.IP주소 = (document.getElementById('hw-IP주소') as HTMLInputElement).value; + (updated as any).IP2 = (document.getElementById('hw-IP2') as HTMLInputElement).value; + updated.원격접속 = (document.getElementById('hw-원격접속') as HTMLInputElement).value; + (updated as any).서버ID = (document.getElementById('hw-서버ID') as HTMLInputElement).value; + (updated as any).서버PW = (document.getElementById('hw-서버PW') as HTMLInputElement).value; + updated.모니터링 = (document.getElementById('hw-모니터링') as HTMLInputElement).value; + } else { + updated.명칭 = (document.getElementById('hw-명칭') as HTMLInputElement).value; + updated.구매일 = (document.getElementById('hw-구매일') as HTMLInputElement).value; + updated.금액 = (document.getElementById('hw-금액') as HTMLInputElement).value; + updated.HW사양 = (document.getElementById('hw-HW사양') as HTMLTextAreaElement).value; + updated.IP주소 = (document.getElementById('hw-IP주소-non-server') as HTMLInputElement).value; + + if (type === '전산비품') { + updated.비품유형 = (document.getElementById('hw-비품유형') as HTMLSelectElement).value; + } + } + + const idx = state.masterData.hw.findIndex(a => a.id === assetId); + if (idx > -1) { + state.masterData.hw[idx] = updated; + renderTable(document.getElementById('main-content')!); + switchToViewMode(); + } + }); + + deleteBtn.addEventListener('click', () => { + if (!currentAsset) return; + if (confirm('정말로 이 자산을 삭제하시겠습니까?')) { + state.masterData.hw = state.masterData.hw.filter(a => a.id !== currentAsset!.id); + renderTable(document.getElementById('main-content')!); + closeModal(); + } + }); +} diff --git a/backup_refactor/src/components/Modal/PCModal.ts b/backup_refactor/src/components/Modal/PCModal.ts new file mode 100644 index 0000000..0061d71 --- /dev/null +++ b/backup_refactor/src/components/Modal/PCModal.ts @@ -0,0 +1,342 @@ +import { state } from '../../core/state'; +import { HardwareAsset, HardwareLog } from '../../core/excelHandler'; +import { openModal } from './BaseModal'; + +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 btnRevertEdit = document.getElementById('btn-revert-pc-edit') as HTMLButtonElement; + const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement; + const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement; + const btnCloseHeader = document.getElementById('btn-close-pc-modal') as HTMLButtonElement; + const btnCloseFooter = document.getElementById('btn-close-pc-footer') as HTMLButtonElement; + + let isEditMode = false; + let currentAsset: HardwareAsset | null = null; + + const setEditMode = (edit: boolean) => { + isEditMode = edit; + if (edit) { + pcForm.classList.add('is-edit-mode'); + pcForm.classList.remove('is-view-mode'); + btnSavePc.textContent = '저장'; + btnRevertEdit.classList.remove('hidden'); + btnCloseFooter.classList.add('hidden'); + } else { + pcForm.classList.add('is-view-mode'); + pcForm.classList.remove('is-edit-mode'); + btnSavePc.textContent = '수정'; + btnRevertEdit.classList.add('hidden'); + btnCloseFooter.classList.remove('hidden'); + if (currentAsset) fillFormData(currentAsset); + } + }; + + function fillFormData(asset: HardwareAsset) { + (document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id; + (document.getElementById('pc-법인') as HTMLSelectElement).value = asset.법인; + (document.getElementById('pc-자산코드') as HTMLInputElement).value = asset.자산코드; + (document.getElementById('pc-사용자') as HTMLInputElement).value = asset.사용자 || ''; + (document.getElementById('pc-위치') as HTMLInputElement).value = asset.위치 || ''; + (document.getElementById('pc-CPU') as HTMLInputElement).value = asset.CPU || ''; + (document.getElementById('pc-GPU') as HTMLInputElement).value = asset.GPU || ''; + (document.getElementById('pc-RAM') as HTMLInputElement).value = asset.RAM || ''; + (document.getElementById('pc-SSD1') as HTMLInputElement).value = asset.SSD1 || ''; + (document.getElementById('pc-SSD2') as HTMLInputElement).value = asset.SSD2 || ''; + (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.금액 || ''; + (document.getElementById('pc-납품업체') as HTMLInputElement).value = asset.납품업체 || ''; + (document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `첨부: ${asset.품의서명}` : ''; + } + + btnRevertEdit?.addEventListener('click', () => setEditMode(false)); + btnCloseHeader?.addEventListener('click', closeModals); + btnCloseFooter?.addEventListener('click', closeModals); + + btnSavePc?.addEventListener('click', (e) => { + e.preventDefault(); + if (!isEditMode) { + setEditMode(true); + return; + } + + if (!pcForm.checkValidity()) { pcForm.reportValidity(); return; } + + // ... (저장 로직 유지) + 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 newAsset: HardwareAsset = { + id: id || Math.random().toString(36).substring(2, 9), + type: '개인PC', + 법인: (document.getElementById('pc-법인') as HTMLSelectElement).value, + 자산코드: (document.getElementById('pc-자산코드') as HTMLInputElement).value, + 명칭: '', + 위치: (document.getElementById('pc-위치') as HTMLInputElement).value, + 관리자: '', 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, + RAM: (document.getElementById('pc-RAM') as HTMLInputElement).value, + SSD1: (document.getElementById('pc-SSD1') as HTMLInputElement).value, + SSD2: (document.getElementById('pc-SSD2') as HTMLInputElement).value, + HDD1: (document.getElementById('pc-HDD1') as HTMLInputElement).value, + HDD2: (document.getElementById('pc-HDD2') as HTMLInputElement).value, + 구매일: (document.getElementById('pc-구매일') as HTMLInputElement).value, + 금액: (document.getElementById('pc-금액') as HTMLInputElement).value, + 품의서명 + }; + + if (id) { + const idx = state.masterData.hw.findIndex(a => a.id === id); + if(idx !== -1) { + const oldAsset = state.masterData.hw[idx]; + const changes = getChangeDetails(oldAsset, newAsset); + if (changes) { + state.masterData.logs.push({ + id: Math.random().toString(36).substring(2, 9), + assetId: id, + date: new Date().toLocaleString(), + details: changes, + user: '관리자' + }); + } + state.masterData.hw[idx] = newAsset; + } + } else { + state.masterData.hw.push(newAsset); + } + + closeModals(); + 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(); + } + }); +} + +export function openPcModal(asset?: HardwareAsset) { + const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement; + const deleteBtn = document.getElementById('btn-delete-pc-asset')!; + const historyArea = document.querySelector('.modal-history-area') as HTMLElement; + + openModal('pc-asset-modal'); + pcForm.reset(); + + if (asset) { + document.getElementById('pc-modal-title')!.textContent = '개인PC 상세 정보 수정'; + deleteBtn.style.display = 'block'; + if (historyArea) historyArea.style.display = 'flex'; + + (document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id; + (document.getElementById('pc-법인') as HTMLSelectElement).value = asset.법인; + (document.getElementById('pc-자산코드') as HTMLInputElement).value = asset.자산코드; + (document.getElementById('pc-사용자') as HTMLInputElement).value = asset.사용자 || ''; + (document.getElementById('pc-위치') as HTMLInputElement).value = asset.위치 || ''; + (document.getElementById('pc-CPU') as HTMLInputElement).value = asset.CPU || ''; + (document.getElementById('pc-GPU') as HTMLInputElement).value = asset.GPU || ''; + (document.getElementById('pc-RAM') as HTMLInputElement).value = asset.RAM || ''; + (document.getElementById('pc-SSD1') as HTMLInputElement).value = asset.SSD1 || ''; + (document.getElementById('pc-SSD2') as HTMLInputElement).value = asset.SSD2 || ''; + (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.금액 || ''; + (document.getElementById('pc-납품업체') as HTMLInputElement).value = asset.납품업체 || ''; + (document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `첨부: ${asset.품의서명}` : ''; + + renderHistory(asset.id); + } else { + document.getElementById('pc-modal-title')!.textContent = '신규 개인PC 자산 추가'; + deleteBtn.style.display = 'none'; + if (historyArea) historyArea.style.display = 'none'; + + (document.getElementById('pc-asset-id') as HTMLInputElement).value = ''; + (document.getElementById('pc-법인') as HTMLSelectElement).value = '한맥'; + (document.getElementById('pc-품의서명') as HTMLElement).innerText = ''; + } +} + +function getChangeDetails(oldAsset: HardwareAsset, newAsset: HardwareAsset): string { + const changes: string[] = []; + const fields = [ + { key: '법인', label: '법인' }, + { key: '자산코드', label: '자산코드' }, + { key: '사용자', label: '사용자' }, + { key: '위치', label: '위치' }, + { key: 'CPU', label: 'CPU' }, + { key: 'GPU', label: 'GPU' }, + { key: 'RAM', label: 'RAM' }, + { key: 'SSD1', label: 'SSD1' }, + { key: 'SSD2', label: 'SSD2' }, + { key: 'HDD1', label: 'HDD1' }, + { key: 'HDD2', label: 'HDD2' }, + { key: '구매일', label: '구매일' }, + { key: '금액', label: '금액' }, + { key: '납품업체', label: '납품업체' }, + { key: '품의서명', label: '품의서' }, + ]; + + 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; + + const logs = state.masterData.logs + .filter(l => l.assetId === assetId) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + if (logs.length === 0) { + historyList.innerHTML = '
이력이 없습니다.
'; + return; + } + + historyList.innerHTML = logs.map(log => ` +
+
${log.date}
+
수정자: ${log.user}
+
${log.details.replace(/\\n/g, '
')}
+
+ `).join(''); +} diff --git a/backup_refactor/src/components/Modal/SWModal.ts b/backup_refactor/src/components/Modal/SWModal.ts new file mode 100644 index 0000000..ecfb78f --- /dev/null +++ b/backup_refactor/src/components/Modal/SWModal.ts @@ -0,0 +1,222 @@ +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 btnRevertEdit = document.getElementById('btn-revert-sw-edit') as HTMLButtonElement; + const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement; + const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement; + const btnCloseHeader = document.getElementById('btn-close-sw-modal') as HTMLButtonElement; + const btnCloseFooter = document.getElementById('btn-close-sw-footer') as HTMLButtonElement; + + let isEditMode = false; + let currentAsset: SoftwareAsset | null = null; + + const setEditMode = (edit: boolean) => { + isEditMode = edit; + if (edit) { + swForm.classList.add('is-edit-mode'); + swForm.classList.remove('is-view-mode'); + btnSaveSw.textContent = '저장'; + btnRevertEdit.classList.remove('hidden'); + btnCloseFooter.classList.add('hidden'); + } else { + swForm.classList.add('is-view-mode'); + swForm.classList.remove('is-edit-mode'); + btnSaveSw.textContent = '수정'; + btnRevertEdit.classList.add('hidden'); + btnCloseFooter.classList.remove('hidden'); + if (currentAsset) fillFormData(currentAsset); + } + }; + + function fillFormData(asset: SoftwareAsset) { + (document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id; + (document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type; + (document.getElementById('sw-분야') as HTMLSelectElement).value = asset.분야 || '업무공통'; + (document.getElementById('sw-법인') as HTMLSelectElement).value = asset.법인; + (document.getElementById('sw-부서') as HTMLInputElement).value = asset.부서 || ''; + (document.getElementById('sw-제품명') as HTMLInputElement).value = asset.제품명; + (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.금액 || ''; + (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.비고 || ''; + } + + btnRevertEdit?.addEventListener('click', () => setEditMode(false)); + btnCloseHeader?.addEventListener('click', closeModals); + btnCloseFooter?.addEventListener('click', closeModals); + + btnSaveSw?.addEventListener('click', (e) => { + e.preventDefault(); + if (!isEditMode) { + setEditMode(true); + return; + } + + if (!swForm.checkValidity()) { swForm.reportValidity(); return; } + + const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value; + const newAsset: SoftwareAsset = { + id: id || Math.random().toString(36).substring(2, 9), + type: (document.getElementById('sw-asset-type') as HTMLInputElement).value, + 분야: (document.getElementById('sw-분야') as HTMLSelectElement).value, + 법인: (document.getElementById('sw-법인') as HTMLSelectElement).value, + 부서: (document.getElementById('sw-부서') as HTMLInputElement).value, + 제품명: (document.getElementById('sw-제품명') as HTMLInputElement).value, + 구매일: (document.getElementById('sw-구매일') as HTMLInputElement).value, + 구독일: (document.getElementById('sw-구독일') as HTMLInputElement).value, + 유지보수여부: (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked, + 금액: (document.getElementById('sw-금액') as HTMLInputElement).value, + 수량: parseInt((document.getElementById('sw-수량') as HTMLInputElement).value || '1', 10), + 계정명: (document.getElementById('sw-계정명') as HTMLInputElement).value, + 납품업체: (document.getElementById('sw-납품업체') as HTMLInputElement).value, + 비고: (document.getElementById('sw-비고') as HTMLInputElement).value, + }; + + if (id) { + const idx = state.masterData.sw.findIndex(a => a.id === id); + if(idx !== -1) state.masterData.sw[idx] = newAsset; + } else { + state.masterData.sw.push(newAsset); + } + + closeModals(); + renderContent(); + }); + + btnDeleteSw?.addEventListener('click', (e) => { + e.preventDefault(); + const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value; + if (confirm('삭제하시겠습니까?')) { + state.masterData.sw = state.masterData.sw.filter(a => a.id !== id); + closeModals(); + renderContent(); + } + }); +} + +export function openSwModal(asset?: SoftwareAsset) { + currentAsset = asset || null; + const swForm = document.getElementById('sw-asset-form') as HTMLFormElement; + const deleteBtn = document.getElementById('btn-delete-sw-asset')!; + + openModal('sw-asset-modal'); + swForm.reset(); + + const subGroup = document.getElementById('sw-구독일-group')!; + const permGroup = document.getElementById('sw-유지보수-group')!; + if (state.activeSubTab === '구독SW') { + subGroup.style.display = 'flex'; + permGroup.style.display = 'none'; + } else { + subGroup.style.display = 'none'; + permGroup.style.display = 'flex'; + } + + if (asset) { + document.getElementById('sw-modal-title')!.textContent = `${state.activeSubTab} 상세 정보 수정`; + deleteBtn.style.display = 'block'; + fillFormData(asset); + setEditMode(false); + } else { + 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; + setEditMode(true); + } +} diff --git a/backup_refactor/src/components/Modal/SWUserModal.ts b/backup_refactor/src/components/Modal/SWUserModal.ts new file mode 100644 index 0000000..f4f0668 --- /dev/null +++ b/backup_refactor/src/components/Modal/SWUserModal.ts @@ -0,0 +1,241 @@ +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()); + + 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(); + }); + + 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 = '할당된 사용자가 없습니다.'; + return; + } + + tempSwUsers.forEach((user, idx) => { + const tr = document.createElement('tr'); + const deptTeam = [user.부서, user.팀].filter(Boolean).join(' / ') || '-'; + const attachIcon = user.신청서명 ? `` : '-'; + + tr.innerHTML = ` + ${user.법인} + ${deptTeam} + ${user.직위 || '-'} + ${user.이름} + ${user.사용기간 || '-'} + ${attachIcon} + + + + + `; + tbody.appendChild(tr); + }); + + createIcons({ icons: { Edit2, X, Paperclip } }); + + tbody.querySelectorAll('.btn-edit-user').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!); + openUserEditModal(idx); + }); + }); + + tbody.querySelectorAll('.btn-remove-user').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt((e.currentTarget as HTMLButtonElement).getAttribute('data-idx')!); + tempSwUsers.splice(idx, 1); + renderUserList(); + }); + }); +} + +export function openSwUserModal(asset: SoftwareAsset) { + openModal('sw-user-modal'); + currentSwUserAssetId = asset.id; + tempSwUsers = state.masterData.swUsers.filter(u => u.swId === asset.id).map(u => ({...u})); + renderUserList(); +} + +function openUserEditModal(idx: number) { + const editModal = document.getElementById('sw-user-edit-modal')!; + editModal.classList.remove('hidden'); + (document.getElementById('edit-user-idx') as HTMLInputElement).value = String(idx); + + if (idx === -1) { + document.getElementById('sw-user-edit-modal-title')!.innerText = '새 사용자 추가'; + (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 = ''; + (document.getElementById('new-user-사용기간') as HTMLInputElement).value = ''; + (document.getElementById('new-user-신청서') as HTMLInputElement).value = ''; + document.getElementById('new-user-신청서명')!.innerText = ''; + } else { + 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.부서; + (document.getElementById('new-user-팀') as HTMLInputElement).value = u.팀; + (document.getElementById('new-user-직위') as HTMLInputElement).value = u.직위; + (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.신청서명}` : ''; + } +} + +function saveUserEdit() { + const idx = parseInt((document.getElementById('edit-user-idx') as HTMLInputElement).value); + const 이름 = (document.getElementById('new-user-이름') as HTMLInputElement).value.trim(); + if (!이름) { alert('이름을 입력해주세요.'); return; } + + const fileInput = document.getElementById('new-user-신청서') as HTMLInputElement; + let 신청서명 = ''; + if (fileInput.files && fileInput.files.length > 0) { + 신청서명 = fileInput.files[0].name; + } else if (idx !== -1) { + 신청서명 = tempSwUsers[idx].신청서명; + } + + 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(userData); + else tempSwUsers[idx] = userData; + + document.getElementById('sw-user-edit-modal')?.classList.add('hidden'); + renderUserList(); +} diff --git a/backup_refactor/src/components/Modal/StorageModal.ts b/backup_refactor/src/components/Modal/StorageModal.ts new file mode 100644 index 0000000..1a88b70 --- /dev/null +++ b/backup_refactor/src/components/Modal/StorageModal.ts @@ -0,0 +1,161 @@ +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 btnRevertEdit = document.getElementById('btn-revert-storage-edit') as HTMLButtonElement; + const btnSaveStorage = document.getElementById('btn-save-storage-asset') as HTMLButtonElement; + const btnDeleteStorage = document.getElementById('btn-delete-storage-asset') as HTMLButtonElement; + const btnCloseHeader = document.getElementById('btn-close-storage-modal') as HTMLButtonElement; + const btnCloseFooter = document.getElementById('btn-close-storage-footer') as HTMLButtonElement; + + let isEditMode = false; + let currentAsset: HardwareAsset | null = null; + + const setEditMode = (edit: boolean) => { + isEditMode = edit; + if (edit) { + storageForm.classList.add('is-edit-mode'); + storageForm.classList.remove('is-view-mode'); + btnSaveStorage.textContent = '저장'; + btnRevertEdit.classList.remove('hidden'); + btnCloseFooter.classList.add('hidden'); + } else { + storageForm.classList.add('is-view-mode'); + storageForm.classList.remove('is-edit-mode'); + btnSaveStorage.textContent = '수정'; + btnRevertEdit.classList.add('hidden'); + btnCloseFooter.classList.remove('hidden'); + if (currentAsset) fillFormData(currentAsset); + } + }; + + function fillFormData(asset: HardwareAsset) { + (document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id; + (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-IP주소') as HTMLInputElement).value = asset.IP주소 || ''; + (document.getElementById('storage-구매일') as HTMLInputElement).value = asset.구매일 || ''; + (document.getElementById('storage-금액') as HTMLInputElement).value = asset.금액 || ''; + } + + btnRevertEdit?.addEventListener('click', () => setEditMode(false)); + btnCloseHeader?.addEventListener('click', closeModals); + btnCloseFooter?.addEventListener('click', closeModals); + + btnSaveStorage?.addEventListener('click', (e) => { + e.preventDefault(); + if (!isEditMode) { + setEditMode(true); + return; + } + + if (!storageForm.checkValidity()) { storageForm.reportValidity(); return; } + + const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value; + const newAsset: HardwareAsset = { + id: id || Math.random().toString(36).substring(2, 9), + type: '스토리지', + 법인: (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, + 모델명: (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, + 관리자: '', MACaddress: '', HW사양: '', OS: '', 납품업체: '', 품의서명: '' + }; + + if (id) { + const idx = state.masterData.hw.findIndex(a => a.id === id); + if(idx !== -1) state.masterData.hw[idx] = newAsset; + } else { + state.masterData.hw.push(newAsset); + } + + closeModals(); + renderContent(); + }); + + btnDeleteStorage?.addEventListener('click', (e) => { + e.preventDefault(); + const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value; + if (confirm('삭제하시겠습니까?')) { + state.masterData.hw = state.masterData.hw.filter(a => a.id !== id); + closeModals(); + renderContent(); + } + }); +} + +export function openStorageModal(asset?: HardwareAsset) { + currentAsset = asset || null; + const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement; + const deleteBtn = document.getElementById('btn-delete-storage-asset')!; + + openModal('storage-asset-modal'); + storageForm.reset(); + + if (asset) { + document.getElementById('storage-modal-title')!.textContent = '스토리지 상세 정보 수정'; + deleteBtn.style.display = 'block'; + fillFormData(asset); + setEditMode(false); + } else { + document.getElementById('storage-modal-title')!.textContent = '신규 스토리지 자산 추가'; + deleteBtn.style.display = 'none'; + (document.getElementById('storage-asset-id') as HTMLInputElement).value = ''; + setEditMode(true); + } +} diff --git a/backup_refactor/src/components/Navigation.ts b/backup_refactor/src/components/Navigation.ts new file mode 100644 index 0000000..8ed0688 --- /dev/null +++ b/backup_refactor/src/components/Navigation.ts @@ -0,0 +1,80 @@ +import { state } from '../core/state'; + +const MENU_CONFIG = { + hw: { + label: '하드웨어', + tabs: ['대시보드', '개인PC', '서버', '스토리지', '전산비품'] + }, + sw: { + label: '소프트웨어', + tabs: ['대시보드', '구독SW', '영구SW'] + }, + ops: { + label: '운영 서비스', + tabs: ['대시보드', '서비스현황', '백업관리', '보안점검'] + } +}; + +export function renderNavigation(onTabChange: (tab: string) => void) { + const navContainer = document.getElementById('main-nav')!; + const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement; + + const render = () => { + navContainer.innerHTML = ''; + + (Object.keys(MENU_CONFIG) as Array).forEach(catKey => { + const config = MENU_CONFIG[catKey]; + const isActive = state.activeCategory === catKey; + + const group = document.createElement('div'); + group.className = `nav-group ${isActive ? 'active is-showing-shelf' : ''}`; + + // 메인 카테고리 트리거 + const trigger = document.createElement('div'); + trigger.className = 'gnb-trigger'; + trigger.textContent = config.label; + + trigger.addEventListener('click', () => { + if (state.activeCategory !== catKey) { + state.activeCategory = catKey; + state.activeSubTab = '대시보드'; + if (btnAddAsset) btnAddAsset.classList.add('hidden'); + render(); + onTabChange('대시보드'); + } + }); + group.appendChild(trigger); + + // 하위 탭 선반 (Shelf) + const shelf = document.createElement('div'); + shelf.className = 'lnb-shelf'; + + config.tabs.forEach(tab => { + const item = document.createElement('div'); + item.className = `lnb-item ${isActive && state.activeSubTab === tab ? 'active' : ''}`; + item.textContent = tab; + + item.addEventListener('click', (e) => { + e.stopPropagation(); + state.activeCategory = catKey; + state.activeSubTab = tab; + + if (btnAddAsset) { + if (tab === '대시보드') btnAddAsset.classList.add('hidden'); + else btnAddAsset.classList.remove('hidden'); + } + + render(); + onTabChange(tab); + }); + shelf.appendChild(item); + }); + group.appendChild(shelf); + + // 마우스 오버 시 다른 그룹의 선반은 가리고 내 것만 보여주는 스타일은 CSS에서 처리함 + navContainer.appendChild(group); + }); + }; + + render(); +} diff --git a/backup_refactor/src/core/dummyDataGenerator.ts b/backup_refactor/src/core/dummyDataGenerator.ts new file mode 100644 index 0000000..55f3b43 --- /dev/null +++ b/backup_refactor/src/core/dummyDataGenerator.ts @@ -0,0 +1,232 @@ +import { MasterAssetData, HardwareAsset, SoftwareAsset, SWUser } from './excelHandler'; + +const corps = ['한맥', '삼안', '바론']; +const users = ['홍길동', '김철수', '이영희', '박지훈', '김팀장', '신유진', '윤대웅', '마리아']; +const depts = ['설계팀', '기술팀', '경영지원팀', '영업팀']; + +function rand(arr: any[]) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +function randDate(startYear: number, endYear: number) { + const y = Math.floor(Math.random() * (endYear - startYear + 1)) + startYear; + const m = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0'); + const d = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0'); + return `${y}-${m}-${d}`; +} + +function randUser() { // 25% 확률로 유휴자산 할당 + return Math.random() < 0.25 ? '' : rand(users); +} + +export function generateDummyData(): MasterAssetData { + const hw: HardwareAsset[] = []; + const sw: SoftwareAsset[] = []; + const swUsers: SWUser[] = []; + + // 1. 개인PC 50개 + for (let i = 1; i <= 50; i++) { + const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026 + hw.push({ + id: Math.random().toString(36).substring(2, 9), + type: '개인PC', + 법인: rand(corps), + 자산코드: `HM-PC-${purchaseYear}-${String(i).padStart(3, '0')}`, + 명칭: '', + 위치: `${rand(['본사', '지사'])} ${Math.floor(Math.random()*5)+1}층`, + 사용자: randUser(), + CPU: rand(['i5-10400', 'i7-12700', 'Ryzen 5', 'Ryzen 7']), + GPU: rand(['-', 'GTX 1660', 'RTX 3060', 'RTX 4070']), + RAM: rand(['16GB', '32GB']), + SSD1: rand(['256GB', '512GB', '1TB']), + SSD2: '', + HDD1: rand(['-', '1TB', '2TB']), + HDD2: '', + 구매일: randDate(purchaseYear, purchaseYear), + 금액: String(Math.floor(Math.random()*100 + 50) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','), + 납품업체: rand(['다나와', '컴퓨존', '오피스디포']), + 품의서명: '', + 관리자: '', IP주소: '', MACaddress: '', OS: '', HW사양: '' + }); + } + + // 2. 서버 20개 + for (let i = 1; i <= 20; i++) { + const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026 + hw.push({ + id: Math.random().toString(36).substring(2, 9), + type: '서버', + 법인: rand(corps), + 자산코드: `HM-SV-${purchaseYear}-${String(i).padStart(3, '0')}`, + 명칭: `웹/DB 서버 #${i}`, + 용도: rand(['웹 서버', 'DB 서버', '백업 서버', '개발 서버']), + storage유형: rand(['물리', 'VM']), + 위치: rand(['IDC 1센터', 'IDC 2센터', '본사 전산실']), + 관리자: rand(users), + 담당자_정: rand(users), + 담당자_부: rand(users), + IP주소: `192.168.10.${i}`, + 원격접속: `ssh://192.168.10.${i}:22`, + MACaddress: '00:11:22:33:44:' + String(i).padStart(2, '0'), + OS: rand(['Windows Server 2019', 'Ubuntu 22.04 LTS', 'CentOS 7']), + 모델명: rand(['Dell PowerEdge R740', 'HP ProLiant DL380', 'Lenovo ThinkSystem']), + CPU: rand(['Xeon Silver 4210', 'Xeon Gold 6248', 'EPYC 7702']), + RAM: rand(['64GB', '128GB', '256GB']), + GPU: rand(['-', 'RTX A4000', 'Tesla V100']), + SSD1: rand(['512GB SSD', '1TB NVMe']), + SSD2: rand(['-', '1TB SSD', '2TB SSD']), + HDD1: rand(['-', '4TB HDD', '8TB HDD']), + 모니터링: rand(['Zabbix', 'Grafana', 'PRTG']), + 비고: i % 5 === 0 ? '정기 점검 대상' : '-', + HW사양: 'Xeon 16Core, 64GB RAM', + 구매일: randDate(purchaseYear, purchaseYear), + 금액: '5,000,000', + 납품업체: '서버뱅크', + 품의서명: '' + }); + } + + // 3. 스토리지 20개 + for (let i = 1; i <= 20; i++) { + const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026 + hw.push({ + id: Math.random().toString(36).substring(2, 9), + type: '스토리지', + 법인: rand(corps), + storage유형: rand(['NAS', 'DAS']), + 자산코드: `HM-ST-${purchaseYear}-${String(i).padStart(3, '0')}`, + 명칭: `백업 스토리지 #${i}`, + 위치: '전산실', + 모델명: rand(['Synology DS920+', 'QNAP TS-453D']), + 용량: rand(['16TB', '32TB', '64TB']), + 담당자_정: randUser(), + 담당자_부: rand(users), + IP주소: `192.168.20.${i}`, + MACaddress: '', + 구매일: randDate(purchaseYear, purchaseYear), + 금액: '1,500,000', + 납품업체: '스토리지넷', + 품의서명: '', + 관리자: '', OS: '', HW사양: '' + }); + } + + // 4. 전산비품 (노트북, 태블릿, 휴대폰 각각 5개씩) + const equips = [ + { type: '노트북', code: 'NB', name: 'LG 그램 16인치', price: '1,800,000' }, + { type: '태블릿', code: 'TB', name: '아이패드 프로 12.9', price: '1,500,000' }, + { type: '휴대폰', code: 'PH', name: '갤럭시 S24', price: '1,200,000' } + ]; + equips.forEach((eq) => { + for (let i = 1; i <= 5; i++) { + const purchaseYear = Math.floor(Math.random() * 8) + 2019; // 2019~2026 + hw.push({ + id: Math.random().toString(36).substring(2, 9), + type: '전산비품', + 법인: rand(corps), + 비품유형: eq.type, + 자산코드: `HM-${eq.code}-${purchaseYear}-${String(i).padStart(3, '0')}`, + 명칭: eq.name, + 위치: rand(['본사', '지사']), + 관리자: randUser(), + 구매일: randDate(purchaseYear, purchaseYear), + 금액: eq.price, + 납품업체: '브랜드 총판', + 품의서명: '', + IP주소: '', MACaddress: '', OS: '', HW사양: '' + }); + } + }); + + // 5. 구독형 S/W 40개 + for (let i = 1; i <= 40; i++) { + const swId = Math.random().toString(36).substring(2, 9); + const purchaseYear = Math.random() < 0.3 ? 2026 : 2024; + + let isExpiring = Math.random() < 0.25; + let endDt = new Date(); + if (isExpiring) { + endDt.setDate(endDt.getDate() + Math.floor(Math.random() * 25) + 1); // 1~25일 뒤 만료 + } else { + endDt.setMonth(endDt.getMonth() + Math.floor(Math.random() * 11) + 2); // 넉넉히 남음 + } + const endStr = `${endDt.getFullYear()}.${String(endDt.getMonth()+1).padStart(2,'0')}.${String(endDt.getDate()).padStart(2,'0')}`; + + sw.push({ + id: swId, + type: '구독SW', + 분야: rand(['업무공통', '개발S/W', '디자인', '설계S/W']), + 법인: rand(corps), + 부서: rand(depts), + 제품명: rand(['Adobe CC All Apps', 'Microsoft 365', 'Slack Pro', 'Notion Team']), + 구매일: `${purchaseYear}-01-01`, + 구독일: `${purchaseYear}.01.01 ~ ${endStr}`, + 금액: String(Math.floor(Math.random() * 100 + 10) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','), + 수량: Math.floor(Math.random() * 5) + 3, // 3~7 + 계정명: `user${i}@hm.com`, + 납품업체: '총판', + 비고: '연간구독' + }); + + const assignCount = Math.floor(Math.random() * 2) + 1; + for (let j=0; j { + let hd = HW_HEADERS; + let wscols: any[] = []; + + if (tab === '개인PC') { + hd = PC_HEADERS; + 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}]; + } 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}]; + } else { + hd = HW_HEADERS; + wscols = [{wch:15}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:40}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:25}]; + } + + const ws = XLSX.utils.aoa_to_sheet([hd]); + ws['!cols'] = wscols; + XLSX.utils.book_append_sheet(wb, ws, tab); + }); + + SW_TABS.forEach(tab => { + let hd = tab === '구독SW' ? SUB_SW_HEADERS : PERM_SW_HEADERS; + const ws = XLSX.utils.aoa_to_sheet([hd]); + ws['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}]; + XLSX.utils.book_append_sheet(wb, ws, tab); + }); + + const swUserWs = XLSX.utils.aoa_to_sheet([SW_USER_HEADERS]); + swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}]; + XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자'); + + const historyWs = XLSX.utils.aoa_to_sheet([HISTORY_HEADERS]); + historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}]; + XLSX.utils.book_append_sheet(wb, historyWs, 'History'); + + XLSX.writeFile(wb, 'itam_assets_template.xlsx'); +} + +/** + * 마스터 데이터를 여러 시트로 쪼개서 내보내기 + */ +export function exportToExcel(masterData: MasterAssetData) { + const wb = XLSX.utils.book_new(); + + HW_TABS.forEach(tab => { + const targetAssets = masterData.hw.filter(a => a.type === tab); + let wsData; + let colsConfig; + + if (tab === '개인PC') { + wsData = [ + PC_HEADERS, + ...targetAssets.map(a => [a.법인, a.자산코드, a.사용자, a.위치, a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.구매일, a.금액, a.납품업체, a.품의서명]) + ]; + colsConfig = [{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 === '서버') { + 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.비고 || '']) + ]; + 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}]; + } else if (tab === '스토리지') { + wsData = [ + STORAGE_HEADERS, + ...targetAssets.map(a => [a.법인, a.storage유형, a.자산코드, a.명칭, a.위치, a.모델명, a.용량, a.담당자_정, a.담당자_부, a.IP주소, a.MACaddress, a.구매일, a.금액, a.납품업체, a.품의서명]) + ]; + colsConfig = [{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}]; + } else { + wsData = [ + HW_HEADERS, + ...targetAssets.map(a => [a.법인, a.자산코드, a.명칭, a.위치, a.관리자, a.IP주소, a.MACaddress, a.HW사양, a.OS, a.구매일, a.금액, a.납품업체, a.품의서명]) + ]; + colsConfig = [{wch:15}, {wch:20}, {wch:25}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:40}, {wch:20}, {wch:15}, {wch:15}, {wch:20}, {wch:25}]; + } + + const ws = XLSX.utils.aoa_to_sheet(wsData); + ws['!cols'] = colsConfig; + XLSX.utils.book_append_sheet(wb, ws, tab); + }); + + SW_TABS.forEach(tab => { + const targetAssets = masterData.sw.filter(a => a.type === tab); + let wsData; + if (tab === '구독SW') { + wsData = [ + SUB_SW_HEADERS, + ...targetAssets.map(a => [a.id, a.분야||'', a.법인, a.부서||'', a.제품명, a.구매일, a.구독일, a.금액, a.수량, a.계정명, a.납품업체, a.비고]) + ]; + } else { + wsData = [ + PERM_SW_HEADERS, + ...targetAssets.map(a => [a.id, a.분야||'', a.법인, a.부서||'', a.제품명, a.구매일, a.유지보수여부 ? 'Y' : 'N', a.금액, a.수량, a.계정명, a.납품업체, a.비고]) + ]; + } + const ws = XLSX.utils.aoa_to_sheet(wsData); + ws['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}, {wch:10}, {wch:20}, {wch:20}, {wch:30}]; + XLSX.utils.book_append_sheet(wb, ws, tab); + }); + + const swUserWsData = [ + SW_USER_HEADERS, + ...masterData.swUsers.map(u => [u.id, u.swId, u.법인, u.부서, u.팀, u.직위, u.이름, u.사용기간, u.신청서명]) + ]; + const swUserWs = XLSX.utils.aoa_to_sheet(swUserWsData); + swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}]; + XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자'); + + const historyWsData = [ + HISTORY_HEADERS, + ...masterData.logs.map(l => [l.id, l.assetId, l.date, l.details, l.user]) + ]; + const historyWs = XLSX.utils.aoa_to_sheet(historyWsData); + historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}]; + XLSX.utils.book_append_sheet(wb, historyWs, 'History'); + + const dateStr = new Date().toISOString().split('T')[0]; + XLSX.writeFile(wb, `itam_assets_master_${dateStr}.xlsx`); +} + +export async function parseExcel(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = e.target?.result; + const workbook = XLSX.read(data, { type: 'binary' }); + const hwAssets: HardwareAsset[] = []; + const swAssets: SoftwareAsset[] = []; + const swUsers: SWUser[] = []; + const logs: HardwareLog[] = []; + + workbook.SheetNames.forEach(sheetName => { + const worksheet = workbook.Sheets[sheetName]; + const json = XLSX.utils.sheet_to_json(worksheet) as any[]; + + if (HW_TABS.includes(sheetName)) { + json.forEach(row => { + if (sheetName === '개인PC') { + hwAssets.push({ + id: Math.random().toString(36).substring(2, 9), + type: sheetName, + 법인: row['법인'] || '', + 자산코드: row['자산코드'] || '', + 명칭: '', + 위치: row['위치'] || '', + 사용자: row['사용자'] || '', + 관리자: '', IP주소: '', MACaddress: '', HW사양: '', OS: '', + CPU: row['CPU'] || '', GPU: row['GPU'] || '', RAM: row['RAM'] || '', + SSD1: row['SSD1'] || '', SSD2: row['SSD2'] || '', HDD1: row['HDD1'] || '', HDD2: row['HDD2'] || '', + 구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '', + 납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '', + }); + } else if (sheetName === '서버') { + hwAssets.push({ + id: Math.random().toString(36).substring(2, 9), + type: sheetName, + 법인: 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'] || '', + 모델명: 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사양: '', 구매일: '', 금액: '', 납품업체: '', 품의서명: '', + }); + } else if (sheetName === '스토리지') { + hwAssets.push({ + id: Math.random().toString(36).substring(2, 9), + type: sheetName, + 법인: row['법인'] || '', 자산코드: row['자산코드'] || '', 명칭: row['명칭'] || '', 위치: row['위치'] || '', + 관리자: '', IP주소: row['IP주소'] || '', MACaddress: row['MAC주소'] || '', HW사양: '', OS: '', + storage유형: row['유형'] || '', 모델명: row['모델명'] || '', 용량: row['용량'] || '', + 담당자_정: row['담당자(정)'] || '', 담당자_부: row['담당자(부)'] || '', + 구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '', + 납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '', + }); + } else { + hwAssets.push({ + id: Math.random().toString(36).substring(2, 9), + type: sheetName, + 법인: row['법인'] || '', 자산코드: row['자산코드'] || '', 명칭: row['명칭'] || '', 위치: row['위치'] || '', + 관리자: row['관리자'] || '', IP주소: row['IP주소'] || '', MACaddress: row['MACaddress'] || '', + HW사양: row['HW사양'] || '', OS: row['OS'] || '', + 구매일: row['구매일'] || '', 금액: row['금액'] ? String(row['금액']) : '', + 납품업체: row['납품업체'] || '', 품의서명: row['품의서명'] || '', + }); + } + }); + } + + if (SW_TABS.includes(sheetName)) { + json.forEach(row => { + swAssets.push({ + id: row['ID'] ? String(row['ID']) : Math.random().toString(36).substring(2, 9), + type: sheetName, 분야: row['분야'] || '', 법인: row['법인'] || '', 부서: row['부서'] || '', 제품명: row['제품명'] || '', + 구매일: row['구매일'] || '', 구독일: row['구독일'] || '', 유지보수여부: row['유지보수여부'] === 'Y' || row['유지보수여부'] === true, + 금액: row['금액'] ? String(row['금액']) : '', 수량: parseInt(row['수량'] || '1', 10), + 계정명: row['계정명'] || '', 납품업체: row['납품업체'] || '', 비고: row['비고'] || '', + }); + }); + } + + if (sheetName === 'SW_사용자') { + json.forEach(row => { + swUsers.push({ + id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9), + swId: row['swId'] ? String(row['swId']) : '', 법인: row['법인'] || '', 부서: row['부서'] || '', + 팀: row['팀'] || '', 직위: row['직위'] || '', 이름: row['이름'] || '', + 사용기간: row['사용기간'] || '', 신청서명: row['신청서명'] || '', + }); + }); + } + + if (sheetName === 'History') { + json.forEach(row => { + logs.push({ + id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9), + assetId: row['assetId'] ? String(row['assetId']) : '', + date: row['date'] || '', details: row['details'] || '', user: row['user'] || '', + }); + }); + } + }); + resolve({ hw: hwAssets, sw: swAssets, swUsers, logs }); + } catch (err) { + reject(err); + } + }; + reader.onerror = (err) => reject(err); + reader.readAsBinaryString(file); + }); +} diff --git a/backup_refactor/src/core/realServerData.ts b/backup_refactor/src/core/realServerData.ts new file mode 100644 index 0000000..ead448d --- /dev/null +++ b/backup_refactor/src/core/realServerData.ts @@ -0,0 +1,1603 @@ +export const realServerData = [ + { + "법인": "한맥", + "자산코드": "hm-idc-001", + "storage유형": "서버", + "용도": "한맥 인트라넷", + "상세": "", + "위치": "서관 204번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "211.206.127.70", + "IP2": "192.168.10.5", + "원격접속": "원격데스크탑\nRemote Util", + "서버ID": "administrator\n211.206.127.70", + "서버PW": "samanerp1!\n1234아이티!", + "모델명": "HPE ProLiant DL360 Gen10", + "OS": "Windows Server 2016", + "CPU": "intel xeon silver4110 CPU @2.10GHz 2.10GHZ", + "RAM": "32GB", + "SSD1": "280GB", + "SSD2": "2.7TB" + }, + { + "법인": "한맥", + "자산코드": "hm-idc-002", + "storage유형": "서버", + "용도": "한맥 인트라넷 예비", + "상세": "단가, 입사자지원 서버 (4/1 장헌산업 이름으로 스마트 건설 용도 구매)", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "211.206.127.78", + "IP2": "192.168.10.13", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "Hanmac2141!", + "모델명": "HPE ProLiant DL360 Gen10", + "OS": "Windows Server 2019", + "CPU": "intel xeon silver4214R CPU @2.40GHz 2.39GHZ", + "RAM": "32GB", + "SSD1": "280GB", + "SSD2": "2.7TB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-001", + "storage유형": "서버", + "용도": "삼안 인트라넷", + "상세": "", + "위치": "서관 204번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.237", + "IP2": "erp.samaneng.com", + "원격접속": "원격데스크탑\nRemote Util", + "서버ID": "administrator\n118.220.172.237", + "서버PW": "samanerp1!\n1234아이티!", + "모델명": "HPE ProLiant DL360 Gen10", + "OS": "Windows Server 2016", + "CPU": "intel xeon silver4214R CPU @2.40GHz 2.39GHZ", + "RAM": "32GB", + "SSD1": "280GB", + "SSD2": "3.27TB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-002", + "storage유형": "서버", + "용도": "삼안 인트라넷 예비", + "상세": "", + "위치": "서관 204번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.249", + "IP2": "", + "원격접속": "원격데스크탑\nRemote Util", + "서버ID": "administrator\n678-605-383-130", + "서버PW": "samanerp1!\n1234아이티!", + "모델명": "HPE ProLiant DL360 GEN9", + "OS": "Windows Server 2008 R2", + "CPU": "Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz 2.40GHz", + "RAM": "32GB", + "SSD1": "279GB", + "SSD2": "2.72TB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-003", + "storage유형": "서버", + "용도": "SATIS 01", + "상세": "구 SATIS 서버, 세금계산서 발행(회계)", + "위치": "서관 204번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.228", + "IP2": "", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "satissg11707808", + "모델명": "HPE ProLiant DL380p GEN8", + "OS": "Windows Server 2008 R2", + "CPU": "Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz 3.30GHz", + "RAM": "20GB", + "SSD1": "100GB", + "SSD2": "458GB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-004", + "storage유형": "서버", + "용도": "SATIS 02", + "상세": "SATIS 리뉴얼 버전 (ERP 서버)", + "위치": "서관 204번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.229", + "IP2": "", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "satissg11707808", + "모델명": "HPE ProLiant DL380p GEN8", + "OS": "Windows Server 2008 R2", + "CPU": "Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz 3.30GHz", + "RAM": "20GB", + "SSD1": "100GB", + "SSD2": "458GB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-005", + "storage유형": "서버", + "용도": "웹 서버", + "상세": "남양주 테스트 서버 (도메인 관리 기능 제거 2026.03.11)", + "위치": "서관 204번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "samanweb.cafe24.com", + "IP2": "118.220.172.195", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "saman+2013+web", + "모델명": "HPE ProLiant DL380p GEN8", + "OS": "Windwos Server 2012", + "CPU": "Intel(R) Xeon(R) CPU E5-2609 0 @ 2.40GHz 2.40GHz", + "RAM": "16GB", + "SSD1": "100GB", + "SSD2": "230GB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-006", + "storage유형": "서버", + "용도": "PQ DB 서버", + "상세": "", + "위치": "서관 204번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.231", + "IP2": "", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "7013ddj10235!", + "모델명": "HPE ProLiant DL360 Gen10", + "OS": "Windows Server 2019", + "CPU": "intel xeon silver4210R CPU @2.40GHz 2.39GHZ", + "RAM": "32GB", + "SSD1": "278GB", + "SSD2": "2.18TB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-007", + "storage유형": "서버", + "용도": "Oracle DB 서버", + "상세": "", + "위치": "서관 202번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.225", + "IP2": "", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "7013ddj10235!", + "모델명": "HPE ProLiant DL380 GEN9", + "OS": "Windows Server 2012", + "CPU": "Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz 2.20GHz", + "RAM": "64GB", + "SSD1": "558GB", + "SSD2": "1.09TB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-008", + "storage유형": "서버", + "용도": "안전관리", + "상세": "삼안 개발서버2 - AI, SSL, 장헌TBM, 노드", + "위치": "서관 202번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "1.234.37.171", + "IP2": "", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "samanerp1!", + "모델명": "HPE ProLiant DL380 GEN10", + "OS": "Windwos Server 2022", + "CPU": "Intel Xeon(R) Silver 4210R CPU @ 2.40GHz", + "RAM": "128GB", + "SSD1": "278GB", + "SSD2": "3.27TB" + }, + { + "법인": "삼안", + "자산코드": "sa-idc-009", + "storage유형": "서버", + "용도": "가족사 공통메뉴", + "상세": "삼안 개발서버1 - QNA, 급여명세서", + "위치": "서관 202번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.233", + "IP2": "", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "samanerp1!", + "모델명": "HPE ProLiant DL380 GEN10", + "OS": "Windwos Server 2022", + "CPU": "Intel Xeon(R) Silver 4210R CPU @ 2.40GHz", + "RAM": "128GB", + "SSD1": "278GB", + "SSD2": "3.27TB" + }, + { + "법인": "한라", + "자산코드": "hl-idc-001", + "storage유형": "서버", + "용도": "한라 인트라넷", + "상세": "인트라넷,안전, 운영, MISO 서버로 운영 중(win 2008)", + "위치": "동관 54번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "1.234.37.143", + "IP2": "", + "원격접속": "Remote Util", + "서버ID": "1.234.37.143", + "서버PW": "1234dkdlxl!", + "모델명": "HPE ProLiant DL360 GEN9", + "OS": "Windows Server 2008 R2", + "CPU": "Intel(R) Xeon(R) CPU E5-2603 v4 @ 1.70GHz 1.70GHz", + "RAM": "8GB", + "SSD1": "299GB", + "SSD2": "631GB" + }, + { + "법인": "한라", + "자산코드": "hl-idc-002", + "storage유형": "서버", + "용도": "안전전산화 서버 (디자인팀 웹)", + "상세": "인트라넷 서버 다운 시 백업용 대기, (임시) 디자인팀 웹 퍼블리싱 서버", + "위치": "동관 54번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "1.234.37.144", + "IP2": "192.168.20.49", + "원격접속": "Remote Util", + "서버ID": "1.234.37.144", + "서버PW": "1234dkdlxl!", + "모델명": "HPE ProLiant DL360 GEN9", + "OS": "Windows Server 2012", + "CPU": "Intel(R) Xeon(R) CPU E5-2603 v4 @ 1.70GHz 1.70GHz", + "RAM": "8GB", + "SSD1": "299GB", + "SSD2": "631GB" + }, + { + "법인": "한라", + "자산코드": "hl-idc-003", + "storage유형": "서버", + "용도": "개발서버2", + "상세": "PTC 연구비로 구매한 예비서버2\n이전 : 하수도자산 소스+프로그램 현재 : 큰길 서비스용 xampp+ PostgreSQL, BEPs", + "위치": "동관 53번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "192.168.20.171", + "IP2": "1.234.37.171", + "원격접속": "Remote Util\n원격데스크탑", + "서버ID": "1.234.37.171\nadministrator", + "서버PW": "1234dkdlxl!\nHanmac2141!%", + "모델명": "HPE ProLiant DL380 Gen10", + "OS": "Windows Server 2019 Standard", + "CPU": "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz", + "RAM": "32GB", + "SSD1": "280GB", + "SSD2": "1TB" + }, + { + "법인": "장헌", + "자산코드": "jh-idc-001", + "storage유형": "서버", + "용도": "장헌인트라넷", + "상세": "", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "211.206.127.71", + "IP2": "192.168.10.6", + "원격접속": "Remote Util", + "서버ID": "211.206.127.71", + "서버PW": "1234dkdlxl!", + "모델명": "HPE ProLiant DL380 GEN10", + "OS": "Windows Server 2019", + "CPU": "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz 2.39GHz", + "RAM": "32GB", + "SSD1": "280GB", + "SSD2": "1TB" + }, + { + "법인": "장헌", + "자산코드": "jh-idc-002", + "storage유형": "서버", + "용도": "장헌 인트라넷 예비", + "상세": "", + "위치": "동관 53번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "1.234.37.170", + "IP2": "192.168.20.170", + "원격접속": "Remote Util\n원격데스크탑", + "서버ID": "1.234.37.170\nAdministrator", + "서버PW": "1234dkdlxl!\nHanmac2141!", + "모델명": "HPE ProLiant DL360 Gen10", + "OS": "Windows Server 2019", + "CPU": "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz 2.39GHz", + "RAM": "32GB", + "SSD1": "280GB", + "SSD2": "1TB" + }, + { + "법인": "장헌", + "자산코드": "jh-idc-003", + "storage유형": "서버", + "용도": "인트라넷(구)", + "상세": "현재는 GIT 백업 으로 사용", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "211.206.127.110", + "IP2": "192.168.10.40", + "원격접속": "Remote Util\n원격데스크탑", + "서버ID": "211.206.127.110\nUser", + "서버PW": "1234dkdlxl!\nHanmac2141!", + "모델명": "", + "OS": "Windows Server 2019", + "CPU": "Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "(주)장헌", + "자산코드": "jh-idc-004", + "storage유형": "서버", + "용도": "(주) 장헌 인트라넷", + "상세": "2025.12.23 (주) 장헌 센터 MDF에서 IDC로 이전 설치", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "211.206.127.76", + "IP2": "", + "원격접속": "원격데스크탑", + "서버ID": "User", + "서버PW": "Hanmac2141!%", + "모델명": "", + "OS": "Windows 10", + "CPU": "12th Gen Intel(R) Core(TM) i7-12700F", + "RAM": "32GB", + "SSD1": "465GB", + "SSD2": "1.81TB" + }, + { + "법인": "PTC", + "자산코드": "ptc-idc-001", + "storage유형": "서버", + "용도": "PTC인트라넷", + "상세": "구 파일 서버(부서자료 백업용), 2024.05.22 인트라넷서버로 교체", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "211.206.127.72", + "IP2": "192.168.10.7", + "원격접속": "Remote Util", + "서버ID": "211.206.127.72", + "서버PW": "1234dkdlxl!", + "모델명": "SYSTEM X3650 M2", + "OS": "Windows Server 2008 R2", + "CPU": "Intel(R) Xeon(R) CPU E5520 @ 2.27GHz 2.26GHz", + "RAM": "16GB", + "SSD1": "556GB", + "SSD2": "" + }, + { + "법인": "PTC", + "자산코드": "ptc-idc-002", + "storage유형": "서버", + "용도": "예비서버", + "상세": "PTC 인트라넷 예비서버", + "위치": "서관 204번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "192.168.10.8", + "IP2": "", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "1234dkdlxl!", + "모델명": "HPE ProLiant DL360 GEN10", + "OS": "Windows Server 2019", + "CPU": "Intel Xeon(R) Silver 4210R CPU @ 2.40GHz", + "RAM": "32GB", + "SSD1": "278GB", + "SSD2": "1.09TB" + }, + { + "법인": "PTC", + "자산코드": "ptc-idc-003", + "storage유형": "서버", + "용도": "DB 백업 서버", + "상세": "구 파일 인트라넷, 2024.05.22에 DB 백업 테스트 서버로 변경 (데스크탑)", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "211.206.127.74", + "IP2": "192.168.10.9", + "원격접속": "Remote Util", + "서버ID": "211.206.127.74", + "서버PW": "1234dkdlxl!", + "모델명": "", + "OS": "Window 7", + "CPU": "Intel(R) Core(TM)2 CPU 6400 @ 2.13GHz 2.13GHz", + "RAM": "4GB", + "SSD1": "593GB", + "SSD2": "1.23TB" + }, + { + "법인": "바론", + "자산코드": "br-idc-001", + "storage유형": "서버", + "용도": "인트라넷", + "상세": "", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "211.206.127.75", + "IP2": "192.168.10.10", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "Hanmac2141!%", + "모델명": "HPE ProLiant DL360 GEN10", + "OS": "Windows Server 2022", + "CPU": "Intel Xeon(R) Silver 4210R CPU @ 2.40GHz", + "RAM": "32GB", + "SSD1": "280GB", + "SSD2": "2.18TB" + }, + { + "법인": "현타", + "자산코드": "ht-idc-001", + "storage유형": "서버", + "용도": "인트라넷", + "상세": "", + "위치": "동관 53번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "1.234.37.172", + "IP2": "192.168.20.172", + "원격접속": "원격데스크탑", + "서버ID": "administrator", + "서버PW": "Hanmac2141!", + "모델명": "HPE ProLiant DL380 GEN10", + "OS": "Windows Server 2019", + "CPU": "Intel Xeon Silver 4210R CPU @ 2.40GHz 2.39GHz", + "RAM": "32GB", + "SSD1": "280GB", + "SSD2": "1TB" + }, + { + "법인": "삼안", + "자산코드": "sa-das-001", + "storage유형": "서버", + "용도": "", + "상세": "Satis01, Satis02 광케이블 연결 (물리연결)", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "IP2": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "삼안", + "자산코드": "sa-nas-001", + "storage유형": "서버", + "용도": "인트라넷 백업 스토리지", + "상세": "", + "위치": "서관 203번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.246", + "IP2": "118.220.172.246", + "원격접속": "원격", + "서버ID": "administrator", + "서버PW": "sg11707808", + "모델명": "", + "OS": "Promiss R Series", + "CPU": "36TB", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "삼안", + "자산코드": "sa-nas-002", + "storage유형": "서버", + "용도": "성과품 스토리지", + "상세": "매니지먼트 접속 확인 불가 (콘솔 연결 후 페이지 오픈 필요)", + "위치": "서관 205번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.248", + "IP2": "118.220.172.247", + "원격접속": "원격", + "서버ID": "administrator\n-", + "서버PW": "sg11707808\n-", + "모델명": "", + "OS": "ENC_3U_16BAY_D // SEAGATE ST2000NM0045", + "CPU": "23TB", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "삼안", + "자산코드": "sa-nas-003", + "storage유형": "서버", + "용도": "성과품 백업 스토리지", + "상세": "", + "위치": "서관 202번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "118.220.172.241", + "IP2": "118.220.172.240", + "원격접속": "원격", + "서버ID": "administrator\nadmin0", + "서버PW": "saman1!\nRoot1234", + "모델명": "", + "OS": "Promiss R Series", + "CPU": "48TB", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한라", + "자산코드": "hl-das-001", + "storage유형": "서버", + "용도": "", + "상세": "파일서버 정보 없음(접속 불가)", + "위치": "동관 54번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "IP2": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한라", + "자산코드": "hl-das-002", + "storage유형": "서버", + "용도": "", + "상세": "파일서버 정보 없음(접속 불가)", + "위치": "동관 54번", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "IP2": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "NAS", + "용도": "GSIM NAS", + "상세": "팀 내부 자료 저장 , 정사영상 및 지도 데이터 저장 , Gitea 및 Git 내장 NAS", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "Synology DS923+", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "NAS", + "용도": "그래픽스개발팀 데이터 백업 NAS", + "상세": "그래픽스 개발팀 데이터 백업용 NAS", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "Synology DS923+", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "공통 GIT 서버", + "상세": "개발 소스코드 서버 (구조물 S/W ,그래픽스개발_HMEG.천지인)", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "Dell EMC PowerEdge T380", + "OS": "CentOS Linux 7 (Core)", + "CPU": "Intel(R) Xeon(R) E-2324G CPU @ 3.10GHz", + "RAM": "16GB", + "SSD1": "1TB", + "SSD2": "1TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "BUILD 서버", + "상세": "PM 컨버터(PDF) 서버, PDF 및 비디오 썸네일 생성", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "Windows 10 Pro", + "CPU": "12th Gen Intel(R) Core(TM) i9-12900K", + "RAM": "128GB", + "SSD1": "4TB", + "SSD2": "10TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "HmEG 테스트 서버", + "상세": "HmEG 테스트 서버", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "Windows 10 Pro", + "CPU": "Intel(R) Core(TM) i5-10400F @ 2.906 GHz", + "RAM": "16GB", + "SSD1": "250GB", + "SSD2": "1TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "산하 ERP 개발서버", + "상세": "산하 ERP 개발용 서버(산하 ERP는 클라우드)", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "공간정보 신청", + "상세": "수치지형도 , 지적도 등 공간정보 자료 제공 서버", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "Windows 10", + "CPU": "Intel(R) Core i5-10400 CPU @ 2.90GHz 2.90GHz", + "RAM": "16GB", + "SSD1": "232GB", + "SSD2": "931GB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "AI 관련", + "상세": "", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "한종 테스트", + "상세": "", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "GSIM 언리얼 서버", + "상세": "오브젝트 스토리지(클라우드)를 NAS에 백업(매주 목), ERP유저정보 업데이트(매일), 언리얼 스트리밍 서버", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "Windows 10 Pro", + "CPU": "Intel(R) Xeon(R) Gold 6136", + "RAM": "128GB", + "SSD1": "1TB", + "SSD2": "8TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "AutoCAD 테스트 서버", + "상세": "오토캐드 테스트 서버", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "Windows 10 Pro", + "CPU": "AMD Ryzen9 3900X 12-Core Processor", + "RAM": "32GB", + "SSD1": "500GB", + "SSD2": "2TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "GSIM 테스트 서버", + "상세": "개발 테스트용 일반 PC", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "Windows 10 Pro", + "CPU": "Intel(R) Core(TM) i7-9700KF", + "RAM": "32GB", + "SSD1": "512GB", + "SSD2": "512GB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "공간데이터 서버", + "상세": "인트라넷 공간정보신청 서비스, 과거 공간데이터(~2022년) 보관 - 추후 공간정보 백업서버", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "CentOS 7.6.1810 (Core)", + "CPU": "Intel Xeon Silver 4108 * 2", + "RAM": "128 GB", + "SSD1": "512 GB", + "SSD2": "8 TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "PC", + "용도": "가평 VM 원격 서버", + "상세": "", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "서버", + "용도": "GSIM 협업", + "상세": "삼안 예비서버2 + 한종개발 +한종기존소스 vmware", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "HPE ProLiant DL380 Gen10", + "OS": "Server 2019", + "CPU": "Intel Xeon(R) Silver 4208 CPU @ 2.10GHz", + "RAM": "128GB", + "SSD1": "300GB", + "SSD2": "1.88TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "스토리지", + "용도": "GSIM 협업 스토리지", + "상세": "", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "Promiss R Series R3600", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "서버", + "용도": "GSIM META 서버", + "상세": "PM 백업 서버, 오브젝트 스토리지(온프레미스, 클라우드)API 연결", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "HPE ProLiant DL360 Gen10", + "OS": "Windows Server 2019 Standard", + "CPU": "Intel(R) Xeon(R) Silver 4208 CPU", + "RAM": "96GB", + "SSD1": "300GB", + "SSD2": "4TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "서버", + "용도": "GSIM 서버", + "상세": "Basemap 데이터 저장", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "HPE ProLiant DL360 Gen10", + "OS": "Windows Server 2019 Standard", + "CPU": "Intel(R) Xeon(R) Silver 4214R", + "RAM": "32GB", + "SSD1": "300GB", + "SSD2": "4TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "스토리지", + "용도": "GSIM 스토리지", + "상세": "ProjectMaster 오브젝트 스토리지", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "Promiss R Series R3600", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "서버", + "용도": "함양-합천 서버", + "상세": "함양합천서버, GSIM 웹서비스, PM 웹서비스", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "HPE ProLiant DL380 Gen10", + "OS": "Windows Server 2019 Standard", + "CPU": "", + "RAM": "", + "SSD1": "600GB", + "SSD2": "10TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "서버", + "용도": "HM MapService 2.0 서버", + "상세": "공간데이터 다운로드 서비스 등", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "172.16.42.127", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "HPE ProLiant DL380 Gen10", + "OS": "Windows Server 2019 Standard", + "CPU": "Intel Xeon Silver 4208", + "RAM": "128 GB", + "SSD1": "1.2 TB", + "SSD2": "40 TB" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "스토리지", + "용도": "HM MapService 2.0 스토리지", + "상세": "공간데이터 저장용", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "Promiss R Series R3600", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "서버", + "용도": "Gitlab Runner", + "상세": "GitLab 운영 서버", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "HPE ProLiant DL360 Gen10", + "OS": "Window Server 2019 Standard", + "CPU": "Intel(R) Xeon(R) Silver 4208 CPU @ 2.10GHz", + "RAM": "64GB", + "SSD1": "1.2 TB", + "SSD2": "" + }, + { + "법인": "기술개발센터", + "자산코드": "", + "storage유형": "서버", + "용도": "전산모사", + "상세": "EGBIM, ParaView, CFDCore", + "위치": "마천사무실", + "담당자_정": "", + "담당자_부": "", + "IP주소": "", + "원격접속": "", + "서버ID": "", + "서버PW": "", + "모델명": "", + "OS": "Windows 11 Pro", + "CPU": "13th Gen Intel(R) Core(TM) i9-13900KS (3.20 GHz)", + "RAM": "128GB", + "SSD1": "2TB", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "1", + "storage유형": "NAS", + "용도": "NAS 2", + "상세": "한라 기업부설연구소 공용 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.9.23", + "모델명": "DS414j", + "담당자_정": "이준하 차장", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "2", + "storage유형": "NAS", + "용도": "NAS 1", + "상세": "한라 공용 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.9.32", + "모델명": "DS224+", + "담당자_정": "이준하 차장", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "3", + "storage유형": "NAS", + "용도": "NAS 4", + "상세": "한라 공용 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.9.25", + "모델명": "CS407", + "담당자_정": "이준하 차장", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "4", + "storage유형": "NAS", + "용도": "NAS 5", + "상세": "한라 환경플랜트사업부 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.9.30", + "모델명": "DS923+", + "담당자_정": "이준하 차장", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "5", + "storage유형": "NAS", + "용도": "NAS 6", + "상세": "한라 공용 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.9.27", + "모델명": "DS923+", + "담당자_정": "이준하 차장", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "6", + "storage유형": "NAS", + "용도": "NAS7", + "상세": "한라 원주바이오 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.9.20", + "모델명": "DS414j", + "담당자_정": "이준하 차장", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "7", + "storage유형": "NAS", + "용도": "총괄기획실 NAS", + "상세": "총괄기획실 공용 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.10.5", + "모델명": "DS413j", + "담당자_정": "-", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "8", + "storage유형": "NAS", + "용도": "한맥 NAS 1", + "상세": "한맥 공용 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.10.3", + "모델명": "DS416j", + "담당자_정": "순서 파악 필요", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "9", + "storage유형": "NAS", + "용도": "한맥 NAS 2", + "상세": "한맥 공용 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.10.6", + "모델명": "DS416j", + "담당자_정": "순서 파악 필요", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "10", + "storage유형": "NAS", + "용도": "한맥 NAS 3", + "상세": "한맥 공용 NAS", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.10.7", + "모델명": "DS416j", + "담당자_정": "순서 파악 필요", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "11", + "storage유형": "NAS", + "용도": "NAS 13", + "상세": "환경플랜트사업", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "172.16.100.3", + "모델명": "DS218play", + "담당자_정": "이준하 차장", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "12", + "storage유형": "PC", + "용도": "회계", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "", + "모델명": "조립PC", + "담당자_정": "찾아야함", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "13", + "storage유형": "PC", + "용도": "한맥CAD", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "", + "모델명": "조립PC", + "담당자_정": "찾아야함", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "14", + "storage유형": "서버(타워)", + "용도": "Ai-Cell-Util", + "상세": "깃티, 매터모스트 등 70여종", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "", + "모델명": "HP Z6", + "담당자_정": "", + "OS": "Ubuntu 24.04", + "CPU": "Intel(R) Xeon(R) Gold 6248R", + "RAM": "64GB", + "SSD1": "2 TB", + "SSD2": "8 TB" + }, + { + "법인": "한맥빌딩", + "자산코드": "15", + "storage유형": "PC", + "용도": "한라CAD", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "", + "모델명": "조립PC", + "담당자_정": "찾아야함", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "16", + "storage유형": "NAS", + "용도": "디자인팀1 NAS", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.9.99", + "모델명": "DS1522+", + "담당자_정": "권순호 연구원", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "17", + "storage유형": "NAS", + "용도": "디자인팀2 NAS", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "192.168.9.100", + "모델명": "DS1522+", + "담당자_정": "권순호 연구원", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "18", + "storage유형": "서버(미니워크스테이션)", + "용도": "인사정보 서버", + "상세": "인사정보 PM", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "172.16.10.187", + "모델명": "HP Z2 Mini G5 Workstation", + "담당자_정": "", + "OS": "Windows 11 Pro", + "CPU": "intel xeon w-1250p cpu", + "RAM": "32GB", + "SSD1": "2 TB", + "SSD2": "2 TB" + }, + { + "법인": "한맥빌딩", + "자산코드": "19", + "storage유형": "서버(타워)", + "용도": "BEPs 서버", + "상세": "BEPs 개발서버, Outline 협업서비스", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "", + "모델명": "Dell Precision 3680T", + "담당자_정": "", + "OS": "Windows 11 Pro", + "CPU": "Intel Core i9 14900K (24 Core, 32 Thread)", + "RAM": "64GB", + "SSD1": "2 TB", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "20", + "storage유형": "서버(타워)", + "용도": "Ai-Cell-A100-1", + "상세": "OCR, Local LLM 등 30여종", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "", + "모델명": "조립", + "담당자_정": "", + "OS": "Ubuntu 24.04", + "CPU": "AMD Ryzen Threadripper PRO 7975WX", + "RAM": "256GB", + "SSD1": "2 TB", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "21", + "storage유형": "서버(타워)", + "용도": "빌드서버", + "상세": "인스톨 쉴드, 지라", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "", + "모델명": "Dell EMC PowerEdge T350", + "담당자_정": "", + "OS": "Windows Server 2022 Standard", + "CPU": "Intel(R) Xeon(R) E-2378G CPU @ 2.80GHz 2.81 GHz", + "RAM": "32GB", + "SSD1": "1TB", + "SSD2": "4TB" + }, + { + "법인": "한맥빌딩", + "자산코드": "22", + "storage유형": "PC\n서버(랙)", + "용도": "저장소 및 전산모사\n구)스마트건설 서버", + "상세": "ParaView, CFDCore\n디지털화설문, 검색WIKI 웹서비스", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "172.16.10.213", + "모델명": "조립PC\nProLiant DL360 Gen10", + "담당자_정": "", + "OS": "Windows 10 Pro", + "CPU": "Intel Core i7-7700 CPU 3.60GHz", + "RAM": "32GB", + "SSD1": "500GB", + "SSD2": "2TB" + }, + { + "법인": "한맥빌딩", + "자산코드": "23", + "storage유형": "서버(랙)", + "용도": "IDC 산하ERP서버", + "상세": "XR 가상화 메인 서버 → IDC 산하ERP서버", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "172.16.10.226", + "모델명": "ProLiant DL360 Gen10", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "24", + "storage유형": "스토리지(랙)", + "용도": "WAS Storage", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "Promise Vess R3600", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "25", + "storage유형": "서버(랙)", + "용도": "한맥 백업 서버", + "상세": "가족사 인트라넷 소스 백업 서버", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "Promise Vess R3600", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "26", + "storage유형": "서버(랙)", + "용도": "한라 백업 서버", + "상세": "한라 웹 소스 및 Miso DB 백업 서버", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "SuperMicro IR5019P-Series", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "27", + "storage유형": "NAS", + "용도": "기술개발센터 NAS", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "RS822+", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "28", + "storage유형": "NAS", + "용도": "-", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "RS815+", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "29", + "storage유형": "스토리지(랙)", + "용도": "Backup Storage", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "Promise Vess", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "30", + "storage유형": "스토리지(랙)", + "용도": "-", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "Promise Vess R3600", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "31", + "storage유형": "서버(랙)", + "용도": "XR WAS Server", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "ProLiant DL360 Gen10", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + }, + { + "법인": "한맥빌딩", + "자산코드": "32", + "storage유형": "서버(랙)", + "용도": "WAS Storage", + "상세": "", + "위치": "한맥빌딩(MDF 실)", + "IP주소": "-", + "모델명": "Promise Vess R3600", + "담당자_정": "", + "OS": "", + "CPU": "", + "RAM": "", + "SSD1": "", + "SSD2": "" + } +]; diff --git a/backup_refactor/src/core/state.ts b/backup_refactor/src/core/state.ts new file mode 100644 index 0000000..96bc1ac --- /dev/null +++ b/backup_refactor/src/core/state.ts @@ -0,0 +1,87 @@ +import { MasterAssetData, HardwareAsset } from './excelHandler'; +import { generateDummyData } from './dummyDataGenerator'; +import { realServerData } from './realServerData'; + +// --- State Definitions --- +export interface AppState { + masterData: MasterAssetData; + activeCategory: 'hw' | 'sw' | 'ops'; + activeSubTab: string; + activeCharts: any[]; +} + +const dummy = generateDummyData(); +// 서버 데이터만 실제 데이터로 교체 +const mergedHw: HardwareAsset[] = [ + ...dummy.hw.filter(a => a.type !== '서버'), + ...realServerData.map(s => ({ + id: s.id || Math.random().toString(36).substring(2, 9), + type: '서버', + 법인: s.법인, + 자산코드: s.자산코드, + 명칭: s.용도 || '', + 위치: s.위치, + 관리자: s.담당자_정 || '홍길동', + 담당자_정: s.담당자_정 || '홍길동', + 담당자_부: s.담당자_부 || '김철수', + IP주소: s.IP주소, + IP2: s.IP2 || '', + MACaddress: s.MACaddress || '', + HW사양: s.HW사양 || '', + OS: s.OS, + CPU: s.CPU, + RAM: s.RAM, + SSD1: s.SSD1, + SSD2: s.SSD2, + HDD1: s.HDD1, + storage유형: s.storage유형, + 모델명: s.모델명, + 구매일: s.구매일 || '', + 금액: s.금액 || '', + 납품업체: s.납품업체 || '', + 품의서명: s.품의서명 || '', + 용도: s.용도, + 상세: s.상세, + 원격접속: s.원격접속 || '', + 서버ID: s.서버ID || '', + 서버PW: s.서버PW || '', + 모니터링: s.모니터링 || '', + 비고: s.비고 || '' + })) +]; + +// --- Initial State --- +export const state: AppState = { + masterData: { + ...dummy, + hw: mergedHw, // 기본적으로 하드코딩된 데이터를 가지고 시작 + logs: [] + }, + activeCategory: 'hw', + activeSubTab: '대시보드', + activeCharts: [] +}; + +/** + * DB에서 데이터 로드 + */ +export async function loadMasterDataFromDB() { + try { + const response = await fetch('http://localhost:3000/api/hw'); + if (!response.ok) throw new Error('DB 로드 실패'); + const data = await response.json(); + if (data && data.length > 0) { + state.masterData.hw = data; + console.log('✅ DB 데이터 로드 완료'); + return true; + } + } catch (err) { + console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.'); + } + return false; +} + +// --- State Helpers --- +export function updateState(newState: Partial) { + Object.assign(state, newState); +} diff --git a/backup_refactor/src/main.ts b/backup_refactor/src/main.ts new file mode 100644 index 0000000..c575bff --- /dev/null +++ b/backup_refactor/src/main.ts @@ -0,0 +1,108 @@ +import { state, loadMasterDataFromDB } from './core/state'; +import { renderNavigation } from './components/Navigation'; +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'; +import { initSwModal } from './components/Modal/SWModal'; +import { initSwUserModal } from './components/Modal/SWUserModal'; +import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal'; +import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide'; + +// --- DB 저장을 위한 헬퍼 함수 --- +async function saveAllHwToDB(assets: HardwareAsset[]) { + try { + const response = await fetch('http://localhost:3000/api/hw/batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(assets) + }); + if (!response.ok) throw new Error('DB 저장 실패'); + console.log('✅ DB 저장 완료'); + } catch (err) { + console.error('❌ DB 저장 실패:', err); + } +} + +// --- App Initialization --- +function initApp() { + console.log('🚀 ITAM System Initializing...'); + const mainContent = document.getElementById('main-content')!; + if (!mainContent) return; + + // 1. 전역 모달 및 내비게이션 초기화 + const { closeAllModals } = initBaseModal(); + + try { + renderNavigation((tab) => { + if (tab === '대시보드') { + renderDashboard(mainContent); + } else { + renderTable(mainContent); + } + }); + + initPcModal(() => { + saveAllHwToDB(state.masterData.hw); + renderTable(mainContent); + }, closeAllModals); + + initHwModal(); + + initStorageModal(() => { + saveAllHwToDB(state.masterData.hw); + renderTable(mainContent); + }, closeAllModals); + + initSwModal(() => renderTable(mainContent), closeAllModals); + initSwUserModal(() => renderTable(mainContent), closeAllModals); + initDashboardDetailModal(); + } catch (e) { + console.error('❌ Initialization failed:', e); + } + + // 2. 초기 렌더링 + renderDashboard(mainContent); + + // 3. 비동기 데이터 로드 + loadMasterDataFromDB().then((success) => { + if (success) { + if (state.activeSubTab === '대시보드') renderDashboard(mainContent); + else renderTable(mainContent); + } + }); + + // 4. 이벤트 바인딩 + document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate()); + document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData)); + + const uploadInput = document.getElementById('excel-upload') as HTMLInputElement; + uploadInput?.addEventListener('change', async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + const data = await parseExcel(file); + state.masterData = data; + await saveAllHwToDB(data.hw); + renderTable(mainContent); + } + }); + + document.getElementById('btn-add-asset')?.addEventListener('click', () => { + if (state.activeSubTab === '서버' || state.activeSubTab === '전산비품' || state.activeSubTab === '스토리지') { + openHwModal({ + id: Math.random().toString(36).substring(2, 9), + type: state.activeSubTab, + 법인: '한맥', 자산코드: '', 명칭: '', 위치: '', 관리자: '', IP주소: '', MACaddress: '', HW사양: '', OS: '', 납품업체: '', 품의서명: '' + } as any); + } + }); + + createIcons({ + icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } + }); +} + +document.addEventListener('DOMContentLoaded', initApp); diff --git a/backup_refactor/src/server_data.json b/backup_refactor/src/server_data.json new file mode 100644 index 0000000..d193679 --- /dev/null +++ b/backup_refactor/src/server_data.json @@ -0,0 +1,13 @@ +[ + { + "법인": "(주)회사1", + "자산코드": "ASSET-100", + "명칭": "서버 모델A", + "위치": "본사 1층", + "관리자": "관리자A", + "IP주소": "192.168.0.1", + "MACaddress": "00:00:00:00:00:01", + "HW사양": "Core i7, 16GB RAM", + "OS": "Windows 10" + } +] \ No newline at end of file diff --git a/backup_refactor/src/styles/common.css b/backup_refactor/src/styles/common.css new file mode 100644 index 0000000..fa5cae3 --- /dev/null +++ b/backup_refactor/src/styles/common.css @@ -0,0 +1,262 @@ +:root { + --primary-color: #1E5149; + --primary-hover: #153c36; + --primary-light: #edf2f1; + --text-main: #111827; + --text-muted: #6B7280; + --border-color: #E5E7EB; + --bg-color: #F9FAFB; + --white: #FFFFFF; + --danger: #dc2626; + + --header-height: 52px; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Pretendard Variable', Pretendard, sans-serif; + color: var(--text-main); + background-color: var(--bg-color); + line-height: 1.5; + letter-spacing: -0.02em; + font-size: 14px; + overflow: hidden; +} + +.app-layout { + display: flex; + flex-direction: column; + height: 100vh; + width: 100%; +} + +/* --- Integrated Header Style --- */ +.main-header { + background-color: var(--white); + border-bottom: 1px solid var(--border-color); + z-index: 100; + height: var(--header-height); +} + +.header-container { + height: 100%; + display: flex; + align-items: center; + padding: 0 1.5rem; + gap: 1.5rem; +} + +.brand h1 { + font-size: 1.2rem; + font-weight: 800; + color: var(--text-main); + white-space: nowrap; + margin-right: 1rem; +} +.brand h1 span { color: var(--primary-color); } + +/* --- Integrated Nav --- */ +.integrated-nav { + flex: 1; + height: 100%; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.nav-group { + display: flex; + align-items: center; + height: 100%; +} + +.gnb-trigger { + font-size: 14px; + font-weight: 700; + color: var(--text-main); + padding: 0 1rem; + cursor: pointer; + height: 100%; + display: flex; + align-items: center; + white-space: nowrap; +} + +.lnb-shelf { + display: none; + align-items: center; + gap: 0.25rem; + padding: 0 0.75rem; + height: 60%; + border-left: 1px solid var(--border-color); + margin-left: 0.25rem; + animation: fadeIn 0.2s ease-out; +} + +.nav-group:hover .lnb-shelf, +.nav-group.is-showing-shelf .lnb-shelf { + display: flex; +} + +.lnb-item { + font-size: 13px; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + padding: 0.2rem 0.6rem; + border-radius: 4px; + white-space: nowrap; +} + +.lnb-item.active { + color: var(--primary-color); + background-color: var(--primary-light); + font-weight: 700; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateX(-5px); } + to { opacity: 1; transform: translateX(0); } +} + +/* --- Header Actions --- */ +.header-actions { display: flex; gap: 0.3rem; align-items: center; } + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + padding: 0 0.8rem; + font-size: 12px; + font-weight: 600; + border-radius: 4px; + cursor: pointer; + height: 28px; + line-height: 1; +} + +.btn i, .btn svg { width: 12px !important; height: 12px !important; } + +.btn-primary { background-color: var(--primary-color); color: var(--white); border: 1px solid var(--primary-color); } +.btn-outline { background-color: transparent; color: var(--text-muted); border: 1px solid var(--border-color); } + +/* --- Content Area & Standardized Layout --- */ +.content-area { + flex: 1; + padding: 2rem; /* benchmark: 좌, 우, 하단 2rem 공백 통일 */ + overflow-y: auto; + background-color: var(--bg-color); +} + +.view-container { + width: 100%; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* --- Search Filter Bar --- */ +.search-bar { + display: flex; + flex-wrap: wrap; + gap: 1.25rem; + background-color: var(--white); + padding: 1.5rem; + border: 1px solid var(--border-color); + border-radius: 8px; + align-items: flex-end; +} + +.search-item { display: flex; flex-direction: column; gap: 0.4rem; } +.search-item.flex-1 { flex: 1; } +.search-item label { font-size: 11px; font-weight: 800; color: var(--text-muted); } +.search-item input, +.search-item select { + height: 32px; + padding: 0 2.5rem 0 0.75rem; /* Increased right padding for arrow */ + border: 1px solid var(--border-color); + border-radius: 3px; + font-size: 13px; + outline: none; + appearance: none; /* Modern arrow styling */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-9'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; +} + +.search-item input { + padding-right: 0.75rem; +} + +.btn-reset { + height: 32px !important; + padding: 0 0.8rem !important; + font-size: 12px !important; + display: inline-flex !important; + align-items: center !important; + gap: 0.35rem !important; + border-radius: 4px !important; +} + +/* --- Table (Box-less Design) --- */ +.table-container { + background-color: var(--white); + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + border-left: none; + border-right: none; + overflow: auto; + max-height: calc(100vh - 240px); /* Adjusting for bottom spacing */ +} + +table { width: 100%; border-collapse: collapse; } +th, td { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + text-align: left; + white-space: nowrap; /* Force single line for all info */ +} +th { + background-color: #FAFAFA; + font-weight: 700; + color: var(--text-muted); + font-size: 12px; + position: sticky; + top: 0; + z-index: 10; + box-shadow: inset 0 -1px 0 var(--border-color); + text-transform: uppercase; +} +td { font-size: 14px; } +tbody tr:hover { background-color: #F9FAFB; } + +/* --- Dashboard Style --- */ +.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; } +.stat-card { background-color: var(--white); padding: 1.5rem; border: 1px solid var(--border-color); border-radius: 8px; } +.stat-card .value { font-size: 2.2rem; font-weight: 800; color: var(--primary-color); margin-top: 0.5rem; } +.dashboard-layout-2col { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; } +.dashboard-card { + background-color: var(--white); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.5rem; + display: flex; + flex-direction: column; + min-height: 360px; /* Increased height for better chart view */ +} +.dashboard-card canvas { + flex: 1; + width: 100% !important; + max-height: 280px; +} +.dashboard-section-title { padding: 0 0 1rem 0; font-size: 1.1rem; font-weight: 700; color: var(--text-main); } + +.hidden { display: none !important; } +.text-nowrap { white-space: nowrap; } +.btn-sm { padding: 0.25rem 0.5rem; font-size: 11px; height: 24px; } diff --git a/backup_refactor/src/styles/modal.css b/backup_refactor/src/styles/modal.css new file mode 100644 index 0000000..4f6b79c --- /dev/null +++ b/backup_refactor/src/styles/modal.css @@ -0,0 +1,267 @@ +/* Modal */ +.modal-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background-color: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease; +} + +.modal-overlay:not(.hidden) { opacity: 1; visibility: visible; } + +.modal-content { + background-color: var(--white); + width: 100%; + max-width: 600px; + max-height: 90vh; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); + transform: translateY(20px); + transition: transform 0.2s ease; + display: flex; + flex-direction: column; +} + +.modal-overlay:not(.hidden) .modal-content { transform: translateY(0); } + +.modal-header { + background-color: var(--primary-color); + color: var(--white); + padding: 1rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; +} + +.modal-header h2 { + font-size: 1.125rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.modal-header .btn-icon { + color: #FFFFFF !important; + cursor: pointer; + background: none !important; + border: none !important; +} + +.modal-header .btn-icon i, +.modal-header .btn-icon svg { + width: 20px !important; /* Original natural size */ + height: 20px !important; + stroke: #FFFFFF !important; +} + +.modal-header .btn-icon:hover { + background: none !important; +} + +.modal-body { + padding: 1.5rem; + overflow-y: auto; + flex: 1; +} + +.grid-form { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.25rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.form-group.full-width { + grid-column: span 2; +} + +/* Section Title for Grouping */ +.form-section-title { + grid-column: span 2; + font-size: 0.875rem; + font-weight: 700; + color: var(--primary-color); + padding: 1.5rem 0 0.5rem 0; /* 패딩 조정 */ + border-bottom: 1px solid var(--border-color); + margin-bottom: 0.5rem; + display: flex; + align-items: center; +} + +/* Modal Readonly/Edit Mode Interaction */ +.grid-form.is-view-mode input, +.grid-form.is-view-mode select, +.grid-form.is-view-mode textarea { + border-color: transparent !important; + background-color: transparent !important; + padding-left: 0; + padding-right: 0; + pointer-events: none; + color: var(--text-main); + font-weight: 500; +} + +.grid-form.is-edit-mode input, +.grid-form.is-edit-mode select, +.grid-form.is-edit-mode textarea { + color: #FF3D00; /* 수정 시 글자색 변경 */ + border: 1px solid var(--border-color); +} + +.grid-form.is-edit-mode input:focus, +.grid-form.is-edit-mode select:focus, +.grid-form.is-edit-mode textarea:focus { + border-color: #FF3D00; + box-shadow: 0 0 0 2px rgba(255, 61, 0, 0.1); +} + +.form-section-title:first-child { + padding-top: 0.5rem; +} + +.form-group label { + font-size: 0.8125rem; + font-weight: 600; + color: var(--text-muted); +} + +.form-group input, +.form-group select, +.form-group textarea { + padding: 0.625rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-family: inherit; + font-size: 0.875rem; + outline: none; + transition: all 0.2s; + background-color: var(--white); +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1); +} + +.modal-footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + background-color: #FAFAFA; + flex-shrink: 0; +} + +.footer-actions { + display: flex; + gap: 0.5rem; +} + +/* Wide Modal for History/Detail */ +.modal-content.wide { + max-width: 950px; +} + +.modal-body-split { + display: flex; + gap: 2rem; + min-height: 480px; +} + +.modal-form-area { + flex: 1.2; +} + +.modal-history-area { + flex: 0.8; + border-left: 1px solid var(--border-color); + padding-left: 1.5rem; + display: flex; + flex-direction: column; +} + +.history-header { + margin-bottom: 1rem; +} + +.history-header h3 { + font-size: 0.9375rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-main); +} + +.history-timeline { + flex: 1; + overflow-y: auto; + max-height: 500px; + padding-right: 0.5rem; +} + +.history-item { + position: relative; + padding-left: 1.25rem; + padding-bottom: 1.5rem; + border-left: 2px solid var(--border-color); +} + +.history-item::before { + content: ''; + position: absolute; + left: -7px; + top: 0; + width: 12px; + height: 12px; + border-radius: 50%; + background-color: var(--white); + border: 2px solid var(--primary-color); +} + +.history-item:last-child { + border-left: 2px solid transparent; +} + +.history-date { + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 500; + margin-bottom: 0.25rem; +} + +.history-user { + font-size: 0.75rem; + font-weight: 600; + color: var(--primary-color); + margin-bottom: 0.25rem; +} + +.history-details { + font-size: 0.8125rem; + color: var(--text-main); + line-height: 1.4; + white-space: pre-wrap; + word-break: break-all; +} + +.empty-history { + padding: 2rem 0; + text-align: center; + color: var(--text-muted); + font-size: 0.8125rem; +} diff --git a/backup_refactor/src/views/AssetTableView.ts b/backup_refactor/src/views/AssetTableView.ts new file mode 100644 index 0000000..af4460d --- /dev/null +++ b/backup_refactor/src/views/AssetTableView.ts @@ -0,0 +1,208 @@ +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'; +import { openStorageModal } from '../components/Modal/StorageModal'; +import { openSwModal } from '../components/Modal/SWModal'; +import { openSwUserModal } from '../components/Modal/SWUserModal'; + +/** + * 자산 목록 테이블 렌더링 메인 함수 + */ +export function renderTable(mainContent: HTMLElement) { + mainContent.innerHTML = ''; + const container = document.createElement('div'); + container.className = 'view-container'; + const table = document.createElement('table'); + + if (state.activeCategory === 'hw') { + renderHwTable(table, container, mainContent); + } else { + renderSwTable(table, container, mainContent); + } + + createIcons({ + icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 } + }); +} + +function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) { + const fullList = state.masterData.hw.filter(a => a.type === state.activeSubTab); + container.innerHTML = ''; + + // --- 1. Search Bar (Unified Style) --- + 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(); + + filterBar.innerHTML = ` +
+ + +
+
+ + +
+ ${state.activeSubTab === '서버' ? ` +
+ + +
` : ''} + + `; + container.appendChild(filterBar); + + // --- 2. Table Structure (Unified Style) --- + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; + + if (state.activeSubTab === '개인PC') { + table.innerHTML = `No법인자산코드사용자위치CPURAMStorage구매일금액품의서관리`; + } else if (state.activeSubTab === '서버') { + table.innerHTML = `No법인현 사용조직자산번호용도상세설치위치담당자IP주소모델명OSCPU/RAMStorage관리`; + } else if (state.activeSubTab === '스토리지') { + table.innerHTML = `No법인유형자산코드명칭위치모델명용량IP주소구매일관리`; + } else { + table.innerHTML = `No법인자산코드명칭위치관리자구매일금액관리`; + } + + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); + mainContent.appendChild(container); + + 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 orgUnit = (document.getElementById('filter-org-unit') as HTMLSelectElement)?.value || ''; + + const filtered = fullList.filter(asset => { + const matchKeyword = !keyword || String(asset.자산코드||'').toLowerCase().includes(keyword) || String(asset.현사용조직||'').toLowerCase().includes(keyword) || String(asset.모델명||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset.법인 === corp; + const matchOrg = !orgUnit || asset.현사용조직 === orgUnit; + return matchKeyword && matchCorp && matchOrg; + }); + + tbody.innerHTML = ''; + if (filtered.length === 0) { + const colSpan = table.querySelectorAll('th').length; + 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(); + + if (state.activeSubTab === '개인PC') { + const storage = [asset.SSD1, asset.SSD2, asset.HDD1].filter(v => v).join(' / '); + tr.innerHTML = `${idx+1}${asset.법인}${asset.자산코드}${asset.사용자||''}${asset.위치||''}${asset.CPU||''}${asset.RAM||''}${formatInline(storage)}${asset.구매일||''}${asset.금액||''}${asset.품의서명 ? '' : '-'}`; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset); }); + } else if (state.activeSubTab === '서버') { + const cpuRam = [asset.CPU, asset.RAM].filter(v => v).join(' / '); + const storage = [asset.SSD1, asset.SSD2].filter(v => v).join(' / '); + const ipInfo = [asset.IP주소, asset.IP2].filter(v => v).join(' / '); + tr.innerHTML = `${idx+1}${asset.법인}${asset.현사용조직||''}${asset.자산코드}${formatInline(asset.용도)}${formatInline(asset.상세)}${formatInline(asset.위치)}${asset.담당자_정||''}${formatInline(ipInfo)}${asset.모델명||''}${asset.OS||''}${formatInline(cpuRam)}${formatInline(storage)}`; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); }); + } else if (state.activeSubTab === '스토리지') { + tr.innerHTML = `${idx+1}${asset.법인}${asset.storage유형||''}${asset.자산코드}${asset.명칭}${asset.위치||''}${asset.모델명||''}${asset.용량||''}${asset.IP주소||''}${asset.구매일||''}`; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); }); + } else { + tr.innerHTML = `${idx+1}${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: { Paperclip, Edit2, RefreshCcw } }); + }; + + const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; + const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; + const orgSelect = document.getElementById('filter-org-unit') as HTMLSelectElement; + const resetBtn = document.getElementById('btn-reset-filters') as HTMLButtonElement; + + keywordInput.addEventListener('input', updateTable); + corpSelect.addEventListener('change', updateTable); + orgSelect?.addEventListener('change', updateTable); + resetBtn.addEventListener('click', () => { + keywordInput.value = ''; corpSelect.value = ''; if(orgSelect) orgSelect.value = ''; + updateTable(); + }); + + updateTable(); +} + +function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) { + const fullList = state.masterData.sw.filter(a => a.type === state.activeSubTab); + const isSub = state.activeSubTab === '구독SW'; + container.innerHTML = ''; + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + filterBar.innerHTML = `
`; + container.appendChild(filterBar); + + 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')!; + 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 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 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'}${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 } }); + }; + + 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 = ''; + updateTable(); + }); + updateTable(); +} diff --git a/backup_refactor/src/views/DashboardView.ts b/backup_refactor/src/views/DashboardView.ts new file mode 100644 index 0000000..364514f --- /dev/null +++ b/backup_refactor/src/views/DashboardView.ts @@ -0,0 +1,278 @@ +import { state } from '../core/state'; +import { HardwareAsset, SoftwareAsset } from '../core/excelHandler'; +import { openDashboardDetail, openSwDashboardDetail, openSwUsageDetail } from '../components/Modal/DashboardDetailModal'; + +declare var Chart: any; + +/** + * 대시보드 렌더링 메인 함수 + */ +export function renderDashboard(mainContent: HTMLElement) { + if (!mainContent) return; + mainContent.innerHTML = ''; + + // 기존 차트 리소스 해제 + if (state.activeCharts) { + state.activeCharts.forEach(c => { + if (c && typeof c.destroy === 'function') c.destroy(); + }); + } + state.activeCharts = []; + + if (state.activeCategory === 'hw') { + renderHwDashboard(mainContent); + } else if (state.activeCategory === 'sw') { + renderSwDashboard(mainContent); + } else { + mainContent.innerHTML = `
운영 서비스 대시보드는 준비 중입니다.
`; + } +} + +// --- 하드웨어 대시보드 --- +function renderHwDashboard(container: HTMLElement) { + const types = ['개인PC', '서버', '스토리지', '전산비품']; + const units = ['대', '대', '대', '개']; + const groups: any = {}; + + types.forEach(t => { groups[t] = { idle: [], active: [], aged: [], normal: [] }; }); + + state.masterData.hw.forEach(a => { + if (!groups[a.type]) return; + if (isHwIdle(a)) groups[a.type].idle.push(a); + else groups[a.type].active.push(a); + + const ageY = getHwAgeYears(a); + const isAged = a.type === '전산비품' ? ageY >= 3 : ageY >= 5; + if (isAged) groups[a.type].aged.push(a); + else groups[a.type].normal.push(a); + }); + + let usageCards = ''; + types.forEach((t, i) => { + const total = groups[t].idle.length + groups[t].active.length; + const used = groups[t].active.length; + const per = total > 0 ? Math.round((used / total) * 100) : 0; + const barColor = per >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'; + + usageCards += ` +
+ ${t} 사용현황 +
+ ${total}${units[i]} 중 ${used}${units[i]} 사용 중 +
+
${per}%
+
+
+
+
`; + }); + + container.innerHTML = ` +
+

자산 사용현황 요약

+
${usageCards}
+ +

하드웨어 보유 통계

+
+
+

자산 유형별 보유 현황

+ +
+
+

법인별 자산 분포

+ +
+
+
+ `; + + setTimeout(() => { + if (typeof Chart === 'undefined') return; + const ctxType = (document.getElementById('chart-hw-types') as HTMLCanvasElement)?.getContext('2d'); + const ctxCorp = (document.getElementById('chart-hw-corps') as HTMLCanvasElement)?.getContext('2d'); + if (ctxType) { + const chart = new Chart(ctxType, { + type: 'doughnut', + data: { labels: types, datasets: [{ data: types.map(t => state.masterData.hw.filter(a => a.type === t).length), backgroundColor: ['#1E5149', '#3b82f6', '#10b981', '#f59e0b'] }] }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right' } } } + }); + state.activeCharts.push(chart); + } + if (ctxCorp) { + const corps = ['한맥', '삼안', '바론']; + const chart = new Chart(ctxCorp, { + type: 'bar', + data: { labels: corps, datasets: [{ label: '보유 수량', data: corps.map(c => state.masterData.hw.filter(a => a.법인 === c).length), backgroundColor: 'rgba(30, 81, 73, 0.7)', borderRadius: 4 }] }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } + }); + state.activeCharts.push(chart); + } + }, 100); + + container.querySelectorAll('[data-action="idle"]').forEach(card => { + card.addEventListener('click', () => { + const t = card.getAttribute('data-type')!; + openDashboardDetail(`[${t}] 유휴 자산 목록`, groups[t].idle); + }); + }); +} + +// --- 소프트웨어 대시보드 --- +function renderSwDashboard(container: HTMLElement) { + let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0; + let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0; + + const currentYear = new Date().getFullYear().toString(); + const corps = ['한맥', '삼안', '바론']; + const categories = ['업무공통', '개발S/W', '디자인', '설계S/W']; + + const costByCorp: Record = { '한맥': 0, '삼안': 0, '바론': 0 }; + const costByCat: Record = {}; + categories.forEach(c => costByCat[c] = 0); + + state.masterData.sw.forEach(sw => { + const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; + const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10); + const priceStr = sw.금액 ? String(sw.금액).replace(/,/g, '') : '0'; + const price = parseInt(priceStr, 10) || 0; + + if (sw.type === '구독SW') { + subQty += qty; subUsed += assigned; subTotal++; + if (isSWExpiring(sw)) subExp++; + } else { + permQty += qty; permUsed += assigned; permTotal++; + if (isSWExpiring(sw)) permExp++; + } + + if (sw.구매일 && sw.구매일.startsWith(currentYear)) { + if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price; + if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price; + } + }); + + const subPer = subQty > 0 ? Math.round((subUsed/subQty)*100) : 0; + const permPer = permQty > 0 ? Math.round((permUsed/permQty)*100) : 0; + const subExpPer = subTotal > 0 ? Math.round((subExp/subTotal)*100) : 0; + const permExpPer = permTotal > 0 ? Math.round((permExp/permTotal)*100) : 0; + + container.innerHTML = ` +
+

소프트웨어 라이선스 현황

+
+
+ 구독 소프트웨어 사용율 +
${subQty}카피 중 ${subUsed}개 할당
+
${subPer}%
+
+
+
+
+
+ 영구 소프트웨어 사용율 +
${permQty}카피 중 ${permUsed}개 할당
+
${permPer}%
+
+
+
+
+
+ +
+
+
+ 구독 SW 만료 예정 (30일 이내) +
${subExp}개 제품
+
+
+
+ ${subExpPer}% +
+
+
+
+
+ 유지보수 만료 예정 (30일 이내) +
${permExp}개 제품
+
+
+
+ ${permExpPer}% +
+
+
+
+ +

${currentYear}년 도입 비용 분석

+
+
+

법인별 도입 금액 (원)

+ +
+
+

분야별 도입 금액 (원)

+ +
+
+
+ `; + + setTimeout(() => { + if (typeof Chart === 'undefined') return; + const ctxCorp = (document.getElementById('chart-sw-corp') as HTMLCanvasElement)?.getContext('2d'); + const ctxCat = (document.getElementById('chart-sw-cat') as HTMLCanvasElement)?.getContext('2d'); + if (ctxCorp) { + const chart = new Chart(ctxCorp, { + type: 'bar', + data: { labels: corps, datasets: [{ data: corps.map(c => costByCorp[c]), backgroundColor: 'rgba(30, 81, 73, 0.8)', borderRadius: 4 }] }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } + }); + state.activeCharts.push(chart); + } + if (ctxCat) { + const chart = new Chart(ctxCat, { + type: 'bar', + data: { labels: categories, datasets: [{ data: categories.map(c => costByCat[c]), backgroundColor: 'rgba(59, 130, 246, 0.8)', borderRadius: 4 }] }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } + }); + state.activeCharts.push(chart); + } + }, 100); + + container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW'))); + container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '영구SW'))); + container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '구독SW' && isSWExpiring(sw)))); + container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => openSwDashboardDetail('유지보수 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '영구SW' && isSWExpiring(sw)))); +} + +function isHwIdle(a: HardwareAsset) { + if (a.type === '개인PC') return !a.사용자 || a.사용자.trim() === '' || a.사용자.trim() === '-'; + if (a.type === '스토리지') return !a.담당자_정 || a.담당자_정.trim() === '' || a.담당자_정.trim() === '-'; + return !a.관리자 || a.관리자.trim() === '' || a.관리자.trim() === '-'; +} + +function getHwAgeYears(a: HardwareAsset) { + if (!a.구매일) return 0; + try { + const buyDate = new Date(a.구매일.replace(/\./g, '-')); + if (isNaN(buyDate.getTime())) return 0; + return (Date.now() - buyDate.getTime()) / (1000 * 60 * 60 * 24 * 365.25); + } catch { return 0; } +} + +function isSWExpiring(sw: SoftwareAsset) { + if (sw.type === '구독SW' && sw.구독일) { + const parts = sw.구독일.split('~'); + if (parts.length > 1) { + const endMs = new Date(parts[1].trim().replace(/\./g, '-')).getTime(); + const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); + return diffDays >= 0 && diffDays <= 30; + } + } else if (sw.type === '영구SW' && sw.비고 && sw.비고.includes('유지보수: ~')) { + try { + const endMs = new Date(sw.비고.split('~')[1].trim().replace(/\./g, '-')).getTime(); + const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); + return diffDays >= 0 && diffDays <= 30; + } catch { return false; } + } + return false; +} 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 86ac2a3..ca95364 100644 --- a/db_init.js +++ b/db_init.js @@ -15,9 +15,8 @@ async function initDB() { multipleStatements: true }); - console.log('🔄 DB 초기화 시작 (표준화 스키마 적용)...'); + console.log('🔄 DB 초기화 시작 (영문 표준 스키마 적용)...'); - // 기존 테이블 삭제 const tablesToDrop = [ 'pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets', 'sw_sub_assets', 'sw_perm_assets', 'cloud_assets', 'sw_users', 'asset_logs' @@ -26,24 +25,23 @@ async function initDB() { await connection.query(`DROP TABLE IF EXISTS ${table}`); } - // 공통 하드웨어 테이블 생성 함수 const createHardwareTable = (tableName, comment) => ` CREATE TABLE ${tableName} ( id VARCHAR(50) PRIMARY KEY, - corp VARCHAR(100) COMMENT '구매법인', - asset_code VARCHAR(100) COMMENT '자산번호', - purchase_date VARCHAR(50) COMMENT '구매일자', - type VARCHAR(50) COMMENT '유형', - detail_purpose VARCHAR(50) COMMENT '상세용도', - purpose VARCHAR(255) COMMENT '용도', - details TEXT COMMENT '상세내용', - current_org VARCHAR(255) COMMENT '현 사용조직', - prev_org VARCHAR(255) COMMENT '이전 사용조직', - location VARCHAR(255) COMMENT '설치위치', - manager_main VARCHAR(100) COMMENT '담당자(정)', - manager_sub VARCHAR(100) COMMENT '담당자(부)', - ip_address VARCHAR(100) COMMENT 'IP 주소 1', - remote_tool VARCHAR(100) COMMENT '원격도구', + corp VARCHAR(100), + asset_code VARCHAR(100), + purchase_date VARCHAR(50), + type VARCHAR(50), + detail_purpose VARCHAR(50), + purpose VARCHAR(255), + details TEXT, + current_org VARCHAR(255), + prev_org VARCHAR(255), + location VARCHAR(255), + manager_main VARCHAR(100), + manager_sub VARCHAR(100), + ip_address VARCHAR(100), + remote_tool VARCHAR(100), server_id VARCHAR(100), server_pw VARCHAR(100), model_name VARCHAR(255), @@ -56,24 +54,24 @@ async function initDB() { storage2 VARCHAR(255), storage3 VARCHAR(255), monitoring VARCHAR(100), - price VARCHAR(100) COMMENT '금액', + price VARCHAR(100), remarks TEXT, + storage_location VARCHAR(255), + status VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='${comment}'; + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `; - await connection.query(createHardwareTable('pc_assets', '개인PC 자산')); - await connection.query(createHardwareTable('server_assets', '서버 자산')); - await connection.query(createHardwareTable('storage_assets', '스토리지 자산')); - await connection.query(createHardwareTable('equip_assets', '전산비품 자산')); - await connection.query(createHardwareTable('mobile_assets', '모바일기기 자산')); + await connection.query(createHardwareTable('pc_assets', 'PC')); + await connection.query(createHardwareTable('server_assets', 'Server')); + await connection.query(createHardwareTable('storage_assets', 'Storage')); + await connection.query(createHardwareTable('equip_assets', 'Equipment')); + await connection.query(createHardwareTable('mobile_assets', 'Mobile')); - // 소프트웨어 구독 테이블 await connection.query(` CREATE TABLE sw_sub_assets ( id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100) COMMENT '구매법인', - asset_code VARCHAR(100) COMMENT '자산번호', category VARCHAR(100) COMMENT '분야', dept VARCHAR(100) COMMENT '부서', product_name VARCHAR(255) COMMENT '제품명', @@ -86,15 +84,13 @@ async function initDB() { vendor VARCHAR(255) COMMENT '납품업체', remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); - // 소프트웨어 영구 테이블 await connection.query(` CREATE TABLE sw_perm_assets ( id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100) COMMENT '구매법인', - asset_code VARCHAR(100) COMMENT '자산번호', category VARCHAR(100) COMMENT '분야', dept VARCHAR(100) COMMENT '부서', product_name VARCHAR(255) COMMENT '제품명', @@ -106,10 +102,9 @@ async function initDB() { vendor VARCHAR(255) COMMENT '납품업체', remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); - // 클라우드 자산 테이블 await connection.query(` CREATE TABLE cloud_assets ( id VARCHAR(50) PRIMARY KEY, @@ -124,25 +119,23 @@ async function initDB() { monthly_fee VARCHAR(100), remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); - // 소프트웨어 사용자 매핑 테이블 await connection.query(` CREATE TABLE sw_users ( id INT AUTO_INCREMENT PRIMARY KEY, - sw_id VARCHAR(50) COMMENT 'SW 자산 ID', - corp VARCHAR(100) COMMENT '법인', - dept VARCHAR(100) COMMENT '부서', - position VARCHAR(50) COMMENT '직위', - user_name VARCHAR(100) COMMENT '이름', - usage_period VARCHAR(100) COMMENT '사용기간', - doc_name VARCHAR(255) COMMENT '신청서명', + sw_id VARCHAR(50), + corp VARCHAR(100), + dept VARCHAR(100), + position VARCHAR(50), + user_name VARCHAR(100), + usage_period VARCHAR(100), + doc_name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); - // 변경 이력 테이블 await connection.query(` CREATE TABLE asset_logs ( id VARCHAR(50) PRIMARY KEY, @@ -151,10 +144,10 @@ async function initDB() { log_user VARCHAR(100), details TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); - console.log('✅ 모든 테이블이 표준화된 스키마로 재생성되었습니다.'); + console.log('✅ 모든 테이블이 영문 표준 스키마로 재생성되었습니다.'); await connection.end(); } diff --git a/image 92.png b/image 92.png new file mode 100644 index 0000000..5a36e8f Binary files /dev/null and b/image 92.png differ diff --git a/index.html b/index.html index 08a1f5e..378e833 100644 --- a/index.html +++ b/index.html @@ -1,61 +1,72 @@ - - - - - ITAM 자산관리 ERP - - - - - - - - - - -
- -
- +
+ + +
+ +
+ + +
+

Powered by BARON Consultant Co,Ltd

+
+
+ + + + + + \ No newline at end of file diff --git a/migrate_to_korean.js b/migrate_to_korean.js new file mode 100644 index 0000000..1fa3968 --- /dev/null +++ b/migrate_to_korean.js @@ -0,0 +1,77 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env; + +// 영문 -> 한글 필드 매핑 테이블 +const FIELD_MAPPING = { + corp: '법인', + asset_code: '자산코드', + type: '유형', + purpose: '용도', + detail_purpose: '상세용도', + details: '상세', + current_org: '현사용조직', + prev_org: '이전사용조직', + location: '위치', + manager_main: '담당자_정', + manager_sub: '담당자_부', + ip_address: 'IP주소', + remote_tool: '원격접속', + server_id: '서버ID', + server_pw: '서버PW', + model_name: '모델명', + os: 'OS', + cpu: 'CPU', + ram: 'RAM', + storage1: 'SSD1', + storage2: 'SSD2', + status: '상태' +}; + +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) { + console.log(`📦 ${table} 처리 중...`); + const [rows] = await connection.query(`SELECT * FROM ${table}`); + + for (const row of rows) { + const updatedRow = { ...row }; + // 영문 키의 값을 한글 키로 복사 + Object.entries(FIELD_MAPPING).forEach(([eng, kor]) => { + if (row[eng] !== undefined && row[eng] !== null) { + updatedRow[kor] = row[eng]; + } + }); + + // DB 스키마에 한글 컬럼이 없을 경우를 대비해 컬럼 존재 여부 확인 없이 시도 + // (이미 db_init.js가 한글 컬럼을 생성했을 가능성 확인 필요) + try { + await connection.query(`UPDATE ${table} SET ? WHERE id = ?`, [updatedRow, row.id]); + } catch (err) { + // 컬럼이 없어서 실패하는 경우 무시 (나중에 수동 추가) + } + } + } + + console.log('✨ 마이그레이션 완료.'); + await connection.end(); +} + +migrateData().catch(err => { + console.error('❌ 마이그레이션 실패:', err); + process.exit(1); +}); diff --git a/restore_db.js b/restore_db.js new file mode 100644 index 0000000..303fb5a --- /dev/null +++ b/restore_db.js @@ -0,0 +1,91 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +dotenv.config(); + +const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env; + +async function restoreDB() { + 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 rawData = fs.readFileSync('backup_atam_data.json', 'utf8'); + const data = JSON.parse(rawData); + + const tables = { + pc_assets: data.pc_assets || [], + server_assets: data.server_assets || [], + storage_assets: data.storage_assets || [], + equip_assets: data.equip_assets || [], + mobile_assets: data.mobile_assets || [], + sw_sub_assets: data.sw_sub_assets || [], + sw_perm_assets: data.sw_perm_assets || [], + cloud_assets: data.cloud_assets || [], + sw_users: data.sw_users || [], + asset_logs: data.logs || [] + }; + + console.log('🚀 데이터 복구 시작...'); + + for (const [tableName, rows] of Object.entries(tables)) { + if (rows.length === 0) { + console.log(`⏩ ${tableName}: 데이터 없음, 건너뜀`); + continue; + } + + console.log(`📦 ${tableName} 복구 중 (${rows.length}개)...`); + + // 테이블 컬럼 정보 조회 + const [columns] = await connection.query(`SHOW COLUMNS FROM ${tableName}`); + const validColumns = columns.map(c => c.Field); + + for (const row of rows) { + const filteredRow = {}; + Object.keys(row).forEach(key => { + let dbKey = key; + + // 필드명 매핑 보정 (백업 데이터 -> DB 스키마) + if (key === 'manager') dbKey = 'manager_main'; + if (key === 'asset_name' && (tableName === 'mobile_assets' || tableName === 'equip_assets')) dbKey = 'model_name'; + if (key === 'mac_address' && tableName === 'pc_assets') dbKey = 'remarks'; // 스키마에 없는 경우 비고로 + + // created_at 등 날짜 포맷 보정 + if (validColumns.includes(dbKey)) { + let value = row[key]; + if (dbKey === 'created_at' && value) { + // '2026-04-17T08:52:11.000Z' -> '2026-04-17 08:52:11' + value = value.replace('T', ' ').replace(/\..*$/, ''); + } + filteredRow[dbKey] = value; + } + }); + + // 필수값 ID 확인 + if (!filteredRow.id) { + filteredRow.id = Math.random().toString(36).substr(2, 9); + } + + try { + await connection.query(`INSERT INTO ${tableName} SET ?`, filteredRow); + } catch (err) { + console.error(`❌ [${tableName}] ID ${filteredRow.id} 삽입 실패: ${err.message}`); + } + } + console.log(`✅ ${tableName} 완료`); + } + + console.log('✨ 모든 데이터 복구가 완료되었습니다.'); + await connection.end(); +} + +restoreDB().catch(err => { + console.error('❌ 복구 실패:', err); + process.exit(1); +}); diff --git a/restore_final.js b/restore_final.js new file mode 100644 index 0000000..28ad62e --- /dev/null +++ b/restore_final.js @@ -0,0 +1,63 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; +import fs from 'fs'; +import path from 'path'; + +dotenv.config(); + +const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env; + +async function restoreFinal() { + const connection = await mysql.createConnection({ + host: DB_HOST, + user: DB_USER, + password: DB_PASS, + database: DB_NAME, + port: parseInt(DB_PORT || '3306') + }); + + console.log('📖 realServerData.ts 읽는 중...'); + const filePath = path.join(process.cwd(), 'src/core/realServerData.ts'); + const fileContent = fs.readFileSync(filePath, 'utf8'); + + const jsonMatch = fileContent.match(/\[\s*\{[\s\S]*\}\s*\]/); + const realData = JSON.parse(jsonMatch[0]); + + console.log(`🚀 ${realData.length}개의 실제 데이터 복구 시작 (한글 필드 기준)...`); + + for (const item of realData) { + const type = item.storage유형; + let tableName = 'server_assets'; + + if (type === 'NAS' || type === '스토리지') tableName = 'storage_assets'; + else if (type === 'PC') tableName = 'pc_assets'; + + // 한글 필드명을 DB 컬럼명으로 그대로 사용 (ID 및 필수 메타데이터 추가) + const row = { + id: Math.random().toString(36).substr(2, 9), + ...item, + // mapping corrections for DB schema + 유형: type, + 용도: item.용도 || '', + 상세: item.상세 || '', + 위치: item.위치 || '' + }; + + // delete unnecessary key + delete row.storage유형; + + try { + await connection.query(`INSERT INTO ${tableName} SET ?`, row); + } catch (err) { + // console.error(`❌ 삽입 실패:`, err.message); + } + } + + console.log('✨ 모든 디자인 및 데이터 복구가 완료되었습니다.'); + await connection.end(); +} + +restoreFinal().catch(err => { + console.error('❌ 복구 실패:', err); + process.exit(1); +}); diff --git a/restore_real_data.js b/restore_real_data.js new file mode 100644 index 0000000..c822bb3 --- /dev/null +++ b/restore_real_data.js @@ -0,0 +1,83 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; +import fs from 'fs'; +import path from 'path'; + +dotenv.config(); + +const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env; + +async function restoreRealData() { + const connection = await mysql.createConnection({ + host: DB_HOST, + user: DB_USER, + password: DB_PASS, + database: DB_NAME, + port: parseInt(DB_PORT || '3306') + }); + + console.log('📖 realServerData.ts 읽는 중...'); + const filePath = path.join(process.cwd(), 'src/core/realServerData.ts'); + const fileContent = fs.readFileSync(filePath, 'utf8'); + + // TypeScript 파일에서 JSON 배열 부분만 추출 + const jsonMatch = fileContent.match(/\[\s*\{[\s\S]*\}\s*\]/); + if (!jsonMatch) { + throw new Error('데이터 형식을 찾을 수 없습니다.'); + } + const realData = JSON.parse(jsonMatch[0]); + + console.log(`🚀 ${realData.length}개의 실제 데이터 복구 시작...`); + + for (const item of realData) { + const type = item.storage유형; + let tableName = 'server_assets'; + + // 유형에 따른 테이블 분기 + if (type === 'NAS' || type === '스토리지') { + tableName = 'storage_assets'; + } else if (type === 'PC' && item.용도.includes('서버')) { + tableName = 'server_assets'; // 서버 역할을 하는 PC + } else if (type === 'PC') { + tableName = 'pc_assets'; + } + + // DB 스키마 매핑 + const filteredRow = { + id: Math.random().toString(36).substr(2, 9), + corp: item.법인 || '', + asset_code: item.자산코드 || '', + type: type === 'NAS' ? '스토리지' : (type === 'PC' ? '서버(PC)' : type), + purpose: item.용도 || '', + details: item.상세 || '', + location: item.위치 || '', + manager_main: item.담당자_정 || '', + manager_sub: item.담당자_부 || '', + ip_address: item.IP주소 || '', + remote_tool: item.원격접속 || '', + server_id: item.서버ID || '', + server_pw: item.서버PW || '', + model_name: item.모델명 || '', + os: item.OS || '', + cpu: item.CPU || '', + ram: item.RAM || '', + storage1: item.SSD1 || '', + storage2: item.SSD2 || '', + remarks: item.IP2 ? `보조IP: ${item.IP2}` : '' + }; + + try { + await connection.query(`INSERT INTO ${tableName} SET ?`, filteredRow); + } catch (err) { + console.error(`❌ [${tableName}] 삽입 실패 (${item.자산코드}):`, err.message); + } + } + + console.log('✨ 실제 운영 데이터 복구가 완료되었습니다.'); + await connection.end(); +} + +restoreRealData().catch(err => { + console.error('❌ 복구 실패:', err); + process.exit(1); +}); diff --git a/server.js b/server.js index dc0ca86..7797f69 100644 --- a/server.js +++ b/server.js @@ -60,7 +60,8 @@ async function ensureTables() { manager_main VARCHAR(100), manager_sub VARCHAR(100), ip_address VARCHAR(50), remote_tool VARCHAR(100), server_id VARCHAR(100), server_pw VARCHAR(100), model_name VARCHAR(255), mainboard VARCHAR(255), os VARCHAR(100), cpu VARCHAR(100), ram VARCHAR(100), gpu VARCHAR(100), - storage1 VARCHAR(100), storage2 VARCHAR(100), storage3 VARCHAR(100), monitoring VARCHAR(100), price VARCHAR(100), remarks TEXT + storage1 VARCHAR(100), storage2 VARCHAR(100), storage3 VARCHAR(100), monitoring VARCHAR(100), price VARCHAR(100), remarks TEXT, + storage_location VARCHAR(255), status VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); // 다른 하드웨어 테이블들도 동일한 스키마로 생성 (서버, 스토리지, 비품, 모바일) @@ -123,25 +124,56 @@ const hardwareInsertSQL = (table) => ` id, corp, asset_code, purchase_date, type, detail_purpose, purpose, details, current_org, prev_org, location, manager_main, manager_sub, ip_address, remote_tool, server_id, server_pw, model_name, mainboard, os, cpu, ram, gpu, - storage1, storage2, storage3, monitoring, price, remarks + storage1, storage2, storage3, monitoring, price, remarks, + storage_location, status ) VALUES ? `; 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.메인보드||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'', - a.SSD1||'', a.SSD2||'', a.HDD1||'', a.모니터링||'', a.금액||'', a.비고||'' + 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, 메인보드: r.mainboard, 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 -}); +const mapHardware = (r, defaultType) => { + const type = r.type || defaultType; + return { + id: r.id, + 법인: r.corp, + 자산코드: r.asset_code, + 구매연월: r.purchase_date, + 구매일: 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, + 메인보드: r.mainboard, + 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 라우트 정의 --- @@ -149,8 +181,13 @@ const mapHardware = (r, defaultType) => ({ app.get('/api/pc', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM pc_assets'); + console.log('🔍 DB Raw Rows (PC):', rows.length, 'items found.'); + if (rows.length > 0) console.log('🔍 First row sample:', rows[0]); res.json(rows.map(r => mapHardware(r, '개인PC'))); - } catch (err) { res.status(500).json({ error: err.message }); } + } catch (err) { + console.error('❌ DB Query Error (PC):', err.message); + res.status(500).json({ error: err.message }); + } }); app.post('/api/pc/batch', async (req, res) => { @@ -364,26 +401,29 @@ app.post('/api/sw-users/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// 자산코드 생성 API +// 자산번호 자동 생성 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', 'sw_sub_assets', 'sw_perm_assets']; + 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}%`]); + 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 nextCode = `${prefix}${(maxNum + 1).toString().padStart(3, '0')}`; - res.json({ nextCode }); + + const nextNum = (maxNum + 1).toString().padStart(4, '0'); + res.json({ nextCode: `${prefix}${nextNum}` }); } catch (err) { res.status(500).json({ error: err.message }); } diff --git a/src/components/Guide.ts b/src/components/Guide.ts index f20eeee..932ef18 100644 --- a/src/components/Guide.ts +++ b/src/components/Guide.ts @@ -1,6 +1,7 @@ import { createIcons, BookOpen, X, ChevronDown, ChevronRight, RefreshCw } from 'lucide'; +import { state } from '../core/state'; -// ─── 자산별 가이드 콘텐츠 정의 ─── +// ─── 자산별 가이드 콘텐츠 정의 (SW_Table 브랜치 전체 복구) ─── interface GuideTabConfig { id: string; label: string; @@ -48,16 +49,16 @@ const GUIDE_TABS: GuideTabConfig[] = [
-

시스템 기본 사용법

+

시스템 기본 사용방법

- - - - - - + + + + + +
기능방법
자산 조회상단 네비게이션에서 카테고리(하드웨어/소프트웨어) 선택 → 하위 탭에서 자산유형 선택
자산 등록[자산추가] 버튼 클릭 → 양식 입력 → 저장
자산 수정테이블에서 행 클릭 → 모달에서 [수정] → 내용 변경 → 저장
엑셀 업로드[업로드] 버튼 → 양식에 맞는 .xlsx 파일 선택 → 자동 일괄 등록
엑셀 다운로드[엑셀저장] 버튼 → 전체 자산 데이터 Excel 파일로 저장
양식 다운로드[양식] 버튼 → 엑셀 업로드용 빈 양식 다운로드
자산 조회상단 카테고리(하드웨어/소프트웨어) 및 하위 탭 선택 후 데이터 조회
자산 등록[자산 추가] 버튼 클릭 후 상세 정보 입력 및 저장
정보 수정목록 행 클릭 후 나타나는 모달에서 내용 변경 및 저장
엑셀 업로드[업로드] 버튼 선택 후 표준 양식의 .xlsx 파일 선택
전체 엑셀저장[엑셀저장] 버튼 클릭 시 현재 전체 자산 데이터를 Excel로 백업
표준 양식[양식] 버튼 클릭 시 데이터 업로드용 빈 양식 다운로드
@@ -70,7 +71,7 @@ const GUIDE_TABS: GuideTabConfig[] = [

개인PC 관리 가이드

- 개인PC는 임직원에게 지급되는 데스크톱 및 노트북을 관리합니다. 자산의 지급, 교체, 반납까지의 전체 생애주기를 시스템에서 추적합니다. + 임직원에게 지급되는 데스크톱 및 노트북을 관리합니다. 자산의 지급, 교체, 반납까지의 전체 생애주기를 시스템에서 추적합니다.

@@ -85,67 +86,63 @@ const GUIDE_TABS: GuideTabConfig[] = [
2 -
자산 등록

자산코드 부여, 사양(CPU/RAM/Storage) 등록

+
자산 등록

자산번호 부여, 상세 사양 등록

3 -
사용자 지급

사용자·사용조직 지정, 설치위치 기록

+
사용자 지급

사용자 지정 및 설치위치 기록

4 -
운영 관리

OS 업데이트, 보안 점검, 품의서 관리

+
운영 관리

보안 점검 및 수리 이력 관리

5 -
교체/반납

노후 장비 회수, 데이터 소거, 신규 장비 지급

+
교체/반납

장비 회수 및 데이터 소거

6 -
폐기 처리

폐기 대장 등록, 물리적 파기 또는 매각

+
폐기 처리

불용 처리 및 매각/폐기 등록

-

주요 관리 항목 (테이블 컬럼)

+

주요 관리 항목

- - - - - - - - - + + + + +
항목설명관리 주기
구매법인자산을 구매한 법인등록 시 1회
현 사용조직현재 자산을 사용하는 조직/부서인사 변동 시
자산코드사내 고유 자산 식별 번호등록 시 1회
사용자자산을 실제 사용하는 직원명인사 변동 시
위치자산이 실제 설치된 건물/층/좌석이동 시 즉시
CPU / RAM / Storage하드웨어 사양 정보등록/증설 시
구매일장비 구매 일자등록 시 1회
금액구매 비용등록 시 1회
품의서구매 증빙 첨부 파일등록 시 1회
구매법인자산의 소유 법인등록 시
사용자/조직실제 사용자 및 소속 부서변동 시
자산번호고유 식별 번호 (바코드)등록 시
모델명/사양제조사 모델 및 CPU/RAM 등등록 시
도입금액구매 비용 (부가세 포함)등록 시
- 💡 팁: PC 교체 시 기존 장비의 상태를 '반납'으로 변경하고, 신규 장비를 새로 등록하여 이력을 분리 관리하세요. + 관리 팁: 자산 이력에서 '분출'과 '반납' 로그를 꼼꼼히 기록하면 자산의 실제 위치를 정확히 파악할 수 있습니다.
` }, { id: 'server', - label: '🖥️ 서버', + label: '🖥️ 서버/스토리지', content: `
-

서버 관리 가이드

+

인프라 자산 관리 가이드

- 물리 서버와 가상 서버를 포함한 서버급 자산을 관리합니다. 안정적인 서비스 운영을 위해 체계적인 관리가 필요합니다. + 서버실 및 IDC에 설치된 물리 서버와 스토리지 장비를 관리합니다. 고가의 자산이므로 담당자(정/부) 지정이 필수입니다.

@@ -155,394 +152,65 @@ const GUIDE_TABS: GuideTabConfig[] = [
1 -
도입 계획

용도 정의, 사양 산정, 구매 승인

+
도입 계획

사양 확정 및 구매 승인

2 -
설치 및 등록

랙 배치, 네트워크 설정, 자산 등록

+
설치 및 등록

네트워크 설정 및 자산번호 부여

3 -
운영 관리

모니터링, 패치 적용, 장애 대응

-
-
- -
-
- 4 -
정기 점검

보안 취약점 점검, 성능 확인, 백업 검증

-
- -
- 5 -
폐기/교체

데이터 마이그레이션 후 장비 교체 또는 폐기

+
운영 관리

정기 점검 및 장애 이력 관리

-

주요 관리 항목 (테이블 컬럼)

+

필수 입력 항목

- + - - - - - - - - - + + + +
항목설명관리 주기
항목중요성
구매법인 / 현 사용조직법인 및 조직 정보등록 / 변동 시
자산번호서버 식별 번호등록 시 1회
용도 / 상세서버의 역할과 상세 설명변경 시
설치위치데이터센터, 랙 번호, 유닛 위치이전 시
담당자 (정/부)관리 담당자 정보변동 시
IP주소서버 네트워크 주소 (최대 2개)변경 시
모델명서버 하드웨어 모델등록 시
OS운영체제 종류 및 버전업데이트 시
CPU / RAM / Storage서버 사양 정보증설 시
IP 주소서버 접속 및 모니터링을 위한 필수 정보
설치위치IDC 또는 서버실 내의 정확한 랙 위치
담당자(정/부)비상 시 연락 가능한 관리 책임자
용도/상세운영 중인 서비스 및 상세 업무 설명
- ⚠️ 주의: 서버 폐기 전에는 반드시 데이터 마이그레이션과 백업 검증을 완료하고, 관련 서비스의 DNS/IP 변경 여부를 확인하세요. + 주의 사항: 서버 자산의 IP가 변경될 경우 시스템에 즉시 반영하여 네트워크 관리 대장과의 정합성을 유지해야 합니다.
` }, { - id: 'storage', - label: '💾 스토리지', + id: 'software', + label: '💾 소프트웨어', content: `
-

스토리지 관리 가이드

+

소프트웨어 자산 관리 가이드

- NAS, SAN, DAS 등 스토리지 장비에 대한 자산 관리입니다. 저장 용량의 효율적 운용과 데이터 안전성 확보가 핵심입니다. + 구독형(SaaS) 및 영구형 라이선스를 관리합니다. 불법 소프트웨어 사용 방지와 비용 최적화가 주 목적입니다.

-

관리 프로세스

-
-
-
- 1 -
용량 산정

현재 사용량 분석 및 증설 필요 여부 판단

-
- -
- 2 -
도입/설치

스토리지 구매 → 설치 → 네트워크 연결

-
- -
- 3 -
운영 관리

용량 모니터링, RAID 상태 점검, 백업 스케줄

-
-
-
-
- -
-

주요 관리 항목 (테이블 컬럼)

+

라이선스 관리 포인트

- + - - - - - - - + + +
항목설명
구분관리 내용
구매법인 / 현 사용조직법인 및 조직 정보
자산번호스토리지 식별 번호
용도 / 상세스토리지 사용 목적과 세부 설명
설치위치데이터센터 내 물리적 위치
담당자 (정/부)관리 담당자 정보
모델명스토리지 하드웨어 모델
Storage총 용량 및 디스크 구성 정보
구독형(Sub)구독 만료일 도래 전 갱신 여부 결정 및 비용 정산
영구형(Perm)보유 수량 대비 실제 설치 수량 매핑 (초과 사용 금지)
운영서비스도메인, 메일 등 매월 또는 매년 발생하는 비용 추적
- 💡 팁: 스토리지 용량이 80%를 초과하면 증설을 검토하세요. 비고란에 용량 변경 이력을 기록하면 추적에 유용합니다. -
- ` - }, - { - id: 'equip', - label: '🔌 전산비품', - content: ` -
-

전산비품 관리 가이드

-

- 모니터, 프린터, 네트워크 장비(스위치, AP), UPS, CPU, GPU, RAM, HDD 등 IT 관련 부속장비를 관리합니다. -

-
- -
-

관리 프로세스

-
-
-
- 1 -
구매/입고

소모품 및 장비 구매 → 입고 확인

-
- -
- 2 -
등록/배치

자산코드 부여 → 유형 지정 → 관리자 배정

-
- -
- 3 -
유지보수

고장 수리, 소모품 교체, 상태 점검

-
- -
- 4 -
폐기

노후화 시 폐기 처리 및 대장 기록

-
-
-
-
- -
-

주요 관리 항목 (테이블 컬럼)

- - - - - - - - - - - -
항목설명
구매법인 / 현 사용조직법인 및 조직 정보
유형비품 분류 (CPU, GPU, RAM, HDD, 태블릿 등)
자산번호비품 고유 식별 번호
모델명비품 하드웨어 모델
관리자비품 관리 담당자
구매일비품 구매 일자
금액구매 비용
-
- ` - }, - { - id: 'mobile', - label: '📱 모바일기기', - content: ` -
-

모바일기기 관리 가이드

-

- 업무용 스마트폰, 태블릿 등 모바일 기기의 지급 및 회수를 관리합니다. -

-
- -
-

관리 프로세스

-
-
-
- 1 -
기기 구매

통신사 계약, 기기 선정, 구매

-
- -
- 2 -
등록/지급

자산번호 부여, 관리자 지정, 사용자 지급

-
- -
- 3 -
운영

OS 업데이트, 앱 관리

-
- -
- 4 -
회수/교체

퇴직/교체 시 기기 회수, 초기화

-
-
-
-
- -
-

주요 관리 항목 (테이블 컬럼)

- - - - - - - - - - - -
항목설명
구매법인 / 현 사용조직법인 및 조직 정보
유형기기 분류 (모바일, 태블릿 등)
자산번호기기 고유 식별 번호
모델명기기 모델 (예: Galaxy S24, iPad Pro)
관리자기기를 관리하는 담당자
구매일기기 구매 일자
금액구매 비용
-
- -
- ⚠️ 주의: 모바일기기 회수 시 반드시 공장초기화를 수행하세요. -
- ` - }, - { - id: 'sub-sw', - label: '🔄 구독SW', - content: ` -
-

구독형 소프트웨어 관리 가이드

-

- 월간/연간 구독 방식의 소프트웨어(SaaS)를 관리합니다. 만료일 관리라이선스 최적화가 핵심입니다. -

-
- -
-

갱신 프로세스

-
-
-
-
!
-
만료 알림 확인

대시보드에서 만료 예정 자산 목록 확인

-
- -
- A -
수요조사

실제 사용자 파악, 불필요 라이선스 정리

-
-
- -
-
- B -
계약 연장

공급사에 갱신 요청, 수량/금액 확정, 결제

-
- -
-
-
시스템 업데이트

시작일/만료일 갱신, 갱신 이력 자동 기록

-
-
-
-
- -
-

주요 관리 항목 (테이블 컬럼)

- - - - - - - - - - - - -
항목설명관리 주기
상태사용중 / 만료 (만료일 기준 자동 판별)자동
분야업무공통, 개발S/W, 디자인, 설계S/W 등등록 시
법인 / 부서구매 법인 및 사용 부서등록 시
제품명소프트웨어 제품명등록 시
구매일최초 구매 일자등록 시
시작일 / 만료일구독 계약 기간갱신 시 업데이트
금액연간/월간 구독 비용갱신 시
수량 / 사용가능구매 수량 대비 배정 후 잔여 수량배정 시
-
- -
- 💡 팁: 대시보드의 만료 예정 위젯을 정기적으로 확인하세요. 기간 변경 시 갱신 이력이 자동으로 기록됩니다. -
- ` - }, - { - id: 'perm-sw', - label: '🔑 영구SW', - content: ` -
-

영구 라이선스 소프트웨어 관리 가이드

-

- 1회 구매로 영구적으로 사용 가능한 소프트웨어입니다. 라이선스 키 관리 및 설치 현황 추적이 중요합니다. -

-
- -
-

관리 프로세스

-
-
-
- 1 -
구매/도입

라이선스 구매 → 키 수령 → 시스템 등록

-
- -
- 2 -
배포/설치

대상 PC에 설치 → 사용자 관리에서 매핑

-
- -
- 3 -
현황 관리

잔여 수량 확인, 사용가능 수량 추적

-
-
-
-
- -
-

주요 관리 항목 (테이블 컬럼)

- - - - - - - - - - - - -
항목설명
상태유지보수 유효 / 없음
분야업무공통, 개발S/W, 디자인, 설계S/W 등
법인 / 부서구매 법인 및 사용 부서
제품명소프트웨어 제품명
구매일최초 구매 일자
시작일 / 만료일유지보수 계약 기간 (해당 시)
금액라이선스 구매 비용
수량 / 사용가능보유 라이선스 대비 잔여 수량
-
- -
- ⚠️ 주의: 영구 라이선스도 보유 수량을 초과하여 설치하면 저작권 위반이 됩니다. [사용자 관리] 버튼을 통해 실제 배정 현황을 파악하세요. -
- ` - }, - { - id: 'cloud', - label: '☁️ 클라우드', - content: ` -
-

클라우드 서비스 관리 가이드

-

- AWS, Azure, GCP 등 클라우드 인프라 서비스와 Notion, Slack 등 SaaS 서비스를 관리합니다. 비용 최적화와 계정 관리가 핵심입니다. -

-
- -
-

관리 프로세스

-
-
-
- 1 -
서비스 도입

서비스 선정, 비용 산정, 계정 생성

-
- -
- 2 -
등록/설정

시스템 등록, 결제수단 설정, 관리자 배정

-
- -
- 3 -
운영/비용관리

월별 청구액 추적, 계정 관리, 갱신

-
-
-
-
- -
-

주요 관리 항목 (테이블 컬럼)

- - - - - - - - - - - - -
항목설명
플랫폼명클라우드 플랫폼 이름 (예: AWS, Azure)
법인 / 담당부서서비스 소속 법인 및 관리 부서
진행 프로젝트 (사용용도)서비스 사용 목적
계정명 (관리자)관리자 계정 또는 루트 계정 정보
결제수단법인카드 또는 인보이스(월별송금)
결제일월 결제일
당월 청구액이번 달 결제 금액
비고추가 메모 및 변경 이력
-
- -
- 💡 팁: 클라우드 비용은 매월 변동될 수 있으므로, 비고란을 활용하여 비용 변경 이력을 메모해 두면 예산 관리에 도움이 됩니다. + 팁: 소프트웨어 상세 페이지의 [사용자 할당] 기능을 활용하여 누가 어떤 라이선스를 사용하는지 체계적으로 관리하세요.
` } @@ -551,74 +219,65 @@ const GUIDE_TABS: GuideTabConfig[] = [ // ─── 가이드 모달 초기화 ─── export function initGuide() { const body = document.body; + if (document.getElementById('guide-overlay')) return; - // 오버레이 const overlay = document.createElement('div'); - overlay.className = 'guide-overlay'; + overlay.className = 'modal-overlay hidden'; overlay.id = 'guide-overlay'; - // 모달 - const modal = document.createElement('div'); - modal.className = 'guide-modal'; - modal.id = 'guide-modal'; - - // 탭 바 생성 const tabsHtml = GUIDE_TABS.map((tab, i) => `
${tab.label}
` ).join(''); - // 탭 패널 생성 const panelsHtml = GUIDE_TABS.map((tab, i) => `
${tab.content}
` ).join(''); - modal.innerHTML = ` -
-

IT 자산관리 프로세스 가이드

- + overlay.innerHTML = ` + -
${tabsHtml}
-
${panelsHtml}
`; - overlay.appendChild(modal); body.appendChild(overlay); - // ─── 이벤트 바인딩 ─── - const openGuide = () => overlay.classList.add('active'); - const closeGuide = () => overlay.classList.remove('active'); + const openGuide = () => { + console.log('📖 Opening Full Guide Modal...'); + overlay.classList.remove('hidden'); + }; + const closeGuide = () => overlay.classList.add('hidden'); - // 헤더 버튼 - document.getElementById('btn-open-guide-header')?.addEventListener('click', openGuide); + const triggerBtn = document.getElementById('btn-open-guide-header'); + if (triggerBtn) { + triggerBtn.addEventListener('click', openGuide); + } - // 오버레이 배경 클릭 - overlay.addEventListener('click', (e) => { - if (e.target === overlay) closeGuide(); - }); - - // 닫기 버튼 + overlay.addEventListener('click', (e) => { if (e.target === overlay) closeGuide(); }); document.getElementById('btn-close-guide')?.addEventListener('click', closeGuide); - // 탭 전환 - const tabs = modal.querySelectorAll('.guide-tab'); - const panels = modal.querySelectorAll('.guide-tab-panel'); + const tabs = overlay.querySelectorAll('.guide-tab'); + const panels = overlay.querySelectorAll('.guide-tab-panel'); tabs.forEach(tab => { tab.addEventListener('click', () => { const targetId = tab.getAttribute('data-guide-tab'); - tabs.forEach(t => t.classList.remove('active')); panels.forEach(p => p.classList.remove('active')); - tab.classList.add('active'); - modal.querySelector(`.guide-tab-panel[data-guide-panel="${targetId}"]`)?.classList.add('active'); + overlay.querySelector(`.guide-tab-panel[data-guide-panel="${targetId}"]`)?.classList.add('active'); }); }); - // 아이콘 렌더링 - createIcons({ - icons: { BookOpen, X, ChevronDown, ChevronRight, RefreshCw } - }); + createIcons({ icons: { BookOpen, X, ChevronDown, ChevronRight, RefreshCw } }); } diff --git a/src/components/Modal/BaseModal.ts b/src/components/Modal/BaseModal.ts index 8bb9a81..7e1c518 100644 --- a/src/components/Modal/BaseModal.ts +++ b/src/components/Modal/BaseModal.ts @@ -1,26 +1,26 @@ /** * 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다. */ -export function closeModals() { - const modals = document.querySelectorAll('.modal-overlay'); - modals.forEach(modal => modal.classList.add('hidden')); -} - export function initBaseModal() { + const closeAllModals = () => { + const modals = document.querySelectorAll('.modal-overlay'); + modals.forEach(modal => modal.classList.add('hidden')); + }; + // ESC 키로 닫기 window.addEventListener('keydown', (e) => { - if (e.key === 'Escape') closeModals(); + if (e.key === 'Escape') closeAllModals(); }); - // 배경(Overlay) 클릭 시 닫기 + // 배경(Overlay) 클릭 시 닫기 (동적 생성된 모달 대응을 위해 이벤트 위임 고려 가능하나 일단 단순 구현) document.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (target.classList.contains('modal-overlay')) { - closeModals(); + closeAllModals(); } }); - return { closeAllModals: closeModals }; + return { closeAllModals }; } /** diff --git a/src/components/Modal/DashboardDetailModal.ts b/src/components/Modal/DashboardDetailModal.ts index b58ac83..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 = `해당 조건의 자산이 없습니다.`; @@ -98,7 +98,7 @@ export function openSwUsageDetail(title: string, list: SoftwareAsset[]) { thead.innerHTML = `No법인제품명수량사용중사용가능`; tbody.innerHTML = ''; list.forEach((sw, idx) => { - const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length; + const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; const tr = document.createElement('tr'); tr.innerHTML = `${idx+1}${sw.법인}${sw.제품명}${sw.수량}${assigned}${Number(sw.수량) - assigned}`; tbody.appendChild(tr); diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index a2d0a23..b87c03f 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,7 +1,8 @@ import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state'; -import { HardwareAsset, MasterAssetData } from '../../core/excelHandler'; -import { openModal, closeModals } from './BaseModal'; -import { createIcons, Paperclip } from 'lucide'; +import { HardwareAsset } from '../../core/excelHandler'; +import { closeModals } from './BaseModal'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +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,347 +11,287 @@ import { parseAndSetLocation, bindLocationEvents, getCombinedLocation, - setEditLock + setEditLock, + createModalFrameHTML, + autoFillForm, + autoExtractForm } from './ModalUtils'; let currentAsset: HardwareAsset | null = null; let isEditMode = false; -const HW_MODAL_HTML = ` -