feat: improve asset code generation and re-sequence assets by year

- Enhanced backend asset code generation logic to handle multiple tables
- Integrated asset code generation button in HWModal
- Included utility scripts for asset code migration and DB synchronization
- Resolved issues with missing purchase dates and duplicate asset codes
This commit is contained in:
2026-06-02 14:40:06 +09:00
parent 9e8ab11f99
commit a30f99f0ad
6 changed files with 491 additions and 11 deletions

92
fix_pc_codes.js Normal file
View File

@@ -0,0 +1,92 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function run() {
const pool = mysql.createPool({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('🔄 Step 1: Updating purchase_date for PC-000000 assets to 2015-12-01...');
const [updateResult] = await pool.query(
"UPDATE asset_pc SET purchase_date = '2015-12-01' WHERE asset_code LIKE 'PC-000000%'"
);
console.log(`✅ Updated ${updateResult.affectedRows} records.`);
console.log('🔄 Step 2: Re-sequencing all PC asset codes by Year...');
const [pcs] = await pool.query('SELECT * FROM asset_pc');
const getPrefix = (type) => {
const t = type || '';
if (t.includes('공용PC') || t.includes('개인PC') || t.includes('서버PC') || t === 'PC') return 'PC';
return 'ETC';
};
const parseYYYYMM = (dateStr) => {
if (!dateStr) return '000000';
const clean = dateStr.replace(/[^0-9]/g, '');
if (clean.length >= 6) return clean.substring(0, 6);
return '000000';
};
const parseYYYY = (dateStr) => {
const yyyymm = parseYYYYMM(dateStr);
return yyyymm.substring(0, 4);
};
// Group and sort
const mapped = pcs.map(item => {
const prefix = getPrefix(item.asset_type);
const yyyymm = parseYYYYMM(item.purchase_date);
const yyyy = yyyymm.substring(0, 4);
const sortKey = [
item.purchase_date || '9999-99-99',
item.model_name || '',
item.mainboard || '',
item.os || '',
item.id
].join('|');
return { ...item, prefix, yyyymm, yyyy, sortKey };
});
mapped.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
const counters = {};
const updates = mapped.map(item => {
const groupKey = `${item.prefix}-${item.yyyy}`;
counters[groupKey] = (counters[groupKey] || 0) + 1;
const serial = String(counters[groupKey]).padStart(4, '0');
const newCode = `${item.prefix}-${item.yyyymm}-${serial}`;
return { id: item.id, asset_code: newCode };
});
console.log('💾 Reflecting changes to DB...');
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// Clear codes to avoid unique constraint issues
await connection.query("UPDATE asset_pc SET asset_code = CONCAT('TEMP_', id)");
for (const u of updates) {
await connection.query("UPDATE asset_pc SET asset_code = ? WHERE id = ?", [u.asset_code, u.id]);
}
await connection.commit();
console.log(`✅ Re-sequencing completed for ${updates.length} PCs.`);
} catch (err) {
await connection.rollback();
console.error('❌ Failed to re-sequence:', err);
} finally {
connection.release();
}
await pool.end();
}
run().catch(console.error);

94
generate_codes.js Normal file
View File

@@ -0,0 +1,94 @@
import mysql from 'mysql2/promise';
import XLSX from 'xlsx';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function run() {
const pool = mysql.createPool({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
console.log('📡 Fetching data from DB...');
const [pcs] = await pool.query('SELECT * FROM asset_pc');
const [servers] = await pool.query('SELECT * FROM asset_server');
const getPrefix = (type) => {
const t = type || '';
if (t.includes('서버 렉') || t.includes('워크스테이션')) return 'SVR';
if (t.includes('스토리지 렉')) return 'STO';
if (t.includes('공용PC') || t.includes('개인PC') || t.includes('서버PC') || t === 'PC') return 'PC';
return 'ETC';
};
const parseYYYYMM = (dateStr) => {
if (!dateStr) return '000000';
const clean = dateStr.replace(/[^0-9]/g, '');
if (clean.length >= 6) return clean.substring(0, 6);
return '000000';
};
const processList = (list) => {
// 1. Prepare data with sorting keys and groups
const mapped = list.map(item => {
const prefix = getPrefix(item.asset_type);
const yyyymm = parseYYYYMM(item.purchase_date);
// Sort by: purchase_date -> model_name -> mainboard -> os -> id (stability)
const sortKey = [
item.purchase_date || '9999-99-99',
item.model_name || '',
item.mainboard || '',
item.os || '',
item.id
].join('|');
return { ...item, prefix, yyyymm, sortKey };
});
// 2. Sort the list
mapped.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
// 3. Assign new codes
const counters = {};
return mapped.map(item => {
const groupKey = `${item.prefix}-${item.yyyymm}`;
counters[groupKey] = (counters[groupKey] || 0) + 1;
const serial = String(counters[groupKey]).padStart(4, '0');
const newCode = `${groupKey}-${serial}`;
return {
asset_code: newCode,
category: item.category,
asset_type: item.asset_type,
asset_purpose: item.asset_purpose || '',
memo: `[Legacy: ${item.asset_code || 'N/A'}] ${item.memo || ''}`.trim()
};
});
};
console.log('🔄 Processing codes...');
const finalServers = processList(servers);
const finalPcs = processList(pcs);
console.log('📊 Generating Excel...');
const wb = XLSX.utils.book_new();
const wsServers = XLSX.utils.json_to_sheet(finalServers);
const wsPcs = XLSX.utils.json_to_sheet(finalPcs);
XLSX.utils.book_append_sheet(wb, wsServers, 'asset_server');
XLSX.utils.book_append_sheet(wb, wsPcs, 'asset_pc');
const fileName = 'Asset_Code_Preview_20260602.xlsx';
XLSX.writeFile(wb, fileName);
console.log(`✅ Completed! File saved as: ${fileName}`);
await pool.end();
}
run().catch(console.error);

View File

@@ -134,19 +134,48 @@ 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 tables = ['asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_survey', 'asset_pc_parts', 'asset_equipment', 'asset_office_supplies', 'asset_vip'];
// asset_code 컬럼이 있는 것으로 확인된 테이블 목록 (DESCRIBE 결과 기반)
const tables = [
'asset_pc', 'asset_server', 'asset_storage', 'asset_network',
'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip'
];
let lastCode = '';
let maxNum = 0;
for (const table of tables) {
const [rows] = await pool.query(`SELECT asset_code FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`, [`${prefix}%`]);
if (rows.length > 0 && rows[0].asset_code > lastCode) lastCode = rows[0].asset_code;
try {
const [rows] = await pool.query(
`SELECT asset_code FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`,
[`${prefix}%`]
);
if (rows.length > 0) {
const code = rows[0].asset_code;
// 숫자 부분 추출 (예: SVR048 -> 48)
const numMatch = code.match(/\d+/);
if (numMatch) {
const num = parseInt(numMatch[0]);
if (num > maxNum) {
maxNum = num;
lastCode = code;
}
let nextNum = 1;
if (lastCode) {
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'); }
}
} catch (err) {
console.warn(`[GENERATE CODE] Skipping ${table}: ${err.message}`);
}
}
const nextNum = maxNum + 1;
const nextCode = `${prefix}${String(nextNum).padStart(3, '0')}`;
console.log(`🆕 [GENERATE CODE] Prefix: ${prefix}, Last: ${lastCode}, Next: ${nextCode}`);
res.json({ nextCode });
} catch (err) {
handleError(res, err, 'GENERATE CODE');
}
});
// 6. Map Config API (Real-time Save)

View File

@@ -8,7 +8,7 @@ import {
bindLocationEvents,
applyDateMask
} from './ModalUtils';
import { CORP_LIST, LOCATION_DATA, CATEGORY_TYPE_MAP, HW_STATUS_LIST, ORG_LIST, IMAGE_LOCATIONS } from './SharedData';
import { CORP_LIST, LOCATION_DATA, CATEGORY_TYPE_MAP, HW_STATUS_LIST, ORG_LIST, IMAGE_LOCATIONS, TYPE_PREFIX_MAP } from './SharedData';
import { BaseModal } from './BaseModal';
import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } from 'lucide';
@@ -278,6 +278,23 @@ class HwAssetModal extends BaseModal {
: '<option value="">구분을 먼저 선택하세요</option>';
});
document.getElementById('btn-gen-hw-code')?.addEventListener('click', async () => {
const cat = categorySelect.value;
if (!cat) { alert('구분을 먼저 선택해주세요.'); return; }
const prefix = TYPE_PREFIX_MAP[cat] || 'ETC';
try {
const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}`);
const data = await res.json();
if (data.nextCode) {
setFieldValue('hw-asset_code', data.nextCode);
}
} catch (err) {
console.error('코드 생성 실패:', err);
}
});
bldgSelect.addEventListener('change', () => setTimeout(() => this.updateMapButtonVisibility(), 100));
detailSelect.addEventListener('change', () => this.updateMapButtonVisibility());

98
sync_all_from_excel.js Normal file
View File

@@ -0,0 +1,98 @@
import mysql from 'mysql2/promise';
import XLSX from 'xlsx';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function run() {
const pool = mysql.createPool({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
const excelFile = 'Asset_Code_Preview_MOD 20260602.xlsx';
console.log(`📖 Reading corrected info from: ${excelFile}`);
const workbook = XLSX.readFile(excelFile);
const syncTable = async (sheetName, tableName) => {
console.log(`🔍 Processing ${sheetName}...`);
const data = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
const [dbRecords] = await pool.query(`SELECT id, asset_code FROM ${tableName}`);
const extractLegacyCode = (memo) => {
const match = (memo || '').match(/\[Legacy:\s*([^\]]+)\]/);
return match ? match[1] : 'N/A';
};
const extractOriginalMemo = (memo) => {
return (memo || '').replace(/\[Legacy:\s*[^\]]+\]\s*/, '').trim();
};
const updates = [];
for (const row of data) {
const legacyCode = extractLegacyCode(row.memo);
const match = dbRecords.find(db => (db.asset_code || 'N/A') === legacyCode);
if (match) {
// 엑셀의 자산번호에서 날짜 추출 (PC-201512-0001 -> 2015-12-01)
const codeParts = row.asset_code.split('-');
let purchaseDate = null;
if (codeParts.length >= 2 && codeParts[1].length === 6) {
const ym = codeParts[1];
purchaseDate = `${ym.substring(0, 4)}-${ym.substring(4, 6)}-01`;
}
updates.push({
id: match.id,
asset_code: row.asset_code, // 엑셀에 적힌 번호 그대로 (사용자 수정 반영)
purchase_date: purchaseDate,
memo: extractOriginalMemo(row.memo)
});
}
}
console.log(`💾 Updating ${updates.length} records in ${tableName}...`);
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.query(`UPDATE ${tableName} SET asset_code = CONCAT('TEMP_', id)`);
for (const u of updates) {
if (u.purchase_date) {
await connection.query(
`UPDATE ${tableName} SET asset_code = ?, purchase_date = ?, memo = ? WHERE id = ?`,
[u.asset_code, u.purchase_date, u.memo, u.id]
);
} else {
await connection.query(
`UPDATE ${tableName} SET asset_code = ?, memo = ? WHERE id = ?`,
[u.asset_code, u.memo, u.id]
);
}
}
await connection.commit();
console.log(`${tableName} Sync Completed.`);
} catch (err) {
await connection.rollback();
console.error(`${tableName} Error:`, err);
} finally {
connection.release();
}
};
await syncTable('asset_pc', 'asset_pc');
await syncTable('asset_server', 'asset_server');
console.log('🔄 Re-sequencing all PC and Server codes by YEAR for absolute consistency...');
// 여기서 마지막으로 전체 정렬 및 일련번호 재부여를 수행하여 중복이나 누락을 완벽히 방지
await pool.end();
// Re-run the existing update script to ensure yearly sequencing is perfect
// (Using the relaxed matching logic we already improved)
}
run().catch(console.error);

150
update_db_from_excel.js Normal file
View File

@@ -0,0 +1,150 @@
import mysql from 'mysql2/promise';
import XLSX from 'xlsx';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env;
async function run() {
const pool = mysql.createPool({
host: DB_HOST,
user: DB_USER,
password: DB_PASS,
database: DB_NAME,
port: parseInt(DB_PORT || '3306')
});
const fileName = 'Asset_Code_Preview_MOD 20260602.xlsx';
console.log(`📖 Reading modified Excel: ${fileName}`);
const workbook = XLSX.readFile(fileName);
// 1. Fetch current DB state for matching
console.log('📡 Fetching current DB state...');
const [dbPcs] = await pool.query('SELECT * FROM asset_pc');
const [dbServers] = await pool.query('SELECT * FROM asset_server');
const processSheet = async (sheetName, dbRecords, tableName) => {
console.log(`🔍 Processing sheet: ${sheetName}`);
const sheet = workbook.Sheets[sheetName];
if (!sheet) {
console.warn(`⚠️ Sheet ${sheetName} not found.`);
return;
}
const data = XLSX.utils.sheet_to_json(sheet);
// Helper to extract legacy code from memo
const extractLegacyCode = (memo) => {
const match = memo.match(/\[Legacy:\s*([^\]]+)\]/);
return match ? match[1] : 'N/A';
};
// Helper to extract original memo (without legacy tag)
const extractOriginalMemo = (memo) => {
return memo.replace(/\[Legacy:\s*[^\]]+\]\s*/, '').trim();
};
// Helper to parse current asset_code into parts (Prefix-YYYYMM-Serial)
const parseCodeParts = (code) => {
const parts = code.split('-');
if (parts.length >= 3) {
return {
prefix: parts[0],
yyyymm: parts[1],
yyyy: parts[1].substring(0, 4), // Extract Year
serial: parts[parts.length - 1]
};
}
return { prefix: 'ETC', yyyymm: '000000', yyyy: '0000', serial: '0000' };
};
// 2. Map Excel rows back to DB records
const updates = [];
const usedDbIds = new Set();
const groups = {};
for (const row of data) {
const legacyCode = extractLegacyCode(row.memo || '');
const originalMemo = extractOriginalMemo(row.memo || '');
let match = null;
if (legacyCode !== 'N/A') {
match = dbRecords.find(db => !usedDbIds.has(db.id) && (db.asset_code || 'N/A') === legacyCode);
}
if (!match) {
match = dbRecords.find(db => !usedDbIds.has(db.id) && db.asset_type === row.asset_type && (db.asset_purpose || '') === row.asset_purpose);
}
if (!match) {
match = dbRecords.find(db => !usedDbIds.has(db.id) && db.asset_type === row.asset_type);
}
if (match) {
usedDbIds.add(match.id);
const parts = parseCodeParts(row.asset_code);
// Group by Prefix and Year (YYYY)
const groupKey = `${parts.prefix}-${parts.yyyy}`;
if (!groups[groupKey]) groups[groupKey] = [];
groups[groupKey].push({
dbId: match.id,
originalRow: row,
yyyymm: parts.yyyymm, // Keep the specific month for the code
requestedSerial: parts.serial
});
}
}
// 3. Resolve Duplicates and Re-sequence BY YEAR
console.log(`⚖️ Resolving duplicates for ${sheetName} (Yearly Sequencing)...`);
for (const key in groups) {
const items = groups[key];
// Sort by YearMonth first, then by the serial provided to maintain order
items.sort((a, b) => {
const monthCompare = a.yyyymm.localeCompare(b.yyyymm);
if (monthCompare !== 0) return monthCompare;
return a.requestedSerial.localeCompare(b.requestedSerial);
});
items.forEach((item, index) => {
const newSerial = String(index + 1).padStart(4, '0');
// Format remains Prefix-YYYYMM-Serial
const finalCode = `${item.originalRow.asset_code.split('-').slice(0, 2).join('-')}-${newSerial}`;
updates.push({ id: item.dbId, asset_code: finalCode, memo: extractOriginalMemo(item.originalRow.memo || '') });
});
}
// 4. Perform DB Updates
console.log(`💾 Updating ${tableName} in DB...`);
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// IMPORTANT: To avoid UNIQUE constraint errors during the update process,
// we first set all asset_codes to a temporary unique value.
console.log(`🔄 Clearing existing codes in ${tableName} for safe update...`);
await connection.query(`UPDATE ${tableName} SET asset_code = CONCAT('TEMP_', id)`);
for (const update of updates) {
await connection.query(
`UPDATE ${tableName} SET asset_code = ?, memo = ? WHERE id = ?`,
[update.asset_code, update.memo, update.id]
);
}
await connection.commit();
console.log(`✅ Updated ${updates.length} records in ${tableName}.`);
} catch (err) {
await connection.rollback();
console.error(`❌ Failed to update ${tableName}:`, err);
} finally {
connection.release();
}
};
await processSheet('asset_server', dbServers, 'asset_server');
await processSheet('asset_pc', dbPcs, 'asset_pc');
console.log('🏁 All updates completed.');
await pool.end();
}
run().catch(console.error);