Files
ITAM/server.js

372 lines
16 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 });
};
// --- Mapping Definitions ---
const HW_SELECT_FIELDS = `
id,
asset_type AS \`type\`,
corp AS \`법인\`,
asset_code AS \`자산코드\`,
purchase_date AS \`구매일\`,
user_name AS \`사용자\`,
dept AS \`현사용조직\`,
prev_org AS \`이전사용조직\`,
location AS \`위치\`,
manager_primary AS \`담당자_정\`,
manager_secondary AS \`담당자_부\`,
product_name AS \`모델명\`,
usage_category AS \`상세용도\`,
usage_description AS \`상세\`,
os AS \`OS\`,
cpu AS \`CPU\`,
gpu AS \`GPU\`,
ram AS \`RAM\`,
storage1 AS \`SSD1\`,
storage2 AS \`SSD2\`,
storage3 AS \`SSD3\`,
mainboard AS \`메인보드\`,
ip_address AS \`IP주소\`,
remote_tool AS \`원격접속\`,
server_id AS \`서버ID\`,
server_pw AS \`서버PW\`,
monitoring AS \`모니터링\`,
price AS \`금액\`,
vendor AS \`납품업체\`,
remarks AS \`비고\`,
asset_category AS \`category\`
`;
const HW_REVERSE_MAP = {
'id': 'id',
'type': 'asset_type',
'법인': 'corp',
'자산코드': 'asset_code',
'구매일': 'purchase_date',
'구매연월': 'purchase_date',
'사용자': 'user_name',
'현사용조직': 'dept',
'이전사용조직': 'prev_org',
'위치': 'location',
'담당자_정': 'manager_primary',
'담당자_부': 'manager_secondary',
'모델명': 'product_name',
'상세용도': 'usage_category',
'상세': 'usage_description',
'OS': 'os',
'CPU': 'cpu',
'GPU': 'gpu',
'RAM': 'ram',
'SSD1': 'storage1',
'SSD2': 'storage2',
'SSD3': 'storage3',
'메인보드': 'mainboard',
'IP주소': 'ip_address',
'원격접속': 'remote_tool',
'서버ID': 'server_id',
'서버PW': 'server_pw',
'모니터링': 'monitoring',
'금액': 'price',
'납품업체': 'vendor',
'비고': 'remarks',
'category': 'asset_category'
};
const mapObject = (obj, mapping) => {
const mapped = {};
Object.entries(obj).forEach(([key, val]) => {
const dbKey = mapping[key] || key;
mapped[dbKey] = val;
});
return mapped;
};
// --- GET Routes ---
app.get('/api/pc', async (req, res) => {
try {
const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "개인PC" OR asset_code LIKE "PC%"`);
console.log(`📡 [GET /api/pc] Returning ${rows.length} rows`);
res.json(rows.map(r => ({ ...r, type: '개인PC', 구매연월: r.구매일 })));
} catch (err) { handleError(res, err, 'GET /api/pc', true); }
});
app.get('/api/server', async (req, res) => {
try {
const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "서버" OR asset_code LIKE "SVR%"`);
console.log(`📡 [GET /api/server] Returning ${rows.length} rows`);
res.json(rows.map(r => ({ ...r, type: '서버', 구매연월: r.구매일 })));
} catch (err) { handleError(res, err, 'GET /api/server', true); }
});
app.get('/api/storage', async (req, res) => {
try {
const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "스토리지" OR asset_code LIKE "STO%"`);
console.log(`📡 [GET /api/storage] Returning ${rows.length} rows`);
res.json(rows.map(r => ({ ...r, type: '스토리지', 구매연월: r.구매일 })));
} catch (err) { handleError(res, err, 'GET /api/storage', true); }
});
app.get('/api/equip', async (req, res) => {
try {
const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "전산비품" OR asset_code LIKE "EQP%"`);
console.log(`📡 [GET /api/equip] Returning ${rows.length} rows`);
res.json(rows.map(r => ({ ...r, type: '전산비품', 구매연월: r.구매일 })));
} catch (err) { handleError(res, err, 'GET /api/equip', true); }
});
app.get('/api/mobile', async (req, res) => {
try {
const [rows] = await pool.query(`SELECT ${HW_SELECT_FIELDS} FROM asset_hardware WHERE asset_category = "모바일기기" OR asset_category LIKE "%모바일%" OR asset_code LIKE "MOB%"`);
console.log(`📡 [GET /api/mobile] Returning ${rows.length} rows`);
res.json(rows.map(r => ({ ...r, type: '모바일기기', 구매연월: r.구매일 })));
} catch (err) { handleError(res, err, 'GET /api/mobile', true); }
});
app.get('/api/asset/software/subscription', async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT
id, category AS \`분야\`, corp AS \`법인\`, dept AS \`부서\`, product_name AS \`제품명\`,
quantity AS \`수량\`, price AS \`금액\`, purchase_date AS \`구매일\`, start_date AS \`시작일\`,
expiry_date AS \`만료일\`, vendor AS \`납품업체\`, remarks AS \`비고\`, license_type AS \`라이선스유형\`, account_name AS \`계정명\`
FROM asset_software_subscription
`);
console.log(`📡 [GET /api/asset/software/subscription] Returning ${rows.length} rows`);
res.json(rows.map(r => ({ ...r, type: '구독SW' })));
} catch (err) { handleError(res, err, 'GET /api/asset/software/subscription', true); }
});
app.get('/api/asset/software/perpetual', async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT
id, category AS \`분야\`, corp AS \`법인\`, dept AS \`부서\`, product_name AS \`제품명\`,
quantity AS \`수량\`, price AS \`금액\`, purchase_date AS \`구매일\`, start_date AS \`시작일\`,
expiry_date AS \`만료일\`, vendor AS \`납품업체\`, remarks AS \`비고\`, license_key AS \`라이선스키\`, account_name AS \`계정명\`
FROM asset_software_perpetual
`);
console.log(`📡 [GET /api/asset/software/perpetual] Returning ${rows.length} rows`);
res.json(rows.map(r => ({ ...r, type: '영구SW' })));
} catch (err) { handleError(res, err, 'GET /api/asset/software/perpetual', true); }
});
app.get('/api/asset/cloud', async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT
id, platform_name AS \`플랫폼명\`, corp AS \`법인\`, dept AS \`부서\`, product_name AS \`제품명\`,
account_name AS \`계정명\`, pay_method AS \`결제수단\`, pay_day AS \`결제일\`, card_num AS \`연결카드번호\`,
monthly_cost AS \`당월청구액\`, remarks AS \`비고\`
FROM asset_cloud
`);
console.log(`📡 [GET /api/asset/cloud] Returning ${rows.length} rows`);
res.json(rows.map(r => ({ ...r, type: '클라우드' })));
} catch (err) { handleError(res, err, 'GET /api/asset/cloud', true); }
});
app.get('/api/asset/domain', async (req, res) => {
try {
const [rows] = await pool.query(`SELECT * FROM asset_domain`);
res.json(rows);
} catch (err) { handleError(res, err, 'GET /api/asset/domain', true); }
});
app.get('/api/asset/software/assignment', async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT
id, sw_id, corp AS \`법인\`, dept AS \`부서\`, position AS \`직위\`, user_name AS \`이름\`, usage_period AS \`사용기간\`, doc_name AS \`신청서명\`
FROM asset_software_assignment
`);
res.json(rows);
} catch (err) { handleError(res, err, 'GET /api/asset/software/assignment', true); }
});
app.get('/api/asset/history', async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT
id, asset_id AS assetId, log_date AS \`date\`, log_user AS \`user\`, details, cost
FROM asset_history
`);
res.json(rows);
} catch (err) { handleError(res, err, 'GET /api/asset/history', true); }
});
// --- POST Batch Routes ---
async function saveHwBatch(category, items, res) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 1. 해당 카테고리 기존 데이터 삭제
await connection.query('DELETE FROM asset_hardware WHERE asset_category = ?', [category]);
// 2. 새 데이터 삽입
for (const item of items) {
const dbRow = mapObject(item, HW_REVERSE_MAP);
dbRow.asset_category = category;
if (!dbRow.id) dbRow.id = Math.random().toString(36).substring(2, 9);
// DB 컬럼에 없는 필드 제거
const validColumns = ['id', 'asset_category', 'corp', 'asset_code', 'purchase_date', 'asset_type', 'usage_category', 'user_name', 'usage_description', 'dept', 'prev_org', 'location', 'manager_primary', 'manager_secondary', 'ip_address', 'remote_tool', 'server_id', 'server_pw', 'product_name', 'mainboard', 'os', 'cpu', 'ram', 'gpu', 'storage1', 'storage2', 'storage3', 'monitoring', 'price', 'vendor', 'remarks', 'storage_location', 'status'];
const filteredRow = {};
validColumns.forEach(col => { if (dbRow[col] !== undefined) filteredRow[col] = dbRow[col]; });
await connection.query('INSERT INTO asset_hardware SET ?', [filteredRow]);
}
await connection.commit();
res.json({ success: true, count: items.length });
} catch (err) {
await connection.rollback();
handleError(res, err, `BATCH SAVE ${category}`);
} finally {
connection.release();
}
}
app.post('/api/pc/batch', (req, res) => saveHwBatch('개인PC', req.body, res));
app.post('/api/server/batch', (req, res) => saveHwBatch('서버', req.body, res));
app.post('/api/storage/batch', (req, res) => saveHwBatch('스토리지', req.body, res));
app.post('/api/equip/batch', (req, res) => saveHwBatch('전산비품', req.body, res));
app.post('/api/mobile/batch', (req, res) => saveHwBatch('모바일기기', req.body, res));
app.post('/api/asset/software/subscription/batch', async (req, res) => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('DELETE FROM asset_software_subscription');
const mapping = { '분야': 'category', '법인': 'corp', '부서': 'dept', '제품명': 'product_name', '수량': 'quantity', '금액': 'price', '구매일': 'purchase_date', '시작일': 'start_date', '만료일': 'expiry_date', '납품업체': 'vendor', '비고': 'remarks', '라이선스유형': 'license_type', '계정명': 'account_name' };
for (const item of req.body) {
const dbRow = mapObject(item, mapping);
if (!dbRow.id) dbRow.id = Math.random().toString(36).substring(2, 9);
const filteredRow = {};
['id', 'corp', 'category', 'dept', 'product_name', 'license_type', 'quantity', 'price', 'purchase_date', 'start_date', 'expiry_date', 'vendor', 'remarks', 'account_name'].forEach(c => { if (dbRow[c] !== undefined) filteredRow[c] = dbRow[c]; });
await connection.query('INSERT INTO asset_software_subscription SET ?', [filteredRow]);
}
await connection.commit();
res.json({ success: true });
} catch (err) { await connection.rollback(); handleError(res, err, 'BATCH SW SUB'); } finally { connection.release(); }
});
app.post('/api/asset/software/perpetual/batch', async (req, res) => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('DELETE FROM asset_software_perpetual');
const mapping = { '분야': 'category', '법인': 'corp', '부서': 'dept', '제품명': 'product_name', '수량': 'quantity', '금액': 'price', '구매일': 'purchase_date', '시작일': 'start_date', '만료일': 'expiry_date', '납품업체': 'vendor', '비고': 'remarks', '라이선스키': 'license_key', '계정명': 'account_name' };
for (const item of req.body) {
const dbRow = mapObject(item, mapping);
if (!dbRow.id) dbRow.id = Math.random().toString(36).substring(2, 9);
const filteredRow = {};
['id', 'corp', 'category', 'dept', 'product_name', 'license_key', 'quantity', 'price', 'purchase_date', 'start_date', 'expiry_date', 'vendor', 'remarks', 'account_name'].forEach(c => { if (dbRow[c] !== undefined) filteredRow[c] = dbRow[c]; });
await connection.query('INSERT INTO asset_software_perpetual SET ?', [filteredRow]);
}
await connection.commit();
res.json({ success: true });
} catch (err) { await connection.rollback(); handleError(res, err, 'BATCH SW PERM'); } finally { connection.release(); }
});
app.post('/api/asset/cloud/batch', async (req, res) => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('DELETE FROM asset_cloud');
const mapping = { '플랫폼명': 'platform_name', '법인': 'corp', '부서': 'dept', '제품명': 'product_name', '계정명': 'account_name', '결제수단': 'pay_method', '결제일': 'pay_day', '연결카드번호': 'card_num', '당월청구액': 'monthly_cost', '비고': 'remarks' };
for (const item of req.body) {
const dbRow = mapObject(item, mapping);
if (!dbRow.id) dbRow.id = Math.random().toString(36).substring(2, 9);
const filteredRow = {};
['id', 'platform_name', 'corp', 'dept', 'product_name', 'account_name', 'pay_method', 'pay_day', 'card_num', 'monthly_cost', 'remarks'].forEach(c => { if (dbRow[c] !== undefined) filteredRow[c] = dbRow[c]; });
await connection.query('INSERT INTO asset_cloud SET ?', [filteredRow]);
}
await connection.commit();
res.json({ success: true });
} catch (err) { await connection.rollback(); handleError(res, err, 'BATCH CLOUD'); } finally { connection.release(); }
});
app.post('/api/asset/software/assignment/batch', async (req, res) => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('DELETE FROM asset_software_assignment');
const mapping = { '법인': 'corp', '부서': 'dept', '직위': 'position', '이름': 'user_name', '사용기간': 'usage_period', '신청서명': 'doc_name' };
for (const item of req.body) {
const dbRow = mapObject(item, mapping);
delete dbRow.id; // Auto-increment
await connection.query('INSERT INTO asset_software_assignment SET ?', [dbRow]);
}
await connection.commit();
res.json({ success: true });
} catch (err) { await connection.rollback(); handleError(res, err, 'BATCH SW ASSIGN'); } finally { connection.release(); }
});
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' });
const [rows] = await pool.query('SELECT asset_code FROM asset_hardware WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1', [`${prefix}%`]);
let nextNum = 1;
if (rows.length > 0) {
const lastCode = rows[0].asset_code;
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 (Safe Korean Mapping)');
});