Merge all feature branches into main and optimize core architecture

This commit is contained in:
2026-04-23 10:16:31 +09:00
27 changed files with 1924 additions and 1306 deletions

498
server.js
View File

@@ -1,387 +1,243 @@
import express from 'express';
import mysql from 'mysql2/promise';
import cors from 'cors';
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use(cors());
app.use(express.json({ limit: '50mb' }));
// MySQL Connection Pool
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'),
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'itam_user',
password: process.env.DB_PASSWORD || 'itam_pw',
database: process.env.DB_NAME || 'itam_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// 테이블 존재 여부 확인 및 자동 생성
// Helper for DB updates
async function ensureTables() {
const connection = await pool.getConnection();
try {
await connection.query(`
// Cloud_Assets Table
await pool.query(`
CREATE TABLE IF NOT EXISTS cloud_assets (
id VARCHAR(50) PRIMARY KEY,
platform_name VARCHAR(100),
corp VARCHAR(100),
corp VARCHAR(50),
dept VARCHAR(100),
product_name VARCHAR(255),
account_name VARCHAR(255),
pay_method VARCHAR(100),
pay_day VARCHAR(50),
card_num VARCHAR(100),
monthly_fee VARCHAR(100),
remarks TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
purpose VARCHAR(255),
account_name VARCHAR(100),
payment_method VARCHAR(50),
payment_date VARCHAR(50),
card_number VARCHAR(50),
monthly_fee VARCHAR(50),
note TEXT
)
`);
await connection.query(`
// Logs Table
await pool.query(`
CREATE TABLE IF NOT EXISTS asset_logs (
id VARCHAR(50) PRIMARY KEY,
asset_id VARCHAR(50),
log_date VARCHAR(50),
log_user VARCHAR(100),
date VARCHAR(20),
details TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
user VARCHAR(50),
INDEX(asset_id)
)
`);
console.log('✅ Cloud & Logs tables ensured.');
} finally {
connection.release();
}
}
// 공통 배치 저장 로직
async function batchSave(tableName, assets, getQuery) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query(`DELETE FROM ${tableName}`);
if (assets.length > 0) {
const { sql, values } = getQuery(assets);
await connection.query(sql, [values]);
}
await connection.commit();
return { success: true, count: assets.length };
} catch (err) {
await connection.rollback();
throw err;
} finally {
connection.release();
console.error('❌ Error ensuring tables:', err);
}
}
// 하드웨어 쿼리 헬퍼
const hardwareInsertSQL = (table) => `
INSERT INTO ${table} (
id, corp, asset_code, purchase_date, type, detail_purpose, purpose, details,
current_org, prev_org, location, manager_main, manager_sub, ip_address,
remote_tool, server_id, server_pw, model_name, os, cpu, ram, gpu,
storage1, storage2, storage3, monitoring, price, remarks,
storage_location, status
) VALUES ?
`;
ensureTables();
const getHardwareValues = (a) => [
a.id, a.법인||'', a.자산코드||'', a.구매연월||'', a.type||'', a.상세용도||'', a.용도||'', a.상세||'',
a.현사용조직||'', a.이전사용조직||'', a.위치||'', a.담당자_정||'', a.담당자_부||'', a.IP주소||'',
a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'',
a.SSD1||'', a.SSD2||'', a.HDD1||'', a.모니터링||'', a.금액||'', a.비고||'',
a.보관위치||'', a.현재상태||''
];
// --- API Endpoints ---
const mapHardware = (r, defaultType) => {
const type = r.type || defaultType;
return {
id: r.id,
법인: r.corp,
자산코드: r.asset_code,
구매연월: r.purchase_date,
type: type,
상세용도: (type !== '개인PC' && !r.detail_purpose) ? type : r.detail_purpose,
용도: r.purpose,
상세: r.details,
현사용조직: r.current_org,
이전사용조직: r.prev_org,
위치: r.location,
담당자_정: r.manager_main,
담당자_부: r.manager_sub,
IP주소: r.ip_address,
원격접속: r.remote_tool,
서버ID: r.server_id,
서버PW: r.server_pw,
모델명: r.model_name,
OS: r.os,
CPU: r.cpu,
RAM: r.ram,
GPU: r.gpu,
SSD1: r.storage1,
SSD2: r.storage2,
HDD1: r.storage3,
모니터링: r.monitoring,
금액: r.price,
비고: r.remarks,
보관위치: r.storage_location,
현재상태: r.status
};
};
// --- API 라우트 정의 ---
// PC API
app.get('/api/pc', async (req, res) => {
// Get Master Data (Multi-tab)
app.get('/api/master-data', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM pc_assets');
console.log('🔍 DB Raw Rows (PC):', rows.length, 'items found.');
if (rows.length > 0) console.log('🔍 First row sample:', rows[0]);
res.json(rows.map(r => mapHardware(r, '개인PC')));
} catch (err) {
console.error('❌ DB Query Error (PC):', err.message);
res.status(500).json({ error: err.message });
const [pc] = await pool.query('SELECT * FROM pc_assets');
const [server] = await pool.query('SELECT * FROM server_assets');
const [storage] = await pool.query('SELECT * FROM storage_assets');
const [equip] = await pool.query('SELECT * FROM equip_assets');
const [mobile] = await pool.query('SELECT * FROM mobile_assets');
const [subSw] = await pool.query('SELECT * FROM sw_sub_assets');
const [permSw] = await pool.query('SELECT * FROM sw_perm_assets');
const [cloud] = await pool.query('SELECT * FROM cloud_assets');
const [swUsers] = await pool.query('SELECT * FROM sw_users');
const [logs] = await pool.query('SELECT * FROM asset_logs ORDER BY date DESC');
res.json({
pc, server, storage, equip, mobile,
subSw, permSw, cloud,
swUsers,
logs
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/pc/batch', async (req, res) => {
try {
const result = await batchSave('pc_assets', req.body, (assets) => ({
sql: hardwareInsertSQL('pc_assets'),
values: assets.map(getHardwareValues)
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// Save Hardware Asset (PC, Server, Storage, etc.)
app.post('/api/hardware/save', async (req, res) => {
const asset = req.body;
const type = asset.type;
let table = '';
// 서버 API
app.get('/api/server', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM server_assets');
res.json(rows.map(r => mapHardware(r, '서버')));
} catch (err) { res.status(500).json({ error: err.message }); }
});
if (type === '개인PC' || type === 'PC') table = 'pc_assets';
else if (type === '서버') table = 'server_assets';
else if (type === '스토리지') table = 'storage_assets';
else if (type === '모바일' || type === '모바일기기') table = 'mobile_assets';
else table = 'equip_assets';
app.post('/api/server/batch', async (req, res) => {
try {
const result = await batchSave('server_assets', req.body, (assets) => ({
sql: hardwareInsertSQL('server_assets'),
values: assets.map(getHardwareValues)
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// Check if exists
const [rows] = await pool.query(`SELECT id FROM ${table} WHERE id = ?`, [asset.id]);
const data = { ...asset };
delete data.id;
delete data.type;
// 스토리지 API
app.get('/api/storage', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM storage_assets');
res.json(rows.map(r => mapHardware(r, '스토리지')));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/storage/batch', async (req, res) => {
try {
const result = await batchSave('storage_assets', req.body, (assets) => ({
sql: hardwareInsertSQL('storage_assets'),
values: assets.map(getHardwareValues)
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 전산비품 API
app.get('/api/equip', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM equip_assets');
res.json(rows.map(r => mapHardware(r, '전산비품')));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/equip/batch', async (req, res) => {
try {
const result = await batchSave('equip_assets', req.body, (assets) => ({
sql: hardwareInsertSQL('equip_assets'),
values: assets.map(getHardwareValues)
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 모바일 API
app.get('/api/mobile', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM mobile_assets');
res.json(rows.map(r => mapHardware(r, '모바일기기')));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/mobile/batch', async (req, res) => {
try {
const result = await batchSave('mobile_assets', req.body, (assets) => ({
sql: hardwareInsertSQL('mobile_assets'),
values: assets.map(getHardwareValues)
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 구독 SW API
app.get('/api/sw/sub', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM sw_sub_assets');
res.json(rows.map(r => ({
id: r.id, type: '구독SW', 법인: r.corp, 자산번호: r.asset_code, 제품명: r.product_name,
라이선스유형: r.license_type, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date,
만료일: r.expiry_date, 납품업체: r.vendor, 비고: r.remarks
})));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/sw/sub/batch', async (req, res) => {
try {
const result = await batchSave('sw_sub_assets', req.body, (assets) => ({
sql: `INSERT INTO sw_sub_assets (id, corp, asset_code, product_name, license_type, quantity, price, purchase_date, expiry_date, vendor, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.자산번호||'', a.제품명||'', a.라이선스유형||'', a.수량||0, a.금액||'', a.구매일||'', a.만료일||'', a.납품업체||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 영구 SW API
app.get('/api/sw/perm', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM sw_perm_assets');
res.json(rows.map(r => ({
id: r.id, type: '영구SW', 법인: r.corp, 자산번호: r.asset_code, 제품명: r.product_name,
라이선스키: r.license_key, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date,
납품업체: r.vendor, 비고: r.remarks
})));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/sw/perm/batch', async (req, res) => {
try {
const result = await batchSave('sw_perm_assets', req.body, (assets) => ({
sql: `INSERT INTO sw_perm_assets (id, corp, asset_code, product_name, license_key, quantity, price, purchase_date, vendor, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.자산번호||'', a.제품명||'', a.라이선스키||'', a.수량||0, a.금액||'', a.구매일||'', a.납품업체||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 클라우드 API
app.get('/api/cloud', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM cloud_assets');
res.json(rows.map(r => ({
id: r.id, type: '클라우드', 플랫폼명: r.platform_name, 법인: r.corp, 부서: r.dept,
제품명: r.product_name, 계정명: r.account_name, 결제수단: r.pay_method,
결제일: r.pay_day, 연결카드번호: r.card_num, 당월청구액: r.monthly_fee, 비고: r.remarks
})));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/cloud/batch', async (req, res) => {
try {
const result = await batchSave('cloud_assets', req.body, (assets) => ({
sql: `INSERT INTO cloud_assets (id, platform_name, corp, dept, product_name, account_name, pay_method, pay_day, card_num, monthly_fee, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.플랫폼명||'', a.법인||'', a.부서||'', a.제품명||'', a.계정명||'', a.결제수단||'', a.결제일||'', a.연결카드번호||'', a.당월청구액||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 로그 API
app.get('/api/logs', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM asset_logs ORDER BY log_date DESC');
res.json(rows.map(r => ({
id: r.id, assetId: r.asset_id, date: r.log_date, user: r.log_user, details: r.details
})));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/logs/batch', async (req, res) => {
try {
const result = await batchSave('asset_logs', req.body, (assets) => ({
sql: `INSERT INTO asset_logs (id, asset_id, log_date, log_user, details) VALUES ?`,
values: assets.map(a => [a.id, a.assetId||'', a.date||'', a.user||'', a.details||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// SW 사용자 API
app.get('/api/sw-users', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM sw_users');
const grouped = rows.reduce((acc, u) => {
if (!acc[u.sw_id]) acc[u.sw_id] = [];
acc[u.sw_id].push([u.corp, u.dept, u.position, u.user_name, u.usage_period, u.doc_name]);
return acc;
}, {});
res.json(Object.keys(grouped).map(sw_id => ({ sw_id, userData: grouped[sw_id] })));
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/sw-users/batch', async (req, res) => {
try {
const connection = await pool.getConnection();
await connection.beginTransaction();
await connection.query('DELETE FROM sw_users');
const allUsers = req.body;
if (allUsers.length > 0) {
const values = allUsers.flatMap(item =>
(item.userData || []).map(u => [item.sw_id, u[0], u[1], u[2], u[3], u[4], u[5]])
);
if (values.length > 0) {
await connection.query('INSERT INTO sw_users (sw_id, corp, dept, position, user_name, usage_period, doc_name) VALUES ?', [values]);
}
if (rows.length > 0) {
await pool.query(`UPDATE ${table} SET ? WHERE id = ?`, [data, asset.id]);
} else {
await pool.query(`INSERT INTO ${table} SET ?`, [{ id: asset.id, ...data }]);
}
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Save Software Asset
app.post('/api/software/save', async (req, res) => {
const asset = req.body;
const table = asset.type === '구독SW' ? 'sw_sub_assets' : (asset.type === '영구SW' ? 'sw_perm_assets' : 'cloud_assets');
try {
const [rows] = await pool.query(`SELECT id FROM ${table} WHERE id = ?`, [asset.id]);
const data = { ...asset };
delete data.id;
delete data.type;
if (rows.length > 0) {
await pool.query(`UPDATE ${table} SET ? WHERE id = ?`, [data, asset.id]);
} else {
await pool.query(`INSERT INTO ${table} SET ?`, [{ id: asset.id, ...data }]);
}
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Delete Asset
app.delete('/api/asset/:type/:id', async (req, res) => {
const { type, id } = req.params;
let table = '';
if (type === '개인PC' || type === 'PC') table = 'pc_assets';
else if (type === '서버') table = 'server_assets';
else if (type === '스토리지') table = 'storage_assets';
else if (type === '모바일' || type === '모바일기기') table = 'mobile_assets';
else if (type === '전산비품' || type === '기타자산') table = 'equip_assets';
else if (type === '구독SW') table = 'sw_sub_assets';
else if (type === '영구SW') table = 'sw_perm_assets';
else if (type === '클라우드') table = 'cloud_assets';
try {
await pool.query(`DELETE FROM ${table} WHERE id = ?`, [id]);
// Also delete logs and users if needed
if (table.includes('sw')) await pool.query('DELETE FROM sw_users WHERE sw_id = ?', [id]);
await pool.query('DELETE FROM asset_logs WHERE asset_id = ?', [id]);
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Log Save
app.post('/api/logs/save', async (req, res) => {
const log = req.body;
try {
const [rows] = await pool.query('SELECT id FROM asset_logs WHERE id = ?', [log.id]);
if (rows.length > 0) {
await pool.query('UPDATE asset_logs SET ? WHERE id = ?', [log, log.id]);
} else {
await pool.query('INSERT INTO asset_logs SET ?', [log]);
}
await connection.commit();
connection.release();
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 자산번호 자동 생성 API
// SW User Save
app.post('/api/sw-users/save', async (req, res) => {
const user = req.body;
try {
const [rows] = await pool.query('SELECT id FROM sw_users WHERE id = ?', [user.id]);
if (rows.length > 0) {
await pool.query('UPDATE sw_users SET ? WHERE id = ?', [user, user.id]);
} else {
await pool.query('INSERT INTO sw_users SET ?', [user]);
}
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/sw-users/batch', async (req, res) => {
const { swId, users } = req.body;
try {
await pool.query('DELETE FROM sw_users WHERE sw_id = ?', [swId]);
for (const u of users) {
await pool.query('INSERT INTO sw_users SET ?', { ...u, sw_id: swId });
}
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 자산번호 생성 API
app.get('/api/generate-asset-code', async (req, res) => {
const { prefix } = req.query;
if (!prefix) return res.status(400).json({ error: 'Prefix is required' });
try {
const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets'];
const tables = [
'pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets',
'sw_sub_assets', 'sw_perm_assets'
];
let maxNum = 0;
for (const table of tables) {
const [rows] = await pool.query(
`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`,
[`${prefix}%`]
);
const [rows] = await pool.query(`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, [`${prefix}%`]);
rows.forEach(r => {
const numPart = r.asset_code.replace(prefix, '');
const num = parseInt(numPart);
if (!isNaN(num) && num > maxNum) maxNum = num;
});
}
const nextNum = (maxNum + 1).toString().padStart(3, '0');
res.json({ nextCode: `${prefix}${nextNum}` });
const nextCode = `${prefix}${(maxNum + 1).toString().padStart(3, '0')}`;
res.json({ nextCode });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 초기화 및 서버 기동
ensureTables().then(() => {
app.listen(PORT, () => {
console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`);
});
}).catch(err => {
console.error('❌ Failed to start server:', err);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🚀 ITAM Dedicated API Server running on http://localhost:${PORT}`);
});