feat: 자산 관리 시스템 고도화 및 데이터 구조 최적화
- 모바일 자산(Mobile) 카테고리 추가 및 엑셀 업로드/다운로드 지원 - 클라우드 자산(Cloud) 및 변경 이력(Logs) 테이블 및 API 구현 - 데이터베이스 초기화 로직 개선 및 테이블 자동 생성 기능 추가 - 하드웨어 저장 로직 통합 및 카테고리 판별 자동화 - SW 대시보드 사용량 산출 방식 개선 (sw_id 기반 맵핑) - 수동 모달(Storage)을 통합 하드웨어 모달(HWModal)로 통합 및 정리
This commit is contained in:
249
server.js
249
server.js
@@ -22,6 +22,42 @@ const pool = mysql.createPool({
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
// 테이블 존재 여부 확인 및 자동 생성
|
||||
async function ensureTables() {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS cloud_assets (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
platform_name VARCHAR(100),
|
||||
corp VARCHAR(100),
|
||||
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;
|
||||
`);
|
||||
await connection.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),
|
||||
details TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('✅ Cloud & Logs tables ensured.');
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 공통 배치 저장 로직
|
||||
async function batchSave(tableName, assets, getQuery) {
|
||||
const connection = await pool.getConnection();
|
||||
@@ -42,81 +78,45 @@ async function batchSave(tableName, assets, getQuery) {
|
||||
}
|
||||
}
|
||||
|
||||
// 공통 하드웨어 매핑 함수
|
||||
const mapHardware = (r, defaultType) => ({
|
||||
id: r.id,
|
||||
법인: r.corp,
|
||||
자산코드: r.asset_code,
|
||||
구매일: r.purchase_date,
|
||||
purchase_date: r.purchase_date,
|
||||
type: r.type || defaultType,
|
||||
상세용도: r.detail_purpose,
|
||||
detail_purpose: r.detail_purpose,
|
||||
용도: r.purpose,
|
||||
purpose: r.purpose,
|
||||
상세: r.details,
|
||||
details: r.details,
|
||||
현사용조직: r.current_org,
|
||||
current_org: r.current_org,
|
||||
이전사용조직: r.prev_org,
|
||||
prev_org: r.prev_org,
|
||||
위치: r.location,
|
||||
location: r.location,
|
||||
담당자_정: r.manager_main,
|
||||
manager_main: r.manager_main,
|
||||
담당자_부: r.manager_sub,
|
||||
manager_sub: r.manager_sub,
|
||||
IP주소: r.ip_address,
|
||||
ip_address: r.ip_address,
|
||||
원격접속: r.remote_tool,
|
||||
remote_tool: r.remote_tool,
|
||||
서버ID: r.server_id,
|
||||
server_id: r.server_id,
|
||||
서버PW: r.server_pw,
|
||||
server_pw: r.server_pw,
|
||||
모델명: r.model_name,
|
||||
model_name: r.model_name,
|
||||
OS: r.os,
|
||||
os: r.os,
|
||||
CPU: r.cpu,
|
||||
cpu: r.cpu,
|
||||
RAM: r.ram,
|
||||
ram: r.ram,
|
||||
GPU: r.gpu,
|
||||
gpu: r.gpu,
|
||||
SSD1: r.storage1,
|
||||
storage1: r.storage1,
|
||||
SSD2: r.storage2,
|
||||
storage2: r.storage2,
|
||||
HDD1: r.storage3,
|
||||
storage3: r.storage3,
|
||||
모니터링: r.monitoring,
|
||||
monitoring: r.monitoring,
|
||||
금액: r.price,
|
||||
price: r.price,
|
||||
비고: r.remarks,
|
||||
remarks: r.remarks
|
||||
});
|
||||
// 하드웨어 쿼리 헬퍼
|
||||
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
|
||||
) VALUES ?
|
||||
`;
|
||||
|
||||
// 공통 하드웨어 저장 값 생성 함수
|
||||
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.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.비고||''
|
||||
];
|
||||
|
||||
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)
|
||||
VALUES ?
|
||||
`;
|
||||
const mapHardware = (r, defaultType) => ({
|
||||
id: r.id, 법인: r.corp, 자산코드: r.asset_code, 구매일: r.purchase_date, type: r.type || defaultType,
|
||||
상세용도: 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
|
||||
});
|
||||
|
||||
// --- 1. 개인PC API ---
|
||||
// --- API 라우트 정의 ---
|
||||
|
||||
// PC API
|
||||
app.get('/api/pc', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM pc_assets');
|
||||
res.json(rows.map(r => mapHardware(r, 'PC')));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/pc/batch', async (req, res) => {
|
||||
@@ -129,7 +129,7 @@ app.post('/api/pc/batch', async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// --- 2. 서버 API ---
|
||||
// 서버 API
|
||||
app.get('/api/server', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM server_assets');
|
||||
@@ -147,7 +147,7 @@ app.post('/api/server/batch', async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// --- 3. 스토리지 API ---
|
||||
// 스토리지 API
|
||||
app.get('/api/storage', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM storage_assets');
|
||||
@@ -165,7 +165,7 @@ app.post('/api/storage/batch', async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// --- 4. 전산비품 API ---
|
||||
// 전산비품 API
|
||||
app.get('/api/equip', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM equip_assets');
|
||||
@@ -183,7 +183,7 @@ app.post('/api/equip/batch', async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// --- 5. 모바일기기 API ---
|
||||
// 모바일 API
|
||||
app.get('/api/mobile', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM mobile_assets');
|
||||
@@ -201,16 +201,15 @@ app.post('/api/mobile/batch', async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// --- 6. 소프트웨어 구독 API ---
|
||||
// 구독 SW API
|
||||
app.get('/api/sw/sub', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM sw_sub_assets');
|
||||
const mapped = rows.map(r => ({
|
||||
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
|
||||
}));
|
||||
res.json(mapped);
|
||||
})));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
@@ -224,16 +223,15 @@ app.post('/api/sw/sub/batch', async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// --- 7. 소프트웨어 영구 API ---
|
||||
// 영구 SW API
|
||||
app.get('/api/sw/perm', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM sw_perm_assets');
|
||||
const mapped = rows.map(r => ({
|
||||
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
|
||||
}));
|
||||
res.json(mapped);
|
||||
})));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
@@ -247,15 +245,58 @@ app.post('/api/sw/perm/batch', async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// --- 8. 소프트웨어 사용자 관리 API ---
|
||||
// 클라우드 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 result = rows.map(u => ({
|
||||
sw_id: u.sw_id,
|
||||
userData: [u.corp||'', u.dept||'', u.position||'', u.user_name||'', u.usage_period||'', u.doc_name||'']
|
||||
}));
|
||||
res.json(result);
|
||||
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 }); }
|
||||
});
|
||||
|
||||
@@ -267,7 +308,7 @@ app.post('/api/sw-users/batch', async (req, res) => {
|
||||
const allUsers = req.body;
|
||||
if (allUsers.length > 0) {
|
||||
const values = allUsers.flatMap(item =>
|
||||
item.userDataList.map(u => [item.sw_id, u.구매법인||u.법인||'', u.부서||'', u.직위||'', u.이름||'', u.사용기간||'', u.신청서명||''])
|
||||
(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]);
|
||||
@@ -279,35 +320,11 @@ app.post('/api/sw-users/batch', async (req, res) => {
|
||||
} 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 = ['server_assets', 'pc_assets', 'storage_assets', 'equip_assets', 'mobile_assets'];
|
||||
let maxNum = 0;
|
||||
|
||||
for (const table of tables) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT asset_code as 자산코드 FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`,
|
||||
[`${prefix}%`]
|
||||
);
|
||||
|
||||
if (rows.length > 0) {
|
||||
const lastCode = rows[0].자산코드;
|
||||
const lastNum = parseInt(lastCode.split('-').pop() || '0');
|
||||
if (lastNum > maxNum) maxNum = lastNum;
|
||||
}
|
||||
}
|
||||
|
||||
const nextNum = String(maxNum + 1).padStart(3, '0');
|
||||
res.json({ nextCode: `${prefix}${nextNum}` });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`);
|
||||
// 초기화 및 서버 기동
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user