From 34baea914354daade372d03ae3edd9651d59582d Mon Sep 17 00:00:00 2001 From: JooWangi Date: Tue, 21 Apr 2026 11:40:54 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EB=B0=8F=20=ED=8F=AC=ED=8A=B8=20=EB=8F=99=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 71 ++++++++++++++++++-- src/components/Modal/BaseModal.ts | 18 ++--- src/components/Modal/DashboardDetailModal.ts | 2 +- src/components/Modal/SWUserModal.ts | 14 ++-- src/core/dummyDataGenerator.ts | 2 +- src/core/excelHandler.ts | 9 ++- src/core/state.ts | 21 ++++-- 7 files changed, 106 insertions(+), 31 deletions(-) diff --git a/server.js b/server.js index 103bafb..7944877 100644 --- a/server.js +++ b/server.js @@ -52,7 +52,44 @@ async function ensureTables() { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); - console.log('✅ Cloud & Logs tables ensured.'); + await connection.query(` + CREATE TABLE IF NOT EXISTS pc_assets ( + id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), purchase_date VARCHAR(50), + type VARCHAR(50), detail_purpose VARCHAR(100), purpose VARCHAR(255), details TEXT, + current_org VARCHAR(100), prev_org VARCHAR(100), location VARCHAR(255), + manager_main VARCHAR(100), manager_sub VARCHAR(100), ip_address VARCHAR(50), + remote_tool VARCHAR(100), server_id VARCHAR(100), server_pw VARCHAR(100), + model_name VARCHAR(255), os VARCHAR(100), cpu VARCHAR(100), ram VARCHAR(100), gpu VARCHAR(100), + storage1 VARCHAR(100), storage2 VARCHAR(100), storage3 VARCHAR(100), monitoring VARCHAR(100), price VARCHAR(100), remarks TEXT + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + // 다른 하드웨어 테이블들도 동일한 스키마로 생성 (서버, 스토리지, 비품, 모바일) + for (const table of ['server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']) { + await connection.query(`CREATE TABLE IF NOT EXISTS ${table} LIKE pc_assets`); + } + + await connection.query(` + CREATE TABLE IF NOT EXISTS sw_sub_assets ( + id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), product_name VARCHAR(255), + license_type VARCHAR(100), quantity INT, price VARCHAR(100), purchase_date VARCHAR(50), + expiry_date VARCHAR(50), vendor VARCHAR(100), remarks TEXT + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + await connection.query(` + CREATE TABLE IF NOT EXISTS sw_perm_assets ( + id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), product_name VARCHAR(255), + license_key VARCHAR(255), quantity INT, price VARCHAR(100), purchase_date VARCHAR(50), + vendor VARCHAR(100), remarks TEXT + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + await connection.query(` + CREATE TABLE IF NOT EXISTS sw_users ( + id INT AUTO_INCREMENT PRIMARY KEY, sw_id VARCHAR(50), corp VARCHAR(100), dept VARCHAR(100), + position VARCHAR(100), user_name VARCHAR(100), usage_period VARCHAR(255), doc_name VARCHAR(255) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + + console.log('✅ All ITAM tables ensured.'); } finally { connection.release(); } @@ -110,13 +147,8 @@ const mapHardware = (r, defaultType) => ({ app.get('/api/pc', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM pc_assets'); - console.log('🔍 DB Raw Rows (PC):', rows.length, 'items found.'); - if (rows.length > 0) console.log('🔍 First row sample:', rows[0]); res.json(rows.map(r => mapHardware(r, '개인PC'))); - } catch (err) { - console.error('❌ DB Query Error (PC):', err.message); - res.status(500).json({ error: err.message }); - } + } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/pc/batch', async (req, res) => { @@ -320,6 +352,31 @@ app.post('/api/sw-users/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); +// 자산코드 생성 API +app.get('/api/generate-asset-code', async (req, res) => { + const { prefix } = req.query; + if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); + + try { + const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets', 'sw_sub_assets', 'sw_perm_assets']; + let maxNum = 0; + + for (const table of tables) { + const [rows] = await pool.query(`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, [`${prefix}%`]); + rows.forEach(r => { + const numPart = r.asset_code.replace(prefix, ''); + const num = parseInt(numPart); + if (!isNaN(num) && num > maxNum) maxNum = num; + }); + } + + const nextCode = `${prefix}${(maxNum + 1).toString().padStart(3, '0')}`; + res.json({ nextCode }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // 초기화 및 서버 기동 ensureTables().then(() => { app.listen(PORT, () => { diff --git a/src/components/Modal/BaseModal.ts b/src/components/Modal/BaseModal.ts index 7e1c518..8bb9a81 100644 --- a/src/components/Modal/BaseModal.ts +++ b/src/components/Modal/BaseModal.ts @@ -1,26 +1,26 @@ /** * 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다. */ -export function initBaseModal() { - const closeAllModals = () => { - const modals = document.querySelectorAll('.modal-overlay'); - modals.forEach(modal => modal.classList.add('hidden')); - }; +export function closeModals() { + const modals = document.querySelectorAll('.modal-overlay'); + modals.forEach(modal => modal.classList.add('hidden')); +} +export function initBaseModal() { // ESC 키로 닫기 window.addEventListener('keydown', (e) => { - if (e.key === 'Escape') closeAllModals(); + if (e.key === 'Escape') closeModals(); }); - // 배경(Overlay) 클릭 시 닫기 (동적 생성된 모달 대응을 위해 이벤트 위임 고려 가능하나 일단 단순 구현) + // 배경(Overlay) 클릭 시 닫기 document.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (target.classList.contains('modal-overlay')) { - closeAllModals(); + closeModals(); } }); - return { closeAllModals }; + return { closeAllModals: closeModals }; } /** diff --git a/src/components/Modal/DashboardDetailModal.ts b/src/components/Modal/DashboardDetailModal.ts index 4ea6f96..b58ac83 100644 --- a/src/components/Modal/DashboardDetailModal.ts +++ b/src/components/Modal/DashboardDetailModal.ts @@ -98,7 +98,7 @@ export function openSwUsageDetail(title: string, list: SoftwareAsset[]) { thead.innerHTML = `No법인제품명수량사용중사용가능`; tbody.innerHTML = ''; list.forEach((sw, idx) => { - const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; + const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length; const tr = document.createElement('tr'); tr.innerHTML = `${idx+1}${sw.법인}${sw.제품명}${sw.수량}${assigned}${Number(sw.수량) - assigned}`; tbody.appendChild(tr); diff --git a/src/components/Modal/SWUserModal.ts b/src/components/Modal/SWUserModal.ts index 8131ed6..3d4b93b 100644 --- a/src/components/Modal/SWUserModal.ts +++ b/src/components/Modal/SWUserModal.ts @@ -6,7 +6,7 @@ import { CORP_LIST, ORG_LIST } from './SharedData'; import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils'; let currentSwUserAsset: SoftwareAsset | null = null; -let tempSwUsers: SWUser[] = []; +let tempSwUsers: any[] = []; const SW_USER_MODAL_HTML = `