- Enhanced backend asset code generation logic to handle multiple tables - Integrated asset code generation button in HWModal - Included utility scripts for asset code migration and DB synchronization - Resolved issues with missing purchase dates and duplicate asset codes
216 lines
7.0 KiB
JavaScript
216 lines
7.0 KiB
JavaScript
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)');
|
|
});
|