feat: 자산 관리 시스템 고도화 및 DB 정규화 대응 수정

1. 자산 저장 시 500 에러 해결: V3 정규화 스키마에 맞춰 테이블 매핑 최신화 및 저장 로직 안정화
2. 자산 번호 체계 개편: 구매일자(YYYYMM)와 유형을 기반으로 PREFIX-YYYYMM-NNNN 규칙 적용 (코드 로직 수정 및 기존 데이터 전량 갱신)
3. 구매일자 표준화: 모든 purchase_date를 YYYY-MM-DD 형식으로 통일
4. HWModal 기능 복원: 위치 등록 시 다중 사진 페이지네이션(좌우 버튼) 기능 복구
5. 위치 지도 고도화: HTML 인터랙티브 지도 지원 및 이미지 지도 내 좌석 스내핑 로직 추가
This commit is contained in:
2026-06-12 10:29:42 +09:00
parent 0c1977f707
commit 9186eb50ca
6 changed files with 295 additions and 201 deletions

View File

@@ -36,25 +36,24 @@ const handleError = (res, err, label) => {
// --- Global Constants ---
const CATEGORY_TABLE_MAP = {
pc: 'asset_pc',
server: 'asset_server',
storage: 'asset_storage',
network: 'asset_remote',
equipment: 'asset_equipment',
officeSupplies: 'asset_office_supplies',
survey: 'asset_survey',
vip: 'asset_vip',
swInternal: 'sw_internal',
swExternal: 'sw_external',
cloud: 'asset_cloud',
pc: 'asset_core',
server: 'asset_core',
storage: 'asset_core',
network: 'asset_core',
equipment: 'asset_core',
officeSupplies: 'asset_core',
survey: 'asset_core',
vip: 'asset_core',
pcParts: 'asset_core',
swInternal: 'asset_software_perpetual',
swExternal: 'asset_software_subscription',
swUsers: 'asset_software_assignment',
users: 'system_users',
swUsers: 'sw_assignment',
logs: 'asset_history'
};
const ASSET_TABLES = [
'asset_pc', 'asset_server', 'asset_storage', 'asset_remote',
'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip'
'asset_core'
];
// --- API Endpoints ---
@@ -101,8 +100,9 @@ app.post('/api/:table/batch', async (req, res) => {
// 2. Get All Assets (Integrated Master Data from Normalized V3 Schema)
app.get('/api/assets/master', async (req, res) => {
let connection;
try {
const connection = await pool.getConnection();
connection = await pool.getConnection();
const masterData = {
pc: [], server: [], storage: [], network: [],
@@ -110,6 +110,7 @@ app.get('/api/assets/master', async (req, res) => {
swInternal: [], swExternal: [], swUsers: [], users: [], logs: []
};
// Load from V3 Normalized Schema
const [rows] = await connection.query(`
SELECT
c.*,
@@ -156,10 +157,11 @@ app.get('/api/assets/master', async (req, res) => {
masterData.users = users;
masterData.logs = logs;
connection.release();
res.json(masterData);
} catch (err) {
handleError(res, err, 'MASTER DATA');
} finally {
if (connection) connection.release();
}
});
@@ -177,15 +179,11 @@ app.post('/api/asset/:category/save', async (req, res) => {
const oldCore = oldCoreRows[0] || {};
const oldSpec = oldSpecRows[0] || {};
console.log(`🔍 [History Check] ID: ${asset.id}`);
console.log(` - Dept: [${oldCore.current_dept}] -> [${asset.current_dept}]`);
console.log(` - User: [${oldCore.user_current}] -> [${asset.user_current}]`);
const historyLogs = [];
const logDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const logUser = '관리자';
// 조직 변동 감지 (null/undefined/empty string 세이프 처리)
// 3.0.1 Core 변동 감지 (Dept, User)
const oldDept = oldCore.current_dept || '';
const newDept = asset.current_dept || '';
if (newDept !== '' && oldDept !== newDept) {
@@ -198,7 +196,6 @@ app.post('/api/asset/:category/save', async (req, res) => {
});
}
// 사용자 변동 감지
const oldUser = oldCore.user_current || '';
const newUser = asset.user_current || '';
if (newUser !== '' && oldUser !== newUser) {
@@ -211,26 +208,27 @@ app.post('/api/asset/:category/save', async (req, res) => {
});
}
// 유형/용도 감지
const oldType = oldCore.asset_type || '';
const newType = asset.asset_type || '';
if (newType !== '' && oldType !== newType) {
historyLogs.push({
event_type: 'ROLE_CHANGE',
details: `[유형 변경] ${oldType || '(없음)'} -> ${newType}`
});
}
// 3.0.2 Spec 감지 (CPU, RAM, GPU, OS, Mainboard 등)
const specFieldsToTrack = [
{ key: 'cpu', label: 'CPU' },
{ key: 'ram', label: 'RAM' },
{ key: 'gpu', label: 'GPU' },
{ key: 'os', label: 'OS' },
{ key: 'mainboard', label: '메인보드' }
];
const oldRole = oldCore.current_role || '';
const newRole = asset.current_role || '';
if (newRole !== '' && oldRole !== newRole) {
historyLogs.push({
event_type: 'ROLE_CHANGE',
details: `[용도 변경] ${oldRole || '(없음)'} -> ${newRole}`
});
}
specFieldsToTrack.forEach(field => {
const oldVal = String(oldSpec[field.key] || '').trim();
const newVal = String(asset[field.key] || '').trim();
if (newVal !== '' && oldVal !== newVal) {
historyLogs.push({
event_type: 'SPEC_CHANGE',
details: `[사양 변경] ${field.label}: ${oldVal || '(없음)'} -> ${newVal}`
});
}
});
// 상태 변경 감지
// 3.0.3 상태 변경 감지
const oldStatus = oldSpec.hw_status || '';
const newStatus = asset.hw_status || '';
if (newStatus !== '' && oldStatus !== newStatus) {
@@ -240,8 +238,6 @@ app.post('/api/asset/:category/save', async (req, res) => {
});
}
console.log(` - Logs Generated: ${historyLogs.length}`);
// 로그 일괄 삽입
for (const log of historyLogs) {
await connection.query(
@@ -256,8 +252,23 @@ app.post('/api/asset/:category/save', async (req, res) => {
const coreData = {};
coreFields.forEach(f => { if (asset[f] !== undefined) coreData[f] = asset[f]; });
const coreKeys = Object.keys(coreData);
const coreSql = `INSERT INTO asset_core (${coreKeys.join(', ')}) VALUES (${coreKeys.map(() => '?').join(', ')}) ON DUPLICATE KEY UPDATE ${coreKeys.map(k => `${k} = VALUES(${k})`).join(', ')}`;
await connection.query(coreSql, Object.values(coreData));
console.log(`[DEBUG] Saving Asset ID: ${asset.id}, Code: ${asset.asset_code}`);
const [existingCore] = await connection.query('SELECT id FROM asset_core WHERE id = ?', [asset.id]);
console.log(`[DEBUG] Existing Core Check for ${asset.id}: Found ${existingCore.length}`);
if (existingCore.length > 0) {
// UPDATE
const updateKeys = coreKeys.filter(k => k !== 'id');
const coreSql = `UPDATE asset_core SET ${updateKeys.map(k => `${k} = ?`).join(', ')} WHERE id = ?`;
const [updRes] = await connection.query(coreSql, [...updateKeys.map(k => coreData[k]), asset.id]);
console.log(`[DEBUG] Core UPDATE result: affectedRows=${updRes.affectedRows}`);
} else {
// INSERT
const coreSql = `INSERT INTO asset_core (${coreKeys.join(', ')}) VALUES (${coreKeys.map(() => '?').join(', ')})`;
const [insRes] = await connection.query(coreSql, Object.values(coreData));
console.log(`[DEBUG] Core INSERT result: affectedRows=${insRes.affectedRows}`);
}
// 3.2 asset_spec
const specFields = ['hw_status', 'model_name', 'mainboard', 'os', 'cpu', 'ram', 'gpu', 'monitoring', 'price', 'monitor_inch', 'serial_num'];