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_init.js b/db_init.js new file mode 100644 index 0000000..46862bd --- /dev/null +++ b/db_init.js @@ -0,0 +1,107 @@ +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 initDB() { + const connection = await mysql.createConnection({ + host: DB_HOST, + user: DB_USER, + password: DB_PASS, + port: parseInt(DB_PORT || '3306') + }); + + console.log('🚀 DB 초기화 시작...'); + + // 1. 데이터베이스 생성 + await connection.query(`CREATE DATABASE IF NOT EXISTS ${DB_NAME};`); + await connection.query(`USE ${DB_NAME};`); + console.log(`✅ 데이터베이스 생성 완료: ${DB_NAME}`); + + // 2. 하드웨어 자산 테이블 + const createHwTable = ` + CREATE TABLE IF NOT EXISTS hw_assets ( + id VARCHAR(50) PRIMARY KEY, + type VARCHAR(50) NOT NULL COMMENT '개인PC, 서버, 스토리지, 전산비품', + corp VARCHAR(100) COMMENT '구매법인', + asset_code VARCHAR(100) COMMENT '자산번호/코드', + asset_name VARCHAR(255) COMMENT '명칭/용도', + location VARCHAR(255) COMMENT '설치위치', + current_org VARCHAR(255) COMMENT '현 사용조직', + prev_org VARCHAR(255) COMMENT '이전 사용조직', + manager_main VARCHAR(100) COMMENT '담당자(정)', + manager_sub VARCHAR(100) COMMENT '담당자(부)', + ip_address VARCHAR(100) COMMENT 'IP 주소 1', + ip_address2 VARCHAR(100) COMMENT 'IP 주소 2', + mac_address VARCHAR(100) COMMENT 'MAC 주소', + os VARCHAR(100), + cpu VARCHAR(255), + ram VARCHAR(100), + storage1 VARCHAR(255), + storage2 VARCHAR(255), + model_name VARCHAR(255), + purchase_date VARCHAR(50), + price VARCHAR(100), + vendor VARCHAR(255) COMMENT '납품업체', + doc_name VARCHAR(255) COMMENT '품의서명', + remote_tool VARCHAR(100) COMMENT '원격도구', + server_id VARCHAR(100), + server_pw VARCHAR(100), + monitoring VARCHAR(100), + remarks TEXT COMMENT '비고', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + + // 3. 소프트웨어 자산 테이블 + const createSwTable = ` + CREATE TABLE IF NOT EXISTS sw_assets ( + id VARCHAR(50) PRIMARY KEY, + type VARCHAR(50) NOT NULL COMMENT '구독SW, 영구SW', + category VARCHAR(100) COMMENT '분야', + corp VARCHAR(100) COMMENT '구매법인', + dept VARCHAR(100) COMMENT '부서', + product_name VARCHAR(255) NOT NULL, + purchase_date VARCHAR(50), + subscription_date VARCHAR(50), + maintenance_status TINYINT(1) DEFAULT 0, + price VARCHAR(100), + quantity INT DEFAULT 1, + account_id VARCHAR(255) COMMENT '계정명', + vendor VARCHAR(255), + remarks TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + + // 4. 소프트웨어 사용자 매핑 테이블 + const createSwUsersTable = ` + CREATE TABLE IF NOT EXISTS sw_users ( + id VARCHAR(50) PRIMARY KEY, + sw_id VARCHAR(50), + corp VARCHAR(100), + dept VARCHAR(100), + team VARCHAR(100), + position VARCHAR(50), + name VARCHAR(100), + usage_period VARCHAR(100), + doc_name VARCHAR(255), + FOREIGN KEY (sw_id) REFERENCES sw_assets(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + + await connection.query(createHwTable); + await connection.query(createSwTable); + await connection.query(createSwUsersTable); + + console.log('✅ 테이블 생성 완료!'); + await connection.end(); + console.log('🏁 DB 초기화 프로세스 종료.'); +} + +initDB().catch(err => { + console.error('❌ DB 초기화 실패:', err); + process.exit(1); +}); diff --git a/docs/issues/issue_sw_modal_refactor.md b/docs/issues/issue_sw_modal_refactor.md new file mode 100644 index 0000000..72883d0 --- /dev/null +++ b/docs/issues/issue_sw_modal_refactor.md @@ -0,0 +1,33 @@ +# [이슈] S/W 자산 관리 고도화 및 이력 추적 기능 구현 + +## 1. 개요 +소프트웨어 자산의 라이프사이클을 체계적으로 관리하기 위해 상세 정보 모달을 개편하고, 갱신(업데이트) 이력을 추적할 수 있는 기능을 구현하였습니다. 또한, 사용자의 가독성을 위해 상태를 나타내는 자동 뱃지를 도입하고 날짜 입력 편의성을 개선하였습니다. + +## 2. 작업 상세 내용 + +### A. S/W 목록(Table) 개선 +- **상태 자동 계산 시스템 도입**: + - 구독 S/W: 만료일 기준 **[사용중] / [만료]** 자동 표시. + - 영구 S/W: 유지보수 대상 여부에 따라 **[유효] / [없음]** 표시. +- **UI 뱃지 적용**: 테이블 좌측에 상태 뱃지를 추가하여 시각적 인지도를 높임. + +### B. 상세 정보 모달 개편 (`SWModal.ts`) +- **2단 분할 레이아웃 적용**: 좌측(기본 정보), 우측(업데이트 타임라인)으로 UI 재설계. +- **날짜 입력 필드 개선**: + - '구매일' 필드에 캘린더 피커(Calendar Picker) 적용. + - '구독 기간' 필드를 **시작일**과 **종료일**로 분리하여 각각 캘린더 적용. + - 직접 입력("yyyy-mm-dd") 형식도 동시 지원. + +### C. 계약 업데이트(갱신) 관리 기능 +- **[업데이트 추가]** 버튼 및 전용 서브 팝업 구현. +- 갱신 시 발생하는 비용, 기간 연장, 메모를 기록하여 타임라인(Log)에 누적. +- 업데이트 반영 시 메인 자산 정보의 구독 기한 및 누적 금액이 자동으로 최신화되도록 연동. + +## 3. 관련 파일 +- `src/views/SW_Table.ts`: 테이블 상태 로직 및 뱃지 렌더링. +- `src/components/Modal/SWModal.ts`: 모달 UI 및 날짜 처리, 업데이트 로직. +- `src/styles/modal.css`: 분할 레이아웃 및 타임라인 스타일. + +## 4. 확인 사항 +- 엑셀 업로드/다운로드 시 기존 '구독일' 문자열 형식과의 호환성 유지 확인. +- 브라우저 테스트를 통한 캘린더 작동 및 테이블 상태 연동 확인 완료. diff --git a/index.html b/index.html index a5c94b1..d6f5132 100644 --- a/index.html +++ b/index.html @@ -8,68 +8,50 @@ + +
- - - - -
-
-
-

하드웨어 / 대시보드

+ +
+
+
+
-
- -
-
+ +
+ +
- + diff --git a/package-lock.json b/package-lock.json index d08dc5d..469f08b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "hm-itam", "version": "0.0.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" }, "devDependencies": { @@ -764,6 +768,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", @@ -773,6 +800,77 @@ "node": ">=0.8" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -795,6 +893,63 @@ "node": ">=0.8" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -807,6 +962,112 @@ "node": ">=0.8" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -846,6 +1107,94 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/frac": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", @@ -855,6 +1204,15 @@ "node": ">=0.8" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -870,12 +1228,282 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lucide": { "version": "0.364.0", "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.364.0.tgz", "integrity": "sha512-fUicNBP/uinzxvHUch75z2swiNwRDanakwAB3lgKx2vv6nFeJNjteDkwmmbUrlWsVZqZvO9CDQZQepoB3YDbnw==", "license": "ISC" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.1.tgz", + "integrity": "sha512-48+9UXehKyxxiP2pqCxUq+MSFvX+v41jwsSpFDQO/jAoFuAELutBGJUhWJnDbe82/OBlIhSBMC82WeonmznT/Q==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -895,6 +1523,76 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -931,6 +1629,58 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -976,6 +1726,151 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -986,6 +1881,21 @@ "node": ">=0.10.0" } }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -998,6 +1908,38 @@ "node": ">=0.8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1012,6 +1954,31 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT", + "peer": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -1090,6 +2057,12 @@ "node": ">=0.8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", diff --git a/package.json b/package.json index baeec0b..8b0150a 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,20 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "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/server.js b/server.js new file mode 100644 index 0000000..e05ed1d --- /dev/null +++ b/server.js @@ -0,0 +1,224 @@ +import express from 'express'; +import mysql from 'mysql2/promise'; +import cors from 'cors'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.json({ limit: '50mb' })); + +// DB 연결 풀 생성 +const pool = mysql.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + port: parseInt(process.env.DB_PORT || '3306'), + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +// --- API Routes --- + +// 1. 하드웨어 자산 조회 +app.get('/api/hw', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM hw_assets'); + // DB 컬럼명을 프론트엔드 인터페이스(한글)에 맞게 매핑 + const mapped = rows.map(r => ({ + id: r.id, + type: r.type, + 법인: r.corp, + 자산코드: r.asset_code, + 명칭: r.asset_name, + 위치: r.location, + 현사용조직: r.current_org, + 이전사용조직: r.prev_org, + 담당자_정: r.manager_main, + 관리자: r.manager_main, + 담당자_부: r.manager_sub, + IP주소: r.ip_address, + IP2: r.ip_address2, + MACaddress: r.mac_address, + OS: r.os, + CPU: r.cpu, + RAM: r.ram, + SSD1: r.storage1, + SSD2: r.storage2, + 모델명: r.model_name, + 구매일: r.purchase_date, + 금액: r.price, + 납품업체: r.vendor, + 품의서명: r.doc_name, + 용도: r.asset_name, // 서버의 경우 명칭을 용도로 사용 + 상세: r.remarks, + 원격접속: r.remote_tool, + 서버ID: r.server_id, + 서버PW: r.server_pw, + 모니터링: r.monitoring, + 비고: r.remarks + })); + res.json(mapped); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 2. 하드웨어 자산 일괄 저장 (항상 덮어쓰기) +app.post('/api/hw/batch', async (req, res) => { + const assets = req.body; + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + + await connection.query('DELETE FROM hw_assets'); + + if (assets.length > 0) { + const sql = ` + INSERT INTO hw_assets ( + id, type, corp, asset_code, asset_name, location, current_org, prev_org, + manager_main, manager_sub, ip_address, ip_address2, mac_address, os, + cpu, ram, storage1, storage2, model_name, purchase_date, price, + vendor, doc_name, remote_tool, server_id, server_pw, monitoring, remarks + ) VALUES ? + `; + const values = assets.map(a => [ + a.id, a.type, a.법인, a.자산코드, a.명칭 || a.용도, a.위치, a.현사용조직, a.이전사용조직, + a.담당자_정 || a.관리자, a.담당자_부, a.IP주소, a.IP2, a.MACaddress, a.OS, + a.CPU, a.RAM, a.SSD1, a.SSD2, a.모델명, a.구매일, a.금액, + a.납품업체, a.품의서명, a.원격접속, a.서버ID, a.서버PW, a.모니터링, a.비고 || a.상세 + ]); + await connection.query(sql, [values]); + } + + await connection.commit(); + res.json({ success: true, count: assets.length, mode: 'overwrite' }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { + connection.release(); + } +}); + +// 3. 소프트웨어 자산 조회 +app.get('/api/sw', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM sw_assets'); + const mapped = rows.map(r => ({ + id: r.id, + type: r.type, + 분야: r.category, + 법인: r.corp, + 부서: r.dept, + 제품명: r.product_name, + 구매일: r.purchase_date, + 구독일: r.subscription_date, + 유지보수여부: !!r.maintenance_status, + 금액: r.price, + 수량: r.quantity, + 계정명: r.account_id, + 납품업체: r.vendor, + 비고: r.remarks + })); + res.json(mapped); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 4. 소프트웨어 자산 일괄 저장 (항상 덮어쓰기) +app.post('/api/sw/batch', async (req, res) => { + const assets = req.body; + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + + await connection.query('DELETE FROM sw_assets'); + + if (assets.length > 0) { + const sql = ` + INSERT INTO sw_assets ( + id, type, category, corp, dept, product_name, purchase_date, + subscription_date, maintenance_status, price, quantity, + account_id, vendor, remarks + ) VALUES ? + `; + const values = assets.map(a => [ + a.id, a.type, a.분야, a.법인, a.부서, a.제품명, a.구매일, + a.구독일, a.유지보수여부 ? 1 : 0, a.금액, a.수량, + a.계정명, a.납품업체, a.비고 + ]); + await connection.query(sql, [values]); + } + + await connection.commit(); + res.json({ success: true, count: assets.length, mode: 'overwrite' }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { + connection.release(); + } +}); + +// 5. SW 사용자 매핑 조회 +app.get('/api/sw-users', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM sw_users'); + const mapped = rows.map(r => ({ + id: r.id, + swId: r.sw_id, + 법인: r.corp, + 부서: r.dept, + 팀: r.team, + 직위: r.position, + 이름: r.name, + 사용기간: r.usage_period, + 신청서명: r.doc_name + })); + res.json(mapped); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 6. SW 사용자 일괄 저장 (항상 덮어쓰기) +app.post('/api/sw-users/batch', async (req, res) => { + const users = req.body; + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + + await connection.query('DELETE FROM sw_users'); + + if (users.length > 0) { + const sql = ` + INSERT INTO sw_users ( + id, sw_id, corp, dept, team, position, name, usage_period, doc_name + ) VALUES ? + `; + const values = users.map(u => [ + u.id, u.swId, u.법인, u.부서, u.팀, u.직위, u.이름, u.사용기간, u.신청서명 + ]); + await connection.query(sql, [values]); + } + + await connection.commit(); + res.json({ success: true, count: users.length, mode: 'overwrite' }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { + connection.release(); + } +}); + +app.listen(PORT, () => { + console.log(`📡 ITAM API Server running on http://localhost:${PORT}`); +}); diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 2ca50d1..462c50e 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,6 +1,5 @@ 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; @@ -245,7 +244,7 @@ function fillHwFormData(asset: HardwareAsset) { } } -export function initHwModal() { +export function initHwModal(onSave: () => void, closeModals: () => void) { // HTML 주입 if (!document.getElementById('hw-asset-modal')) { document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML); @@ -260,7 +259,7 @@ export function initHwModal() { const deleteBtn = document.getElementById('btn-delete-hw-asset')!; const closeModal = () => { - modal.classList.add('hidden'); + closeModals(); isEditMode = false; }; @@ -337,7 +336,7 @@ export function initHwModal() { const idx = state.masterData.hw.findIndex(a => a.id === assetId); if (idx > -1) { state.masterData.hw[idx] = updated; - renderTable(document.getElementById('main-content')!); + onSave(); switchToViewMode(); } }); @@ -346,7 +345,7 @@ export function initHwModal() { if (!currentAsset) return; if (confirm('정말로 이 자산을 삭제하시겠습니까?')) { state.masterData.hw = state.masterData.hw.filter(a => a.id !== currentAsset!.id); - renderTable(document.getElementById('main-content')!); + onSave(); closeModal(); } }); diff --git a/src/components/Modal/PCModal.ts b/src/components/Modal/PCModal.ts index cce66ee..0061d71 100644 --- a/src/components/Modal/PCModal.ts +++ b/src/components/Modal/PCModal.ts @@ -109,8 +109,9 @@ const PC_MODAL_HTML = ` @@ -123,15 +124,66 @@ export function initPcModal(renderContent: () => void, closeModals: () => void) } 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 btnCancelPc = document.getElementById('btn-cancel-pc-modal') as HTMLButtonElement; - const btnClosePc = document.getElementById('btn-close-pc-modal') as HTMLButtonElement; + const btnCloseHeader = document.getElementById('btn-close-pc-modal') as HTMLButtonElement; + const btnCloseFooter = document.getElementById('btn-close-pc-footer') as HTMLButtonElement; - btnCancelPc?.addEventListener('click', closeModals); - btnClosePc?.addEventListener('click', closeModals); + 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; } diff --git a/src/components/Modal/SWModal.ts b/src/components/Modal/SWModal.ts index 5ae0625..8c858db 100644 --- a/src/components/Modal/SWModal.ts +++ b/src/components/Modal/SWModal.ts @@ -1,109 +1,231 @@ import { state } from '../../core/state'; import { SoftwareAsset } from '../../core/excelHandler'; import { openModal } from './BaseModal'; +import { createIcons, X, History, Plus } from 'lucide'; const SW_MODAL_HTML = `