From 4b765aba2e0c08739397c4c9533a443d52e48422 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Wed, 22 Apr 2026 10:11:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=90=EC=82=B0=20=EC=9C=A0=ED=98=95?= =?UTF-8?q?=EB=B3=84=20UI=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=90=EC=82=B0=EB=B2=88=ED=98=B8=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=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) {