- 서버PC 자산을 asset_pc 테이블로 통합 마이그레이션 및 스키마 확장 (위치, IP 정보 복구 완료) - 하드웨어 자산 페이지의 구매법인 필터를 자산위치 필터로 교체 및 동적 데이터 바인딩 적용 - 모든 자산 리스트 페이지 상단에 설명(Description) 필드 추가 및 헤더 표준화 - 상세 모달 내 삭제 버튼 기능 구현 및 서버PC 용도 필드 노출 오류 수정 - 현 사용조직 필터 리스트가 비어있던 DOM 셀렉터 버그 수정
195 lines
7.9 KiB
JavaScript
195 lines
7.9 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 ---
|
|
|
|
// 0. User Management
|
|
app.get('/api/users', (req, res) => fetchAssets('system_users', res, 'USERS'));
|
|
app.post('/api/users/batch', (req, res) => saveAssetsBatch('system_users', req.body, res, 'USERS BATCH'));
|
|
|
|
// 1. Hardware Assets
|
|
app.get('/api/pc', (req, res) => fetchAssets('asset_pc', res, 'PC'));
|
|
app.post('/api/pc/batch', (req, res) => saveAssetsBatch('asset_pc', req.body, res, 'PC BATCH'));
|
|
|
|
app.get('/api/server', (req, res) => fetchAssets('asset_server', res, 'SERVER'));
|
|
app.post('/api/server/batch', (req, res) => saveAssetsBatch('asset_server', req.body, res, 'SERVER BATCH'));
|
|
|
|
app.get('/api/storage', (req, res) => fetchAssets('asset_storage', res, 'STORAGE'));
|
|
app.post('/api/storage/batch', (req, res) => saveAssetsBatch('asset_storage', req.body, res, 'STORAGE BATCH'));
|
|
|
|
app.get('/api/network', (req, res) => fetchAssets('asset_network', res, 'NETWORK'));
|
|
app.post('/api/network/batch', (req, res) => saveAssetsBatch('asset_network', req.body, res, 'NETWORK BATCH'));
|
|
|
|
// 2. Software Assets
|
|
app.get('/api/sw/internal', (req, res) => fetchAssets('asset_sw_internal', res, 'SW INTERNAL'));
|
|
app.post('/api/sw/internal/batch', (req, res) => saveAssetsBatch('asset_sw_internal', req.body, res, 'SW INTERNAL BATCH'));
|
|
|
|
app.get('/api/sw/external', (req, res) => fetchAssets('asset_sw_external', res, 'SW EXTERNAL'));
|
|
app.post('/api/sw/external/batch', (req, res) => saveAssetsBatch('asset_sw_external', req.body, res, 'SW EXTERNAL BATCH'));
|
|
|
|
// 3. Other Assets
|
|
app.get('/api/survey', (req, res) => fetchAssets('asset_survey', res, 'SURVEY'));
|
|
app.post('/api/survey/batch', (req, res) => saveAssetsBatch('asset_survey', req.body, res, 'SURVEY BATCH'));
|
|
|
|
app.get('/api/pc-parts', (req, res) => fetchAssets('asset_pc_parts', res, 'PC PARTS'));
|
|
app.post('/api/pc-parts/batch', (req, res) => saveAssetsBatch('asset_pc_parts', req.body, res, 'PC PARTS BATCH'));
|
|
|
|
app.get('/api/equipment', (req, res) => fetchAssets('asset_equipment', res, 'EQUIPMENT'));
|
|
app.post('/api/equipment/batch', (req, res) => saveAssetsBatch('asset_equipment', req.body, res, 'EQUIPMENT BATCH'));
|
|
|
|
app.get('/api/office-supplies', (req, res) => fetchAssets('asset_office_supplies', res, 'OFFICE SUPPLIES'));
|
|
app.post('/api/office-supplies/batch', (req, res) => saveAssetsBatch('asset_office_supplies', req.body, res, 'OFFICE SUPPLIES BATCH'));
|
|
|
|
app.get('/api/cloud', (req, res) => fetchAssets('asset_cloud', res, 'CLOUD'));
|
|
app.post('/api/cloud/batch', (req, res) => saveAssetsBatch('asset_cloud', req.body, res, 'CLOUD BATCH'));
|
|
|
|
app.get('/api/domain', (req, res) => fetchAssets('asset_domain', res, 'DOMAIN'));
|
|
app.post('/api/domain/batch', (req, res) => saveAssetsBatch('asset_domain', req.body, res, 'DOMAIN BATCH'));
|
|
|
|
app.get('/api/cost', (req, res) => fetchAssets('asset_cost', res, 'COST'));
|
|
app.post('/api/cost/batch', (req, res) => saveAssetsBatch('asset_cost', req.body, res, 'COST BATCH'));
|
|
|
|
app.get('/api/vip', (req, res) => fetchAssets('asset_vip', res, 'VIP'));
|
|
app.post('/api/vip/batch', (req, res) => saveAssetsBatch('asset_vip', req.body, res, 'VIP 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(); }
|
|
});
|
|
|
|
app.get('/api/asset/software/assignment', (req, res) => fetchAssets('asset_software_assignment', res, 'SW ASSIGN'));
|
|
app.post('/api/asset/software/assignment/batch', (req, res) => saveAssetsBatch('asset_software_assignment', req.body, res, 'SW ASSIGN BATCH'));
|
|
|
|
// 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)');
|
|
});
|