Files
ITAM-test/server.js
Taehoon 2c67037fc4 refactor: cleanup temp files, centralize API URL, and dynamic routing
- 불필요한 마이그레이션 스크립트, JSON 덤프, 백업 폴더 일괄 삭제

- 프론트엔드 API_BASE_URL 상수 도입 및 하드코딩된 API 엔드포인트 통합

- 백엔드(server.js) GET/POST 라우팅 구조를 Map 기반 동적 라우팅으로 리팩토링

- 미사용 dummyDataGenerator 제거
2026-05-26 19:37:34 +09:00

168 lines
6.0 KiB
JavaScript

import express from 'express';
import mysql from 'mysql2/promise';
import cors from 'cors';
import dotenv from 'dotenv';
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 (or we could use UPSERT logic, but existing code used DELETE-INSERT pattern)
await connection.query(`DELETE FROM ${tableName}`);
// 2. Insert new items
for (const item of items) {
const filteredRow = {};
validColumns.forEach(col => {
// Exclude auto-managed timestamps from manual insertion
if (col === 'created_at' || col === 'updated_at') return;
if (item[col] !== undefined) filteredRow[col] = item[col];
});
// Auto-generate ID if missing
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' }
};
// 동적 라우팅 생성 (Dynamic Routing)
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`));
});
// 4. Legacy/Auxiliary (History & Assignment)
app.get('/api/asset/history', (req, res) => fetchAssets('asset_history', res, 'HISTORY'));
app.post('/api/asset/history/batch', async (req, res) => {
// Custom logic for history as it might not follow the random-id pattern
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(); }
});
// 5. Utility
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' });
// Search in multiple tables if necessary, but typically prefix-based tables are known
const tables = ['asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_survey', 'asset_pc_parts', 'asset_equipment', 'asset_office_supplies', 'asset_vip'];
let lastCode = '';
for (const table of tables) {
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 && rows[0].asset_code > lastCode) {
lastCode = rows[0].asset_code;
}
}
let nextNum = 1;
if (lastCode) {
const lastNum = parseInt(lastCode.split('-').pop() || '0');
nextNum = lastNum + 1;
}
res.json({ nextCode: `${prefix}${String(nextNum).padStart(3, '0')}` });
} catch (err) { handleError(res, err, 'GENERATE CODE'); }
});
app.listen(3000, '0.0.0.0', () => {
console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (Multi-Table Optimized)');
});