feat: 자산 카테고리별 6개 전용 테이블 분리 및 백엔드 API, 프론트엔드 상태 관리 전면 개편 (개인PC, 서버, 스토리지, 전산비품, 구독SW, 영구SW)

This commit is contained in:
2026-04-17 17:25:52 +09:00
parent 6904925146
commit 415727a866
11 changed files with 409 additions and 593 deletions

314
server.js
View File

@@ -23,202 +23,190 @@ const pool = mysql.createPool({
queueLimit: 0
});
// --- API Routes ---
// 1. 하드웨어 자산 조회
app.get('/api/hw', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM hw_assets');
// DB 컬럼명을 프론트엔드 인터페이스(한글)에 맞게 매핑
const mapped = rows.map(r => ({
id: r.id,
type: r.type,
법인: r.corp,
자산코드: r.asset_code,
명칭: r.asset_name,
위치: r.location,
현사용조직: r.current_org,
이전사용조직: r.prev_org,
담당자_정: r.manager_main,
관리자: r.manager_main,
담당자_부: r.manager_sub,
IP주소: r.ip_address,
IP2: r.ip_address2,
MACaddress: r.mac_address,
OS: r.os,
CPU: r.cpu,
RAM: r.ram,
SSD1: r.storage1,
SSD2: r.storage2,
모델명: r.model_name,
구매일: r.purchase_date,
금액: r.price,
납품업체: r.vendor,
품의서명: r.doc_name,
용도: r.asset_name, // 서버의 경우 명칭을 용도로 사용
상세: r.remarks,
원격접속: r.remote_tool,
서버ID: r.server_id,
서버PW: r.server_pw,
모니터링: r.monitoring,
비고: r.remarks
}));
res.json(mapped);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 2. 하드웨어 자산 일괄 저장 (항상 덮어쓰기)
app.post('/api/hw/batch', async (req, res) => {
const assets = req.body;
// --- 공통 헬퍼 함수 ---
async function batchSave(tableName, assets, mapFn) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('DELETE FROM hw_assets');
await connection.query(`DELETE FROM ${tableName}`);
if (assets.length > 0) {
const sql = `
INSERT INTO hw_assets (
id, type, corp, asset_code, asset_name, location, current_org, prev_org,
manager_main, manager_sub, ip_address, ip_address2, mac_address, os,
cpu, ram, storage1, storage2, model_name, purchase_date, price,
vendor, doc_name, remote_tool, server_id, server_pw, monitoring, remarks
) VALUES ?
`;
const values = assets.map(a => [
a.id, a.type, a.법인, a.자산코드, a.명칭 || a.용도, a.위치, a.현사용조직, a.이전사용조직,
a.담당자_정 || a.관리자, a.담당자_부, a.IP주소, a.IP2, a.MACaddress, a.OS,
a.CPU, a.RAM, a.SSD1, a.SSD2, a.모델명, a.구매일, a.금액,
a.납품업체, a.품의서명, a.원격접속, a.서버ID, a.서버PW, a.모니터링, a.비고 || a.상세
]);
const { sql, values } = mapFn(assets);
await connection.query(sql, [values]);
}
await connection.commit();
res.json({ success: true, count: assets.length, mode: 'overwrite' });
return { success: true, count: assets.length };
} catch (err) {
await connection.rollback();
res.status(500).json({ error: err.message });
throw err;
} finally {
connection.release();
}
});
}
// 3. 소프트웨어 자산 조회
app.get('/api/sw', async (req, res) => {
// --- 1. 개인PC (PC) API ---
app.get('/api/pc', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM sw_assets');
const [rows] = await pool.query('SELECT * FROM pc_assets');
const mapped = rows.map(r => ({
id: r.id,
type: r.type,
분야: r.category,
법인: r.corp,
부서: r.dept,
제품명: r.product_name,
구매일: r.purchase_date,
구독일: r.subscription_date,
유지보수여부: !!r.maintenance_status,
금액: r.price,
수량: r.quantity,
계정명: r.account_id,
납품업체: r.vendor,
비고: r.remarks
id: r.id, type: '개인PC', 법인: r.corp, 자산코드: r.asset_code, 사용자: r.user, 위치: r.location,
CPU: r.cpu, GPU: r.gpu, RAM: r.ram, SSD1: r.ssd1, SSD2: r.ssd2, HDD1: r.hdd1, HDD2: r.hdd2,
IP주소: r.ip_address, HW사양: r.hw_spec, 구매일: r.purchase_date, 금액: r.price,
납품업체: r.vendor, 품의서명: r.doc_name, 비고: r.remarks
}));
res.json(mapped);
} catch (err) {
res.status(500).json({ error: err.message });
}
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 4. 소프트웨어 자산 일괄 저장 (항상 덮어쓰기)
app.post('/api/sw/batch', async (req, res) => {
const assets = req.body;
const connection = await pool.getConnection();
app.post('/api/pc/batch', async (req, res) => {
try {
await connection.beginTransaction();
await connection.query('DELETE FROM sw_assets');
if (assets.length > 0) {
const sql = `
INSERT INTO sw_assets (
id, type, category, corp, dept, product_name, purchase_date,
subscription_date, maintenance_status, price, quantity,
account_id, vendor, remarks
) VALUES ?
`;
const values = assets.map(a => [
a.id, a.type, a.분야, a.법인, a.부서, a.제품명, a.구매일,
a.구독일, a.유지보수여부 ? 1 : 0, a.금액, a.수량,
a.계정명, a.납품업체, a.비고
]);
await connection.query(sql, [values]);
}
await connection.commit();
res.json({ success: true, count: assets.length, mode: 'overwrite' });
} catch (err) {
await connection.rollback();
res.status(500).json({ error: err.message });
} finally {
connection.release();
}
const result = await batchSave('pc_assets', req.body, (assets) => ({
sql: `INSERT INTO pc_assets (id, corp, asset_code, user, location, cpu, gpu, ram, ssd1, ssd2, hdd1, hdd2, ip_address, hw_spec, purchase_date, price, vendor, doc_name, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.자산코드||'', a.사용자||'', a.위치||'', a.CPU||'', a.GPU||'', a.RAM||'', a.SSD1||'', a.SSD2||'', a.HDD1||'', a.HDD2||'', a.IP주소||'', a.HW사양||'', a.구매일||'', a.금액||'', a.납품업체||'', a.품의서명||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 5. SW 사용자 매핑 조회
// --- 2. 서버 (Server) API ---
app.get('/api/server', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM server_assets');
const mapped = rows.map(r => ({
id: r.id, type: '서버', 법인: r.corp, 자산코드: r.asset_code, 구매일: r.purchase_date, storage유형: r.type,
용도: 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.remarks
}));
res.json(mapped);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/server/batch', async (req, res) => {
try {
const result = await batchSave('server_assets', req.body, (assets) => ({
sql: `INSERT INTO server_assets (id, corp, asset_code, purchase_date, type, 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, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.자산코드||'', a.구매일||'', a.storage유형||'물리', 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.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 3. 스토리지 (Storage) API ---
app.get('/api/storage', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM storage_assets');
const mapped = rows.map(r => ({
id: r.id, type: '스토리지', 법인: r.corp, storage유형: r.type, 자산코드: r.asset_code, 명칭: r.asset_name,
위치: r.location, 모델명: r.model_name, 용량: r.capacity, 담당자_정: r.manager_main, 담당자_부: r.manager_sub,
IP주소: r.ip_address, MACaddress: r.mac_address, 구매일: r.purchase_date, 금액: r.price,
납품업체: r.vendor, 품의서명: r.doc_name, 비고: r.remarks
}));
res.json(mapped);
} 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: `INSERT INTO storage_assets (id, corp, type, asset_code, asset_name, location, model_name, capacity, manager_main, manager_sub, ip_address, mac_address, purchase_date, price, vendor, doc_name, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.storage유형||'', a.자산코드||'', a.명칭||'', a.위치||'', a.모델명||'', a.용량||'', a.담당자_정||'', a.담당자_부||'', a.IP주소||'', a.MACaddress||'', a.구매일||'', a.금액||'', a.납품업체||'', a.품의서명||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 4. 전산비품 (Equipment) API ---
app.get('/api/equip', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM equip_assets');
const mapped = rows.map(r => ({
id: r.id, type: '전산비품', 법인: r.corp, 비품유형: r.type, 자산코드: r.asset_code, 명칭: r.asset_name,
위치: r.location, 관리자: r.manager, IP주소: r.ip_address, MACaddress: r.mac_address,
HW사양: r.hw_spec, OS: r.os, 구매일: r.purchase_date, 금액: r.price,
납품업체: r.vendor, 품의서명: r.doc_name, 비고: r.remarks
}));
res.json(mapped);
} 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: `INSERT INTO equip_assets (id, corp, type, asset_code, asset_name, location, manager, ip_address, mac_address, hw_spec, os, purchase_date, price, vendor, doc_name, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.법인||'', a.비품유형||'', a.자산코드||'', a.명칭||'', a.위치||'', a.관리자||'', a.IP주소||'', a.MACaddress||'', a.HW사양||'', a.OS||'', a.구매일||'', a.금액||'', a.납품업체||'', a.품의서명||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 5. 구독SW (Subscription) API ---
app.get('/api/sw/sub', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM subscription_sw');
const mapped = rows.map(r => ({
id: r.id, type: '구독SW', 분야: r.category, 법인: r.corp, 부서: r.dept, 제품명: r.product_name,
구매일: r.purchase_date, 구독일: r.subscription_date, 금액: r.price, 수량: r.quantity,
계정명: r.account_id, 납품업체: r.vendor, 비고: r.remarks
}));
res.json(mapped);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/sw/sub/batch', async (req, res) => {
try {
const result = await batchSave('subscription_sw', req.body, (assets) => ({
sql: `INSERT INTO subscription_sw (id, category, corp, dept, product_name, purchase_date, subscription_date, price, quantity, account_id, vendor, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.분야||'', a.법인||'', a.부서||'', a.제품명||'', a.구매일||'', a.구독일||'', a.금액||'', a.수량||1, a.계정명||'', a.납품업체||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 6. 영구SW (Permanent) API ---
app.get('/api/sw/perm', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM permanent_sw');
const mapped = rows.map(r => ({
id: r.id, type: '영구SW', 분야: r.category, 법인: r.corp, 부서: r.dept, 제품명: r.product_name,
구매일: r.purchase_date, 유지보수여부: !!r.maintenance_status, 금액: r.price, 수량: r.quantity,
계정명: r.account_id, 납품업체: r.vendor, 비고: r.remarks
}));
res.json(mapped);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.post('/api/sw/perm/batch', async (req, res) => {
try {
const result = await batchSave('permanent_sw', req.body, (assets) => ({
sql: `INSERT INTO permanent_sw (id, category, corp, dept, product_name, purchase_date, maintenance_status, price, quantity, account_id, vendor, remarks) VALUES ?`,
values: assets.map(a => [a.id, a.분야||'', a.법인||'', a.부서||'', a.제품명||'', a.구매일||'', a.유지보수여부?1:0, a.금액||'', a.수량||1, a.계정명||'', a.납품업체||'', a.비고||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// --- 7. SW 사용자 (SW Users) API ---
app.get('/api/sw-users', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM sw_users');
const mapped = rows.map(r => ({
id: r.id,
swId: r.sw_id,
법인: r.corp,
부서: r.dept,
: r.team,
직위: r.position,
이름: r.name,
사용기간: r.usage_period,
신청서명: r.doc_name
id: r.id, swId: r.sw_id, 법인: r.corp, 부서: r.dept, : r.team, 직위: r.position, 이름: r.name, 사용기간: r.usage_period, 신청서명: r.doc_name
}));
res.json(mapped);
} catch (err) {
res.status(500).json({ error: err.message });
}
} catch (err) { res.status(500).json({ error: err.message }); }
});
// 6. SW 사용자 일괄 저장 (항상 덮어쓰기)
app.post('/api/sw-users/batch', async (req, res) => {
const users = req.body;
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query('DELETE FROM sw_users');
if (users.length > 0) {
const sql = `
INSERT INTO sw_users (
id, sw_id, corp, dept, team, position, name, usage_period, doc_name
) VALUES ?
`;
const values = users.map(u => [
u.id, u.swId, u.법인, u.부서, u., u.직위, u.이름, u.사용기간, u.신청서명
]);
await connection.query(sql, [values]);
}
await connection.commit();
res.json({ success: true, count: users.length, mode: 'overwrite' });
} catch (err) {
await connection.rollback();
res.status(500).json({ error: err.message });
} finally {
connection.release();
}
const result = await batchSave('sw_users', req.body, (users) => ({
sql: `INSERT INTO sw_users (id, sw_id, corp, dept, team, position, name, usage_period, doc_name) VALUES ?`,
values: users.map(u => [u.id, u.swId, u.법인||'', u.부서||'', u.||'', u.직위||'', u.이름||'', u.사용기간||'', u.신청서명||''])
}));
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.listen(PORT, () => {
console.log(`📡 ITAM API Server running on http://localhost:${PORT}`);
console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`);
});