From 4b765aba2e0c08739397c4c9533a443d52e48422 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 22 Apr 2026 10:11:45 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=9E=90=EC=82=B0=20=EC=9C=A0?= =?UTF-8?q?=ED=98=95=EB=B3=84=20UI=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=90=EC=82=B0=EB=B2=88=ED=98=B8=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CPU/GPU/RAM/HDD 등 부품 유형별 필드 라벨 동적 변경 로직 추가\n- 유형별 불필요한 사양 필드 숨김 처리 및 UI 레이아웃 정교화\n- 서버측 자산번호 생성 API (/api/generate-asset-code) 구현\n- 모달 내 자산번호 자동 생성 버튼 이벤트 연동 및 백엔드 동기화 --- server.js | 28 ++++++++++ src/components/Modal/HWModal.ts | 94 +++++++++++++++++++++++++++++---- 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/server.js b/server.js index 22cd962..4772a4c 100644 --- a/server.js +++ b/server.js @@ -323,6 +323,34 @@ app.post('/api/sw-users/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); +// 자산번호 자동 생성 API +app.get('/api/generate-asset-code', async (req, res) => { + const { prefix } = req.query; + if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); + + try { + const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']; + let maxNum = 0; + + for (const table of tables) { + const [rows] = await pool.query( + `SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, + [`${prefix}%`] + ); + rows.forEach(r => { + const numPart = r.asset_code.replace(prefix, ''); + const num = parseInt(numPart); + if (!isNaN(num) && num > maxNum) maxNum = num; + }); + } + + const nextNum = (maxNum + 1).toString().padStart(3, '0'); + res.json({ nextCode: `${prefix}${nextNum}` }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // 초기화 및 서버 기동 ensureTables().then(() => { app.listen(PORT, () => { diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index a6029fc..4515f08 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -249,7 +249,15 @@ function applyTypeSpecificUI(type: string) { detailPurpose: document.getElementById('hw-상세용도-group'), networkTitle: document.getElementById('hw-network-title'), specTitle: document.getElementById('hw-spec-title'), - opTitle: document.getElementById('hw-op-title') + opTitle: document.getElementById('hw-op-title'), + model: document.getElementById('hw-model-group'), + os: document.getElementById('hw-os-group'), + cpu: document.getElementById('hw-cpu-group'), + ram: document.getElementById('hw-ram-group'), + ssd1: document.getElementById('hw-ssd1-group'), + ssd2: document.getElementById('hw-ssd2-group'), + hwSpec: document.getElementById('hw-hwspec-group'), + monitoring: document.getElementById('hw-monitoring-group') }; const serverOnly = document.querySelectorAll('.server-only'); @@ -257,42 +265,83 @@ function applyTypeSpecificUI(type: string) { const opOnly = document.querySelectorAll('.op-only'); const standardLoc = document.querySelectorAll('.loc-standard'); - // 1. 초기화 + // 1. 초기화 (모두 숨김 및 라벨 원복) serverOnly.forEach(el => (el as HTMLElement).style.display = 'none'); nonServer.forEach(el => (el as HTMLElement).style.display = 'none'); opOnly.forEach(el => (el as HTMLElement).style.display = 'none'); standardLoc.forEach(el => (el as HTMLElement).style.display = 'flex'); Object.values(groups).forEach(g => { if (g) g.style.display = 'none'; }); + const osLabel = document.querySelector('label[for="hw-OS"]') as HTMLElement; + const ramLabel = document.querySelector('label[for="hw-RAM"]') as HTMLElement; + const modelLabel = document.querySelector('label[for="hw-모델명"]') as HTMLElement; + if (osLabel) osLabel.innerText = '운영체제 (OS)'; + if (ramLabel) ramLabel.innerText = 'RAM 용량'; + if (modelLabel) modelLabel.innerText = '모델명'; + // 2. 분류 판별 - const isMobileGroup = ['모바일', '태블릿', '노트북', '휴대폰'].some(t => upperType.includes(t)); + const isMobileGroup = ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t)); const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품'); const isOpType = isMobileGroup || isEquipGroup; const isPcType = upperType === 'PC' || upperType === '개인PC' || upperType === '노트북'; // 3. 레이아웃 적용 + if (groups.opTitle) groups.opTitle.style.display = 'flex'; + if (isOpType) { opOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); standardLoc.forEach(el => (el as HTMLElement).style.display = 'none'); + if (groups.specTitle) groups.specTitle.style.display = 'flex'; - } + if (groups.model) groups.model.style.display = 'flex'; - if (isPcType) { + // 특정 부품 유형에 따른 라벨 및 필드 제어 + const isCpuGpu = ['CPU', 'GPU'].some(t => upperType.includes(t)); + const isRamHdd = ['RAM', 'HDD'].some(t => upperType.includes(t)); + + if (isCpuGpu) { + if (groups.os && osLabel) { + osLabel.innerText = '출시연월'; + groups.os.style.display = 'flex'; + } + } else if (isRamHdd) { + if (groups.ram && ramLabel) { + ramLabel.innerText = '용량'; + groups.ram.style.display = 'flex'; + } + // HDD인 경우 모델명 라벨을 S/N으로 변경 + if (upperType.includes('HDD') && modelLabel) { + modelLabel.innerText = 'S/N'; + } + } else { + if (groups.hwSpec) groups.hwSpec.style.display = 'flex'; + } + } + else if (isPcType) { if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex'; + if (groups.specTitle) groups.specTitle.style.display = 'flex'; + if (detailPurpose === '서버') { serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); + if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; + ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { + if (groups[k]) groups[k]!.style.display = 'flex'; + }); } else { nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); + ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { + if (groups[k]) groups[k]!.style.display = 'flex'; + }); } - if (groups.specTitle) groups.specTitle.style.display = 'flex'; - if (groups.networkTitle) groups.networkTitle.style.display = detailPurpose === '서버' ? 'flex' : 'none'; - } else if (upperType.includes('서버') || ['스토리지', 'NAS', 'DAS'].includes(upperType)) { + } + else if (upperType.includes('서버') || ['스토리지', 'NAS', 'DAS'].includes(upperType)) { serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex'); if (groups.networkTitle) groups.networkTitle.style.display = 'flex'; if (groups.specTitle) groups.specTitle.style.display = 'flex'; + ['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { + if (groups[k]) groups[k]!.style.display = 'flex'; + }); } - - if (groups.opTitle) groups.opTitle.style.display = 'flex'; } export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') { @@ -377,6 +426,31 @@ export function initHwModal(onSave: () => void, closeModals: () => void) { if (currentAsset) fillHwFormData(currentAsset); }); + document.getElementById('btn-generate-hw-code')?.addEventListener('click', async () => { + const typeValue = typeSelect.value; + const purchaseDate = getFieldValue('hw-구매일'); + const typeCode = TYPE_PREFIX_MAP[typeValue] || 'ETC'; + + // 구매일에서 연월(YYMM) 추출 (예: 2026-04-21 -> 2604) + const dateStr = purchaseDate.replace(/[^0-9]/g, ''); + if (dateStr.length < 4) { + alert('올바른 구매일(연월)을 입력해주세요. (예: 2026-04-21)'); + return; + } + const prefix = `${typeCode}-${dateStr.substring(2, 6)}-`; + + try { + const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`); + const data = await res.json(); + if (data.nextCode) { + setFieldValue('hw-자산코드', data.nextCode); + } + } catch (err) { + console.error('❌ 자산번호 생성 실패:', err); + alert('자산번호 생성에 실패했습니다.'); + } + }); + saveBtn.addEventListener('click', () => { if (!currentAsset) return; if (!isEditMode) { From d52c2c42004e6efcec893bbcc52a666cdcd9ccd0 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 22 Apr 2026 11:24:15 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EA=B5=AC=EB=A7=A4=EC=97=B0?= =?UTF-8?q?=EC=9B=94=20=ED=91=9C=EC=A4=80=ED=99=94=20=EB=B0=8F=20=EC=9E=90?= =?UTF-8?q?=EC=82=B0=EB=B2=88=ED=98=B8=20YYYYMM=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/HWModal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 9ee4e25..29049d2 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -165,8 +165,8 @@ const HW_MODAL_HTML = `
- - + +
From e1cdcfd93a201b8ffeebcb538473ed2dc88e04fa Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 22 Apr 2026 17:15:58 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=ED=95=98=EB=93=9C=EC=9B=A8?= =?UTF-8?q?=EC=96=B4=20=EC=9E=90=EB=8F=99=20=EB=B3=80=EA=B2=BD=20=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=9E=90=EC=82=B0?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db_fix_data.js | 49 ++ db_init.js | 8 +- server.js | 46 +- src/components/Modal/DashboardDetailModal.ts | 2 +- src/components/Modal/HWModal.ts | 765 +++++++------------ src/components/Modal/ModalUtils.ts | 90 ++- src/components/Modal/PCModal.ts | 362 --------- src/components/Modal/SWModal.ts | 475 ++++-------- src/components/Modal/SharedData.ts | 3 +- src/core/excelHandler.ts | 46 +- src/main.ts | 12 +- src/styles/modal.css | 36 + src/views/Dashboard/HwDashboard.ts | 2 +- src/views/List/EquipmentListView.ts | 5 +- src/views/List/MobileListView.ts | 12 +- src/views/List/PcListView.ts | 8 +- src/views/List/ServerListView.ts | 7 +- src/views/List/SwListView.ts | 2 +- 18 files changed, 730 insertions(+), 1200 deletions(-) create mode 100644 db_fix_data.js delete mode 100644 src/components/Modal/PCModal.ts diff --git a/db_fix_data.js b/db_fix_data.js new file mode 100644 index 0000000..92fb2b4 --- /dev/null +++ b/db_fix_data.js @@ -0,0 +1,49 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env; + +async function migrateData() { + const connection = await mysql.createConnection({ + host: DB_HOST, + user: DB_USER, + password: DB_PASS, + database: DB_NAME, + port: parseInt(DB_PORT || '3306') + }); + + console.log('🔄 기존 데이터 보정 시작 (상세유형 = 유형)...'); + + const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']; + + for (const table of tables) { + // 1. 유형(type)이 비어있는 경우 기본값 채우기 (보정 전 단계) + let defaultType = '기타'; + if (table === 'server_assets') defaultType = '서버'; + else if (table === 'pc_assets') defaultType = '개인PC'; + else if (table === 'storage_assets') defaultType = '스토리지'; + else if (table === 'equip_assets') defaultType = '전산비품'; + else if (table === 'mobile_assets') defaultType = '모바일기기'; + + await connection.query(`UPDATE ${table} SET type = ? WHERE type IS NULL OR type = ''`, [defaultType]); + + // 2. 개인PC가 아닌 데이터들에 대해 상세유형 = 유형 업데이트 + const [result] = await connection.query(` + UPDATE ${table} + SET detail_purpose = type + WHERE type NOT IN ('개인PC', 'PC') + `); + + console.log(`✅ ${table}: ${result.affectedRows}개 데이터 보정 완료`); + } + + console.log('✨ 모든 기존 데이터 보정이 완료되었습니다.'); + await connection.end(); +} + +migrateData().catch(err => { + console.error('❌ 데이터 보정 실패:', err); + process.exit(1); +}); diff --git a/db_init.js b/db_init.js index 0aa41e8..bc9daa1 100644 --- a/db_init.js +++ b/db_init.js @@ -32,7 +32,7 @@ async function initDB() { id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100) COMMENT '구매법인', asset_code VARCHAR(100) COMMENT '자산번호', - purchase_date VARCHAR(50) COMMENT '구매일자', + purchase_date VARCHAR(50) COMMENT '구매연월', type VARCHAR(50) COMMENT '유형', detail_purpose VARCHAR(50) COMMENT '상세용도', purpose VARCHAR(255) COMMENT '용도', @@ -57,6 +57,8 @@ async function initDB() { monitoring VARCHAR(100), price VARCHAR(100) COMMENT '금액', remarks TEXT, + storage_location VARCHAR(255) COMMENT '보관위치', + status VARCHAR(50) COMMENT '현재상태', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='${comment}'; `; @@ -77,7 +79,7 @@ async function initDB() { license_type VARCHAR(100) COMMENT '라이선스 유형', quantity INT COMMENT '수량', price VARCHAR(100) COMMENT '금액', - purchase_date VARCHAR(50) COMMENT '구매일', + purchase_date VARCHAR(50) COMMENT '구매연월', expiry_date VARCHAR(50) COMMENT '만료일', vendor VARCHAR(255) COMMENT '납품업체', remarks TEXT COMMENT '비고', @@ -95,7 +97,7 @@ async function initDB() { license_key VARCHAR(255) COMMENT '라이선스 키', quantity INT COMMENT '수량', price VARCHAR(100) COMMENT '금액', - purchase_date VARCHAR(50) COMMENT '구매일', + purchase_date VARCHAR(50) COMMENT '구매연월', vendor VARCHAR(255) COMMENT '납품업체', remarks TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP diff --git a/server.js b/server.js index 4772a4c..c5d0b95 100644 --- a/server.js +++ b/server.js @@ -90,22 +90,48 @@ const hardwareInsertSQL = (table) => ` `; const getHardwareValues = (a) => [ - a.id, a.법인||'', a.자산코드||'', a.구매일||'', a.type||'', a.상세용도||'', a.용도||'', a.상세||'', + a.id, a.법인||'', a.자산코드||'', a.구매연월||'', a.type||'', a.상세용도||'', a.용도||'', a.상세||'', a.현사용조직||'', a.이전사용조직||'', a.위치||'', a.담당자_정||'', a.담당자_부||'', a.IP주소||'', a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'', a.SSD1||'', a.SSD2||'', a.HDD1||'', a.모니터링||'', a.금액||'', a.비고||'', a.보관위치||'', a.현재상태||'' ]; -const mapHardware = (r, defaultType) => ({ - id: r.id, 법인: r.corp, 자산코드: r.asset_code, 구매일: r.purchase_date, type: r.type || defaultType, - 상세용도: r.detail_purpose, 용도: r.purpose, 상세: r.details, 현사용조직: r.current_org, - 이전사용조직: r.prev_org, 위치: r.location, 담당자_정: r.manager_main, 담당자_부: r.manager_sub, - IP주소: r.ip_address, 원격접속: r.remote_tool, 서버ID: r.server_id, 서버PW: r.server_pw, - 모델명: r.model_name, OS: r.os, CPU: r.cpu, RAM: r.ram, GPU: r.gpu, SSD1: r.storage1, - SSD2: r.storage2, HDD1: r.storage3, 모니터링: r.monitoring, 금액: r.price, 비고: r.remarks, - 보관위치: r.storage_location, 현재상태: r.status -}); +const mapHardware = (r, defaultType) => { + const type = r.type || defaultType; + return { + id: r.id, + 법인: r.corp, + 자산코드: r.asset_code, + 구매연월: r.purchase_date, + type: type, + 상세용도: (type !== '개인PC' && !r.detail_purpose) ? type : r.detail_purpose, + 용도: r.purpose, + 상세: r.details, + 현사용조직: r.current_org, + 이전사용조직: r.prev_org, + 위치: r.location, + 담당자_정: r.manager_main, + 담당자_부: r.manager_sub, + IP주소: r.ip_address, + 원격접속: r.remote_tool, + 서버ID: r.server_id, + 서버PW: r.server_pw, + 모델명: r.model_name, + OS: r.os, + CPU: r.cpu, + RAM: r.ram, + GPU: r.gpu, + SSD1: r.storage1, + SSD2: r.storage2, + HDD1: r.storage3, + 모니터링: r.monitoring, + 금액: r.price, + 비고: r.remarks, + 보관위치: r.storage_location, + 현재상태: r.status + }; +}; // --- API 라우트 정의 --- diff --git a/src/components/Modal/DashboardDetailModal.ts b/src/components/Modal/DashboardDetailModal.ts index 4ea6f96..7c5fe94 100644 --- a/src/components/Modal/DashboardDetailModal.ts +++ b/src/components/Modal/DashboardDetailModal.ts @@ -49,7 +49,7 @@ export function openDashboardDetail(title: string, list: HardwareAsset[]) { if (!thead) return; titleEl.textContent = title; - thead.innerHTML = `No유형자산코드명칭/모델위치담당/사용자구매일금액`; + thead.innerHTML = `No유형자산코드명칭/모델위치담당/사용자구매연월금액`; tbody.innerHTML = ''; if (list.length === 0) { tbody.innerHTML = `해당 조건의 자산이 없습니다.`; diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index dcc94f2..4247dd0 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,7 +1,7 @@ import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state'; -import { HardwareAsset, MasterAssetData, HardwareLog } from '../../core/excelHandler'; -import { openModal, closeModals } from './BaseModal'; -import { createIcons, Paperclip, History, Plus, X, Save, Edit2, RotateCcw } from 'lucide'; +import { HardwareAsset, HardwareLog } from '../../core/excelHandler'; +import { closeModals } from './BaseModal'; +import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Paperclip } from 'lucide'; import { CORP_LIST, ORG_LIST, HW_TYPE_LIST, LOCATION_DATA, TYPE_PREFIX_MAP } from './SharedData'; import { generateOptionsHTML, @@ -10,7 +10,10 @@ import { parseAndSetLocation, bindLocationEvents, getCombinedLocation, - setEditLock + setEditLock, + createModalFrameHTML, + autoFillForm, + autoExtractForm } from './ModalUtils'; let currentAsset: HardwareAsset | null = null; @@ -18,350 +21,119 @@ let isEditMode = false; const STATUS_LIST = ['대여중', '보관중', '수리중', '기타']; -const HW_MODAL_HTML = ` -