import express from 'express'; import mysql from 'mysql2/promise'; import cors from 'cors'; import dotenv from 'dotenv'; import fs from 'fs'; dotenv.config(); const app = express(); app.use(cors()); app.use(express.json({ limit: '100mb' })); // Request Logger app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); }); 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'), charset: 'utf8mb4' }); const handleError = (res, err, context, isGet = false) => { console.error(`❌ [${context}] Error:`, err.message); if (isGet) res.json([]); else res.status(500).json({ error: err.message }); }; // --- API Implementation --- /** * Generic Fetcher for Asset Tables */ const fetchAssets = async (tableName, res, context) => { try { const [rows] = await pool.query(`SELECT * FROM ${tableName}`); console.log(`📡 [GET ${context}] Returning ${rows.length} rows from ${tableName}`); res.json(rows); } catch (err) { handleError(res, err, context, true); } }; /** * Generic Batch Saver for Asset Tables */ const saveAssetsBatch = async (tableName, items, res, context) => { const connection = await pool.getConnection(); try { await connection.beginTransaction(); // Get valid columns for this table const [cols] = await connection.query(`DESCRIBE ${tableName}`); const validColumns = cols.map(c => c.Field); // 1. Clear existing await connection.query(`DELETE FROM ${tableName}`); // 2. Insert new items for (const item of items) { const filteredRow = {}; validColumns.forEach(col => { if (col === 'created_at' || col === 'updated_at') return; if (item[col] !== undefined) filteredRow[col] = item[col]; }); if (!filteredRow.id) filteredRow.id = Math.random().toString(36).substring(2, 9); await connection.query(`INSERT INTO ${tableName} SET ?`, [filteredRow]); } await connection.commit(); res.json({ success: true, count: items.length }); } catch (err) { await connection.rollback(); handleError(res, err, context); } finally { connection.release(); } }; // --- Routes --- const routeMap = { '/api/users': { table: 'system_users', context: 'USERS' }, '/api/pc': { table: 'asset_pc', context: 'PC' }, '/api/server': { table: 'asset_server', context: 'SERVER' }, '/api/storage': { table: 'asset_storage', context: 'STORAGE' }, '/api/network': { table: 'asset_network', context: 'NETWORK' }, '/api/sw/internal': { table: 'asset_sw_internal', context: 'SW INTERNAL' }, '/api/sw/external': { table: 'asset_sw_external', context: 'SW EXTERNAL' }, '/api/survey': { table: 'asset_survey', context: 'SURVEY' }, '/api/pc-parts': { table: 'asset_pc_parts', context: 'PC PARTS' }, '/api/equipment': { table: 'asset_equipment', context: 'EQUIPMENT' }, '/api/office-supplies': { table: 'asset_office_supplies', context: 'OFFICE SUPPLIES' }, '/api/cloud': { table: 'asset_cloud', context: 'CLOUD' }, '/api/domain': { table: 'asset_domain', context: 'DOMAIN' }, '/api/cost': { table: 'asset_cost', context: 'COST' }, '/api/vip': { table: 'asset_vip', context: 'VIP' }, '/api/asset/software/assignment': { table: 'asset_software_assignment', context: 'SW ASSIGN' } }; Object.entries(routeMap).forEach(([route, { table, context }]) => { app.get(route, (req, res) => fetchAssets(table, res, context)); app.post(`${route}/batch`, (req, res) => saveAssetsBatch(table, req.body, res, `${context} BATCH`)); }); app.get('/api/asset/history', (req, res) => fetchAssets('asset_history', res, 'HISTORY')); app.post('/api/asset/history/batch', async (req, res) => { const connection = await pool.getConnection(); try { await connection.beginTransaction(); await connection.query('DELETE FROM asset_history'); for (const item of req.body) { const dbRow = { asset_id: item.assetId, log_date: item.date, log_user: item.user, details: item.details, cost: item.cost || 0 }; await connection.query('INSERT INTO asset_history SET ?', [dbRow]); } await connection.commit(); res.json({ success: true }); } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH HISTORY'); } finally { connection.release(); } }); app.get('/api/generate-asset-code', async (req, res) => { try { const { prefix } = req.query; if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); // asset_code 컬럼이 있는 것으로 확인된 테이블 목록 (DESCRIBE 결과 기반) const tables = [ 'asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip' ]; let lastCode = ''; let maxNum = 0; for (const table of tables) { try { const [rows] = await pool.query( `SELECT asset_code FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`, [`${prefix}%`] ); if (rows.length > 0) { const code = rows[0].asset_code; // 숫자 부분 추출 (예: SVR048 -> 48) const numMatch = code.match(/\d+/); if (numMatch) { const num = parseInt(numMatch[0]); if (num > maxNum) { maxNum = num; lastCode = code; } } } } catch (err) { console.warn(`[GENERATE CODE] Skipping ${table}: ${err.message}`); } } const nextNum = maxNum + 1; const nextCode = `${prefix}${String(nextNum).padStart(3, '0')}`; console.log(`🆕 [GENERATE CODE] Prefix: ${prefix}, Last: ${lastCode}, Next: ${nextCode}`); res.json({ nextCode }); } catch (err) { handleError(res, err, 'GENERATE CODE'); } }); // 6. Map Config API (Real-time Save) app.get('/api/maps', (req, res) => { try { if (!fs.existsSync('map_config.json')) { return res.json({}); } const data = fs.readFileSync('map_config.json', 'utf8'); res.json(JSON.parse(data || '{}')); } catch (err) { handleError(res, err, 'GET MAPS'); } }); app.post('/api/maps/save', (req, res) => { try { const { path, boxes } = req.body; if (!path) return res.status(400).json({ error: 'Path is required' }); let config = {}; if (fs.existsSync('map_config.json')) { config = JSON.parse(fs.readFileSync('map_config.json', 'utf8') || '{}'); } config[path] = boxes; fs.writeFileSync('map_config.json', JSON.stringify(config, null, 2)); console.log(`💾 [MAP SAVE] Updated config for: ${path}`); res.json({ success: true }); } catch (err) { handleError(res, err, 'SAVE MAPS'); } }); app.listen(3000, '0.0.0.0', () => { console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (Multi-Table Optimized)'); });