Files
ITAM-test/server.js
Taehoon 3b9b2ea598 feat: 자산번호 4자리 일련번호 확장 및 날짜 기반 생성 로직 추가
- 백엔드(server.js): 자산번호 자동 생성 시 구매일자(YYYYMM)를 파싱하여 [접두사]-[YYYYMM]-[0000] 형태로 4자리 일련번호를 부여하도록 로직 전면 수정
- 프런트엔드(HWModal.ts): 자산번호 생성 API 호출 시 사용자가 입력한 구매일자 데이터를 파라미터로 함께 전송하도록 연동
- 전체 DB 및 로컬 마스터 데이터의 4자리 일련번호 및 날짜 복구 마이그레이션 반영
2026-06-08 15:18:39 +09:00

267 lines
8.3 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: '50mb' }));
// MySQL Pool Configuration
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'),
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Error Handler
const handleError = (res, err, label) => {
console.error(`❌ [${label}] Error:`, err);
res.status(500).json({ error: err.message });
};
// --- API Endpoints ---
// 1. Generic Batch Save (Dynamic Table Detection)
app.post('/api/:table/batch', async (req, res) => {
const { table } = req.params;
const data = req.body;
if (!Array.isArray(data)) return res.status(400).json({ error: 'Data must be an array' });
let connection;
try {
connection = await pool.getConnection();
await connection.beginTransaction();
// 1. Get Table Schema
const [columns] = await connection.query(`DESCRIBE ${table}`);
const validFields = columns.map(c => c.Field);
// 2. Clear Existing Data (Optional - depending on strategy)
// For now, we use REPLACE INTO or similar. But user requested batch save as sync.
await connection.query(`DELETE FROM ${table}`);
// 3. Insert New Data
if (data.length > 0) {
const placeholders = validFields.map(() => '?').join(', ');
const sql = `INSERT INTO ${table} (${validFields.join(', ')}) VALUES (${placeholders})`;
for (const item of data) {
const values = validFields.map(field => {
const val = item[field];
return val === undefined ? null : val;
});
await connection.query(sql, values);
}
}
await connection.commit();
console.log(`✅ [BATCH SAVE] Table: ${table}, Count: ${data.length}`);
res.json({ success: true, count: data.length });
} catch (err) {
if (connection) await connection.rollback();
handleError(res, err, 'BATCH SAVE');
} finally {
if (connection) connection.release();
}
});
// 2. Get All Assets (Integrated Master Data)
app.get('/api/assets/master', async (req, res) => {
try {
const connection = await pool.getConnection();
const tables = {
pc: 'asset_pc',
server: 'asset_server',
storage: 'asset_storage',
network: 'asset_network',
equipment: 'asset_equipment',
officeSupplies: 'asset_office_supplies',
survey: 'asset_survey',
vip: 'asset_vip',
swInternal: 'sw_internal',
swExternal: 'sw_external',
cloud: 'asset_cloud',
users: 'user_master',
swUsers: 'sw_assignment',
logs: 'asset_history'
};
const masterData = {};
for (const [key, tableName] of Object.entries(tables)) {
try {
const [rows] = await connection.query(`SELECT * FROM ${tableName}`);
masterData[key] = rows;
} catch (err) {
console.warn(`[MASTER DATA] Skipping ${tableName}: ${err.message}`);
masterData[key] = [];
}
}
connection.release();
res.json(masterData);
} catch (err) {
handleError(res, err, 'MASTER DATA');
}
});
// 3. Single Asset Save (Update or Insert)
app.post('/api/asset/:category/save', async (req, res) => {
const { category } = req.params;
const asset = req.body;
const tableMap = {
pc: 'asset_pc',
server: 'asset_server',
storage: 'asset_storage',
network: 'asset_network',
equipment: 'asset_equipment',
officeSupplies: 'asset_office_supplies',
survey: 'asset_survey',
vip: 'asset_vip',
swInternal: 'sw_internal',
swExternal: 'sw_external',
cloud: 'asset_cloud'
};
const table = tableMap[category];
if (!table) return res.status(400).json({ error: 'Invalid category' });
try {
const connection = await pool.getConnection();
const [columns] = await connection.query(`DESCRIBE ${table}`);
const validFields = columns.map(c => c.Field);
const dataObj = {};
validFields.forEach(f => { if (asset[f] !== undefined) dataObj[f] = asset[f]; });
const keys = Object.keys(dataObj);
const values = Object.values(dataObj);
const placeholders = keys.map(() => '?').join(', ');
const updates = keys.map(k => `${k} = VALUES(${k})`).join(', ');
const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`;
await connection.query(sql, values);
connection.release();
console.log(`💾 [ASSET SAVE] Category: ${category}, ID: ${asset.id}`);
res.json({ success: true });
} catch (err) {
handleError(res, err, 'ASSET SAVE');
}
});
// 4. Asset Delete
app.delete('/api/asset/:category/:id', async (req, res) => {
const { category, id } = req.params;
const tableMap = {
pc: 'asset_pc', server: 'asset_server', storage: 'asset_storage', network: 'asset_network',
equipment: 'asset_equipment', officeSupplies: 'asset_office_supplies', survey: 'asset_survey',
vip: 'asset_vip', swInternal: 'sw_internal', swExternal: 'sw_external', cloud: 'asset_cloud'
};
const table = tableMap[category];
if (!table) return res.status(400).json({ error: 'Invalid category' });
try {
const connection = await pool.getConnection();
await connection.query(`DELETE FROM ${table} WHERE id = ?`, [id]);
connection.release();
console.log(`🗑️ [ASSET DELETE] Category: ${category}, ID: ${id}`);
res.json({ success: true });
} catch (err) {
handleError(res, err, 'ASSET DELETE');
}
});
// 5. Generate Next Asset Code
app.get('/api/generate-asset-code', async (req, res) => {
const { prefix, purchaseDate } = req.query;
if (!prefix) return res.status(400).json({ error: 'Prefix is required' });
try {
const connection = await pool.getConnection();
const tables = [
'asset_pc', 'asset_server', 'asset_storage', 'asset_network',
'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip'
];
// Extract YYYYMM from purchaseDate (format: YYYY-MM-DD)
const datePart = purchaseDate ? purchaseDate.toString().replace(/-/g, '').substring(0, 6) : '';
const searchPattern = datePart ? `${prefix}-${datePart}-%` : `${prefix}-%`;
let maxNum = 0;
for (const table of tables) {
try {
const [rows] = await connection.query(
`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`,
[searchPattern]
);
rows.forEach(row => {
const parts = row.asset_code.split('-');
const seqPart = parts[parts.length - 1]; // Last part is sequence
const num = parseInt(seqPart);
if (!isNaN(num) && num > maxNum) maxNum = num;
});
} catch (err) {
// Table might not exist or column missing
}
}
const nextNum = maxNum + 1;
const nextCode = datePart
? `${prefix}-${datePart}-${String(nextNum).padStart(4, '0')}`
: `${prefix}-${String(nextNum).padStart(4, '0')}`; // Fallback if no date
connection.release();
console.log(`🆕 [GENERATE CODE] Prefix: ${prefix}, Date: ${datePart}, 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)');
});