diff --git a/migrate_v6_parts_master.js b/migrate_v6_parts_master.js new file mode 100644 index 0000000..ab1fcb3 --- /dev/null +++ b/migrate_v6_parts_master.js @@ -0,0 +1,195 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config({ override: true }); + +const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env; + +// 기존의 감점 계산 로직을 그대로 이용해 등급과 감점점수를 도출하는 헬퍼 함수 +function parseCpu(cpu) { + if (!cpu) return { tier: '기타', deduction: 30 }; + const cpuUpper = cpu.toUpperCase().trim(); + if (cpuUpper === '-' || cpuUpper === '') return { tier: '기타', deduction: 30 }; + + let tier = '기타'; + let deduction = 30; + + if (cpuUpper.includes('I9') || cpuUpper.includes('RYZEN 9') || cpuUpper.includes('RYZEN9')) { + tier = 'i9 / Ryzen 9'; + deduction = 0; + } else if (cpuUpper.includes('I7') || cpuUpper.includes('RYZEN 7') || cpuUpper.includes('RYZEN7')) { + tier = 'i7 / Ryzen 7'; + deduction = 5; + } else if (cpuUpper.includes('I5') || cpuUpper.includes('RYZEN 5') || cpuUpper.includes('RYZEN5')) { + tier = 'i5 / Ryzen 5'; + deduction = 15; + } else if (cpuUpper.includes('I3') || cpuUpper.includes('RYZEN 3') || cpuUpper.includes('RYZEN3')) { + tier = 'i3 / Ryzen 3'; + deduction = 25; + } + + // CPU 세대 감점 계산 (최대 -15점) + let genDeduction = 0; + const intelMatch = cpuUpper.match(/I\d-?(\d+)/); + let gen = 0; + if (intelMatch && intelMatch[1]) { + const numStr = intelMatch[1]; + if (numStr.length === 5) gen = parseInt(numStr.substring(0, 2), 10); + else if (numStr.length === 4) gen = parseInt(numStr.substring(0, 1), 10); + } + + const amdMatch = cpuUpper.match(/RYZEN\s?\d\s?-?(\d+)/); + let amdGen = 0; + if (amdMatch && amdMatch[1] && !intelMatch) { + const numStr = amdMatch[1]; + if (numStr.length === 4) amdGen = parseInt(numStr.substring(0, 1), 10); + } + + if (intelMatch) { + if (gen >= 12) genDeduction = 0; + else if (gen >= 10) genDeduction = 5; + else if (gen >= 8) genDeduction = 10; + else genDeduction = 15; + } else if (amdMatch) { + if (amdGen >= 5) genDeduction = 0; + else if (amdGen >= 3) genDeduction = 5; + else genDeduction = 10; + } else { + genDeduction = 15; + } + + // 최종 등급 감점 + 세대 감점 합산 + return { tier, deduction: deduction + genDeduction }; +} + +function parseGpu(gpu) { + if (!gpu) return { tier: 'C', deduction: 25 }; + const gpuUpper = gpu.toUpperCase().trim(); + if (gpuUpper === '-' || gpuUpper === '') return { tier: 'C', deduction: 25 }; + + if ( + gpuUpper.includes('RTX 4090') || gpuUpper.includes('RTX 4080') || gpuUpper.includes('RTX 4070') || + gpuUpper.includes('RTX A5000') || gpuUpper.includes('RTX A6000') || gpuUpper.includes('RTX A4000') + ) { + return { tier: 'S', deduction: 0 }; + } else if ( + gpuUpper.includes('RTX 3070') || gpuUpper.includes('RTX 3060') || gpuUpper.includes('RTX 2060') || + gpuUpper.includes('RTX A2000') || gpuUpper.includes('RTX A3000') || gpuUpper.includes('QUADRO') + ) { + return { tier: 'A', deduction: 5 }; + } else if ( + gpuUpper.includes('GTX 1660') || gpuUpper.includes('GTX 1080') || gpuUpper.includes('GTX 1070') || + gpuUpper.includes('GTX 1060') || gpuUpper.includes('RX 6700') || gpuUpper.includes('RX 6600') + ) { + return { tier: 'B', deduction: 15 }; + } else { + return { tier: 'C', deduction: 25 }; + } +} + +function parseRam(ram) { + if (!ram) return { tier: '부족', deduction: 25 }; + const ramUpper = ram.toUpperCase().trim(); + if (ramUpper === '-' || ramUpper === '') return { tier: '부족', deduction: 25 }; + + const ramMatch = ramUpper.match(/(\d+)\s*GB/); + if (ramMatch && ramMatch[1]) { + const ramVal = parseInt(ramMatch[1], 10); + if (ramVal >= 32) return { tier: '최적', deduction: 0 }; + else if (ramVal >= 16) return { tier: '보통', deduction: 10 }; + else if (ramVal >= 8) return { tier: '주의', deduction: 20 }; + } + return { tier: '부족', deduction: 25 }; +} + +async function runMigration() { + console.log('🔄 DB 커넥션 연결 중...'); + const connection = await mysql.createConnection({ + host: DB_HOST, + user: DB_USER, + password: DB_PASS, + database: DB_NAME, + port: parseInt(DB_PORT || '3306') + }); + + try { + console.log('⚙️ 1. hardware_components_master 테이블 생성...'); + await connection.query('DROP TABLE IF EXISTS hardware_components_master'); + await connection.query(` + CREATE TABLE hardware_components_master ( + id INT AUTO_INCREMENT PRIMARY KEY, + category VARCHAR(50) NOT NULL COMMENT 'CPU, GPU, RAM 등', + component_name VARCHAR(255) NOT NULL UNIQUE COMMENT '부품 표준 명칭', + score_tier VARCHAR(50) COMMENT '성능 등급', + deduction INT DEFAULT 0 COMMENT '감점 점수', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + console.log('✅ 테이블 생성 완료.'); + + console.log('🔍 2. 기존 asset_spec 테이블에서 부품명 조회...'); + const [specRows] = await connection.query('SELECT DISTINCT cpu, ram, gpu FROM asset_spec'); + + const uniqueCpus = new Set(); + const uniqueGpus = new Set(); + const uniqueRams = new Set(); + + specRows.forEach(row => { + if (row.cpu && row.cpu.trim() !== '-' && row.cpu.trim() !== '') uniqueCpus.add(row.cpu.trim()); + if (row.gpu && row.gpu.trim() !== '-' && row.gpu.trim() !== '') uniqueGpus.add(row.gpu.trim()); + if (row.ram && row.ram.trim() !== '-' && row.ram.trim() !== '') uniqueRams.add(row.ram.trim()); + }); + + // 만약 데이터가 너무 비어있을 경우를 대비하여 기본 대표 부품 몇 개 추가 + if (uniqueCpus.size === 0) { + ['Intel Core i9-13900K', 'Intel Core i7-14700K', 'Intel Core i5-12400', 'AMD Ryzen 7 7800X3D', 'Intel Core i3-10100'].forEach(c => uniqueCpus.add(c)); + } + if (uniqueGpus.size === 0) { + ['NVIDIA GeForce RTX 4090', 'NVIDIA GeForce RTX 4070', 'NVIDIA GeForce RTX 3060', 'Intel Iris Xe Graphics', 'NVIDIA GeForce GTX 1660 Super'].forEach(g => uniqueGpus.add(g)); + } + if (uniqueRams.size === 0) { + ['8GB', '16GB', '32GB', '64GB'].forEach(r => uniqueRams.add(r)); + } + + console.log(` - 추출된 CPU 개수: ${uniqueCpus.size}`); + console.log(` - 추출된 GPU 개수: ${uniqueGpus.size}`); + console.log(` - 추출된 RAM 개수: ${uniqueRams.size}`); + + console.log('💾 3. 마스터 테이블에 부품 데이터 및 감점 정보 삽입...'); + + // CPU 삽입 + for (const cpu of uniqueCpus) { + const { tier, deduction } = parseCpu(cpu); + await connection.query( + 'INSERT IGNORE INTO hardware_components_master (category, component_name, score_tier, deduction) VALUES (?, ?, ?, ?)', + ['CPU', cpu, tier, deduction] + ); + } + + // GPU 삽입 + for (const gpu of uniqueGpus) { + const { tier, deduction } = parseGpu(gpu); + await connection.query( + 'INSERT IGNORE INTO hardware_components_master (category, component_name, score_tier, deduction) VALUES (?, ?, ?, ?)', + ['GPU', gpu, tier, deduction] + ); + } + + // RAM 삽입 + for (const ram of uniqueRams) { + const { tier, deduction } = parseRam(ram); + await connection.query( + 'INSERT IGNORE INTO hardware_components_master (category, component_name, score_tier, deduction) VALUES (?, ?, ?, ?)', + ['RAM', ram, tier, deduction] + ); + } + + console.log('✅ 마이그레이션이 성공적으로 완료되었습니다!'); + } catch (error) { + console.error('❌ 마이그레이션 오류 발생:', error); + } finally { + await connection.end(); + } +} + +runMigration(); diff --git a/server.js b/server.js index c19e733..9c8c4e1 100644 --- a/server.js +++ b/server.js @@ -107,7 +107,7 @@ app.get('/api/assets/master', async (req, res) => { const masterData = { pc: [], server: [], storage: [], network: [], equipment: [], officeSupplies: [], survey: [], vip: [], pcParts: [], - swInternal: [], swExternal: [], swUsers: [], users: [], logs: [] + swInternal: [], swExternal: [], swUsers: [], users: [], logs: [], partsMaster: [] }; const [rows] = await connection.query(` @@ -149,12 +149,14 @@ app.get('/api/assets/master', async (req, res) => { const [swUsers] = await connection.query('SELECT * FROM asset_software_assignment'); const [users] = await connection.query('SELECT * FROM system_users'); const [logs] = await connection.query('SELECT * FROM asset_history ORDER BY created_at DESC'); + const [partsMaster] = await connection.query('SELECT * FROM hardware_components_master ORDER BY category, component_name'); masterData.swInternal = swInternal; masterData.swExternal = swExternal; masterData.swUsers = swUsers; masterData.users = users; masterData.logs = logs; + masterData.partsMaster = partsMaster; connection.release(); res.json(masterData); @@ -362,19 +364,19 @@ app.post('/api/pc/flow', async (req, res) => { [userName, empNo, dept, position, assetId] ); await connection.query( - `UPDATE asset_spec SET hw_status = '사용중' WHERE asset_id = ?`, + `UPDATE asset_spec SET hw_status = '운영' WHERE asset_id = ?`, [assetId] ); } else if (action === 'return') { await connection.query( `UPDATE asset_core SET previous_user = user_current, previous_dept = current_dept, - user_current = '', emp_no = '', current_dept = '재고창고', user_position = '' + user_current = '', emp_no = '', user_position = '' WHERE id = ?`, [assetId] ); await connection.query( - `UPDATE asset_spec SET hw_status = '대기' WHERE asset_id = ?`, + `UPDATE asset_spec SET hw_status = '재고' WHERE asset_id = ?`, [assetId] ); } else if (action === 'move') { @@ -386,7 +388,7 @@ app.post('/api/pc/flow', async (req, res) => { [userName, empNo, dept, position, assetId] ); await connection.query( - `UPDATE asset_spec SET hw_status = '사용중' WHERE asset_id = ?`, + `UPDATE asset_spec SET hw_status = '운영' WHERE asset_id = ?`, [assetId] ); } else { @@ -483,6 +485,107 @@ app.get('/api/maps', (req, res) => { } catch (err) { handleError(res, err, 'GET MAPS'); } }); +// 6.5. Get Hardware Components Master List +app.get('/api/hardware-components', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM hardware_components_master ORDER BY category, component_name'); + res.json(rows); + } catch (err) { + handleError(res, err, 'GET HARDWARE COMPONENTS'); + } +}); + +// 6.6. Save Hardware Component (Add or Update) +app.post('/api/hardware-components/save', async (req, res) => { + const { id, category, component_name, score_tier, deduction } = req.body; + let connection; + try { + connection = await pool.getConnection(); + if (id) { + await connection.query( + 'UPDATE hardware_components_master SET category = ?, component_name = ?, score_tier = ?, deduction = ? WHERE id = ?', + [category, component_name, score_tier, deduction, id] + ); + } else { + await connection.query( + 'INSERT INTO hardware_components_master (category, component_name, score_tier, deduction) VALUES (?, ?, ?, ?)', + [category, component_name, score_tier, deduction] + ); + } + res.json({ success: true }); + } catch (err) { + handleError(res, err, 'SAVE HARDWARE COMPONENT'); + } finally { + if (connection) connection.release(); + } +}); + +// 6.7. Delete Hardware Component +app.delete('/api/hardware-components/:id', async (req, res) => { + const { id } = req.params; + let connection; + try { + connection = await pool.getConnection(); + await connection.query('DELETE FROM hardware_components_master WHERE id = ?', [id]); + res.json({ success: true }); + } catch (err) { + handleError(res, err, 'DELETE HARDWARE COMPONENT'); + } finally { + if (connection) connection.release(); + } +}); + +// 6.8. Get System Users List +app.get('/api/system-users', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM system_users ORDER BY user_name'); + res.json(rows); + } catch (err) { + handleError(res, err, 'GET SYSTEM USERS'); + } +}); + +// 6.9. Save System User (Add or Update) +app.post('/api/system-users/save', async (req, res) => { + const { id, emp_no, user_name, dept_name, position, status } = req.body; + let connection; + try { + connection = await pool.getConnection(); + if (id) { + await connection.query( + 'UPDATE system_users SET emp_no = ?, user_name = ?, dept_name = ?, position = ?, status = ? WHERE id = ?', + [emp_no, user_name, dept_name, position, status, id] + ); + } else { + const newId = 'USER-' + Math.random().toString(36).substring(2, 9).toUpperCase(); + await connection.query( + 'INSERT INTO system_users (id, emp_no, user_name, dept_name, position, status) VALUES (?, ?, ?, ?, ?, ?)', + [newId, emp_no, user_name, dept_name, position, status] + ); + } + res.json({ success: true }); + } catch (err) { + handleError(res, err, 'SAVE SYSTEM USER'); + } finally { + if (connection) connection.release(); + } +}); + +// 6.10. Delete System User +app.delete('/api/system-users/:id', async (req, res) => { + const { id } = req.params; + let connection; + try { + connection = await pool.getConnection(); + await connection.query('DELETE FROM system_users WHERE id = ?', [id]); + res.json({ success: true }); + } catch (err) { + handleError(res, err, 'DELETE SYSTEM USER'); + } finally { + if (connection) connection.release(); + } +}); + app.post('/api/maps/save', (req, res) => { try { const { path, boxes } = req.body; diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 60e8016..5197aa6 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,5 +1,6 @@ import { state, saveAsset, deleteAsset } from '../../core/state'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { calculatePcScoreDeductive, getPcGrade } from '../../core/utils'; import { generateOptionsHTML, setFieldValue, @@ -13,6 +14,7 @@ import { BaseModal } from './BaseModal'; class HwAssetModal extends BaseModal { private dynamicMapConfig: Record = {}; + private masterComponents: any[] = []; constructor() { super('hw', '자산 상세 정보'); @@ -24,6 +26,39 @@ class HwAssetModal extends BaseModal { const btnStyle = `padding: 0 16px; display: inline-flex; align-items: center; justify-content: center; font-weight: 600; white-space: nowrap; cursor: pointer; ${sharedStyle}`; return ` +