feat: enhance HW modal layout and Server list view columns
- 상세 모달 레이아웃 개선: 모델명과 메인보드 동일 행 배치, 중복 메인보드 필드 제거 - OS 컬럼 스키마 매핑 및 상세 모달 입력 폼 추가 - 모든 하드웨어(서버 포함)에서 HDD 1~4 노출되도록 pc-only 속성 제거 - 서버 리스트 뷰 레이아웃 개선: 자산유형(asset_type) 컬럼 추가 및 너비 조정 - 서버 리스트 모델/메인보드 통합 컬럼 노출 로직 개선 (model_name 우선 표시) - 자산코드 일괄 재부여 스크립트(batch_reformat_codes.js) 추가 및 유니크 제약조건 회피 로직 반영
This commit is contained in:
124
batch_reformat_codes.js
Normal file
124
batch_reformat_codes.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const TYPE_PREFIX_MAP = {
|
||||
'서버': 'SVR', '가상서버(VM)': 'VM', '워크스테이션': 'WKS', '서버PC': 'PC',
|
||||
'개인PC': 'PC', '공용PC': 'PC', '노트북': 'NBK', '태블릿': 'TAB',
|
||||
'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO', '스토리지 렉': 'STO',
|
||||
'스위치': 'NET', '방화벽': 'NET', '공유기': 'NET', '허브': 'NET', '네트워크': 'NET',
|
||||
'모니터': 'MNT', '프린터': 'PRT', '스캐너': 'SCN', '복합기': 'MFP', '빔프로젝터': 'PRJ', '화상회의장비': 'VCF', '업무지원장비': 'EQP',
|
||||
'CPU': 'CPU', 'HDD': 'HDD', 'RAM': 'RAM', 'GPU': 'GPU', 'SSD': 'SSD', '메인보드': 'MBD', '파워서플라이': 'PWR', '쿨러': 'CLR', '케이스': 'CAS', 'PC부품': 'PRT',
|
||||
'드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '공간정보장비': 'SUR',
|
||||
'책상': 'FRN', '의자': 'FRN', '캐비닛': 'FRN', '사무가구': 'FRN',
|
||||
'구독SW': 'SW', '영구SW': 'SW', '외부': 'SW', '내부': 'INT',
|
||||
'선물': 'GFT', 'VIP': 'VIP'
|
||||
};
|
||||
|
||||
function formatPurchaseDate(date) {
|
||||
if (!date) return '000000';
|
||||
let s = String(date).replace(/[^0-9]/g, '');
|
||||
if (s.length >= 6) {
|
||||
return s.substring(0, 6);
|
||||
}
|
||||
return '000000';
|
||||
}
|
||||
|
||||
async function reformatAllCodes() {
|
||||
const connection = await mysql.createConnection({
|
||||
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')
|
||||
});
|
||||
|
||||
try {
|
||||
const tables = [
|
||||
'asset_pc', 'asset_server', 'asset_network', 'asset_storage',
|
||||
'asset_equipment', 'asset_survey', 'asset_pc_parts', 'asset_office_supplies',
|
||||
'asset_sw_external', 'asset_sw_internal', 'asset_vip'
|
||||
];
|
||||
|
||||
let allAssets = [];
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const [rows] = await connection.query(`SELECT * FROM ${table}`);
|
||||
allAssets = allAssets.concat(rows.map(r => ({ ...r, sourceTable: table })));
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_NO_SUCH_TABLE') {
|
||||
console.log(`Skipping missing table: ${table}`);
|
||||
} else {
|
||||
console.error(`Error querying ${table}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Total assets loaded: ${allAssets.length}`);
|
||||
|
||||
// Process each asset
|
||||
const processed = allAssets.map(a => {
|
||||
// 1. Determine prefix
|
||||
let prefix = 'AST';
|
||||
if (a.asset_type && TYPE_PREFIX_MAP[a.asset_type]) {
|
||||
prefix = TYPE_PREFIX_MAP[a.asset_type];
|
||||
} else if (a.category && TYPE_PREFIX_MAP[a.category]) {
|
||||
prefix = TYPE_PREFIX_MAP[a.category];
|
||||
} else if (a.sourceTable === 'asset_sw_external') prefix = 'SW';
|
||||
else if (a.sourceTable === 'asset_sw_internal') prefix = 'INT';
|
||||
|
||||
// 2. Determine YYYYMM
|
||||
const dateStr = a.purchase_date || a.start_date || ''; // start_date for SW
|
||||
const yyyymm = formatPurchaseDate(dateStr);
|
||||
|
||||
return { ...a, prefix, yyyymm };
|
||||
});
|
||||
|
||||
// Group by Prefix
|
||||
const groups = {};
|
||||
processed.forEach(a => {
|
||||
if (!groups[a.prefix]) groups[a.prefix] = [];
|
||||
groups[a.prefix].push(a);
|
||||
});
|
||||
|
||||
// Start renaming
|
||||
for (const prefix in groups) {
|
||||
const items = groups[prefix];
|
||||
|
||||
// Sort logic to maintain some order (by date then id)
|
||||
items.sort((a, b) => {
|
||||
if (a.yyyymm !== b.yyyymm) return a.yyyymm.localeCompare(b.yyyymm);
|
||||
return String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
|
||||
console.log(`Processing group ${prefix}: ${items.length} items`);
|
||||
|
||||
// Temporary rename to avoid UNIQUE constraint conflicts during sequential updates
|
||||
for (const item of items) {
|
||||
const tempCode = `TEMP-${Math.random().toString(36).substring(2, 10)}-${item.id}`;
|
||||
await connection.query(`UPDATE ${item.sourceTable} SET asset_code = ? WHERE id = ?`, [tempCode, item.id]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const serial = String(i + 1).padStart(4, '0'); // SVR-202209-0001
|
||||
|
||||
// Some formats might want 3 or 4 digits. Defaulting to 4.
|
||||
const newCode = `${prefix}-${item.yyyymm}-${serial}`;
|
||||
|
||||
await connection.query(`UPDATE ${item.sourceTable} SET asset_code = ? WHERE id = ?`, [newCode, item.id]);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Asset codes reformatted successfully.');
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Reformatting failed:', err);
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
reformatAllCodes();
|
||||
@@ -107,10 +107,18 @@ const HW_MODAL_HTML = `
|
||||
|
||||
<!-- Group 3: 시스템 사양 -->
|
||||
<div class="form-section-title">시스템 사양</div>
|
||||
<div class="form-group full-width">
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
|
||||
<input type="text" id="hw-model_name" name="model_name" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
|
||||
<input type="text" id="hw-mainboard" name="mainboard" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.OS.ui}</label>
|
||||
<input type="text" id="hw-os" name="os" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.CPU.ui}</label>
|
||||
<input type="text" id="hw-cpu" name="cpu" />
|
||||
@@ -139,18 +147,14 @@ const HW_MODAL_HTML = `
|
||||
<label>HDD 2</label>
|
||||
<input type="text" id="hw-hdd_2" name="hdd_2" />
|
||||
</div>
|
||||
<div class="form-group pc-only">
|
||||
<div class="form-group">
|
||||
<label>HDD 3</label>
|
||||
<input type="text" id="hw-hdd_3" name="hdd_3" />
|
||||
</div>
|
||||
<div class="form-group pc-only">
|
||||
<div class="form-group">
|
||||
<label>HDD 4</label>
|
||||
<input type="text" id="hw-hdd_4" name="hdd_4" />
|
||||
</div>
|
||||
<div class="form-group pc-only">
|
||||
<label>${ASSET_SCHEMA.MAINBOARD.ui}</label>
|
||||
<input type="text" id="hw-mainboard" name="mainboard" />
|
||||
</div>
|
||||
<div class="form-group pc-only">
|
||||
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
|
||||
<input type="text" id="hw-mac_address" name="mac_address" />
|
||||
@@ -425,6 +429,7 @@ function fillHwFormData(asset: any) {
|
||||
setFieldValue('hw-hdd_3', asset.hdd_3 || '');
|
||||
setFieldValue('hw-hdd_4', asset.hdd_4 || '');
|
||||
setFieldValue('hw-mainboard', asset.mainboard || '');
|
||||
setFieldValue('hw-os', asset.os || '');
|
||||
setFieldValue('hw-mac_address', asset.mac_address || '');
|
||||
|
||||
setFieldValue('hw-ip_address', asset.ip_address || '');
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
import { openModal, closeModals } from './BaseModal';
|
||||
import { createIcons, X, Check, Database, Save, FileSpreadsheet, Layers, RefreshCcw } from 'lucide';
|
||||
import { state, loadMasterDataFromDB } from '../../core/state';
|
||||
import { TYPE_PREFIX_MAP } from './SharedData';
|
||||
|
||||
let parsedData: any = null;
|
||||
let currentTab: string = '';
|
||||
let onSuccessCallback: (() => void) | null = null;
|
||||
|
||||
const UPLOAD_PREVIEW_MODAL_HTML = `
|
||||
<div id="upload-preview-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide" style="width: 90vw; max-width: 1400px; height: 85vh; display: flex; flex-direction: column;">
|
||||
<div class="modal-header">
|
||||
<div style="display:flex; align-items:center; gap:0.75rem;">
|
||||
<div style="background:var(--primary-light); padding:0.5rem; border-radius:8px;">
|
||||
<i data-lucide="file-spreadsheet" style="width:20px; height:20px; color:var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="upload-preview-title">데이터 업로드 검토</h2>
|
||||
<p style="font-size:12px; color:var(--text-muted); margin-top:2px;">업로드 전 데이터를 확인하고 수정 사항이 있는지 검토하세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-close-upload-preview" class="btn-icon"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" style="display:flex; padding:0; overflow:hidden; flex: 1;">
|
||||
<!-- Sidebar for Tabs -->
|
||||
<div id="upload-tab-sidebar" style="width:240px; border-right:1px solid var(--border-color); background:#fafafa; padding:1.5rem 1rem; overflow-y:auto; flex-shrink: 0;">
|
||||
<div style="font-size:11px; font-weight:700; color:var(--text-muted); text-transform:uppercase; margin-bottom:1rem; letter-spacing:0.05em;">데이터 카테고리</div>
|
||||
<div id="upload-tabs-container" style="display:flex; flex-direction:column; gap:0.5rem;">
|
||||
<!-- Tabs will be injected here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div style="flex:1; display:flex; flex-direction:column; background:white; overflow:hidden;">
|
||||
<div id="upload-preview-stats" style="padding:1rem 1.5rem; border-bottom:1px solid var(--border-color); display:flex; justify-content:space-between; align-items:center; background:white;">
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<span id="current-tab-name" style="font-weight:700; font-size:16px;">선택된 탭 없음</span>
|
||||
<span id="current-tab-count" class="badge badge-primary">0건</span>
|
||||
<button id="btn-bulk-generate-codes" class="btn btn-outline btn-sm hidden" style="margin-left:1rem; height:28px; font-size:12px; padding:0 0.75rem;">
|
||||
<i data-lucide="refresh-ccw" style="width:14px; height:14px; margin-right:4px;"></i> 자산코드 일괄 생성
|
||||
</button>
|
||||
</div>
|
||||
<div style="font-size:12px; color:var(--text-muted);">
|
||||
* 아래 데이터가 신규로 추가되거나 기존 데이터가 갱신됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="upload-preview-table-wrapper" style="flex:1; overflow:auto; padding:0;">
|
||||
<!-- Table will be injected here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" style="background:#f9fafb; border-top:1px solid var(--border-color); flex-shrink: 0;">
|
||||
<div style="display:flex; gap:0.75rem; width:100%; justify-content:flex-end;">
|
||||
<button id="btn-cancel-upload" class="btn btn-outline" style="height:40px; padding:0 1.5rem;">취소하기</button>
|
||||
<button id="btn-confirm-upload" class="btn btn-primary" style="height:40px; padding:0 2rem;">
|
||||
<i data-lucide="save"></i> 최종 데이터 저장하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export function initUploadPreviewModal(onSuccess?: () => void) {
|
||||
if (onSuccess) onSuccessCallback = onSuccess;
|
||||
if (!document.getElementById('upload-preview-modal')) {
|
||||
document.body.insertAdjacentHTML('beforeend', UPLOAD_PREVIEW_MODAL_HTML);
|
||||
}
|
||||
|
||||
document.getElementById('btn-close-upload-preview')?.addEventListener('click', closeModals);
|
||||
document.getElementById('btn-cancel-upload')?.addEventListener('click', closeModals);
|
||||
document.getElementById('btn-confirm-upload')?.addEventListener('click', () => {
|
||||
confirmUpload();
|
||||
});
|
||||
document.getElementById('btn-bulk-generate-codes')?.addEventListener('click', () => {
|
||||
generateBulkCodes();
|
||||
});
|
||||
}
|
||||
|
||||
export function openUploadPreview(data: any) {
|
||||
parsedData = data;
|
||||
const tabNames = Object.keys(data);
|
||||
if (tabNames.length === 0) {
|
||||
alert('업로드할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
currentTab = tabNames[0];
|
||||
renderTabs();
|
||||
renderCurrentTable();
|
||||
|
||||
openModal('upload-preview-modal');
|
||||
createIcons({ icons: { X, Check, Database, Save, FileSpreadsheet, Layers, RefreshCcw } });
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const container = document.getElementById('upload-tabs-container');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
Object.keys(parsedData).forEach(tab => {
|
||||
const btn = document.createElement('div');
|
||||
btn.className = `upload-tab-btn ${tab === currentTab ? 'active' : ''}`;
|
||||
btn.style.cssText = `
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
background: ${tab === currentTab ? 'white' : 'transparent'};
|
||||
color: ${tab === currentTab ? 'var(--primary-color)' : 'var(--text-main)'};
|
||||
box-shadow: ${tab === currentTab ? '0 2px 4px rgba(0,0,0,0.05)' : 'none'};
|
||||
border: 1px solid ${tab === currentTab ? 'var(--border-color)' : 'transparent'};
|
||||
`;
|
||||
|
||||
btn.innerHTML = `
|
||||
<span>${tab}</span>
|
||||
<span style="font-size:11px; opacity:0.6;">${parsedData[tab].length}</span>
|
||||
`;
|
||||
|
||||
btn.onclick = () => {
|
||||
currentTab = tab;
|
||||
renderTabs();
|
||||
renderCurrentTable();
|
||||
};
|
||||
container.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
function renderCurrentTable() {
|
||||
const tableWrapper = document.getElementById('upload-preview-table-wrapper');
|
||||
const tabNameEl = document.getElementById('current-tab-name');
|
||||
const tabCountEl = document.getElementById('current-tab-count');
|
||||
if (!tableWrapper || !tabNameEl || !tabCountEl) return;
|
||||
|
||||
const data = parsedData[currentTab];
|
||||
tabNameEl.textContent = currentTab;
|
||||
tabCountEl.textContent = `${data.length}건`;
|
||||
|
||||
const generateBtn = document.getElementById('btn-bulk-generate-codes');
|
||||
const isHwTab = ['개인PC', '서버', '스토리지', '전산비품', '모바일기기'].includes(currentTab);
|
||||
if (generateBtn) {
|
||||
if (isHwTab) generateBtn.classList.remove('hidden');
|
||||
else generateBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
tableWrapper.innerHTML = '<div style="padding:4rem; text-align:center; color:var(--text-muted);">표시할 데이터가 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get headers from first item keys, excluding 'id' and 'type' for cleaner view
|
||||
const headers = Object.keys(data[0]).filter(k => k !== 'id' && k !== 'type');
|
||||
|
||||
let tableHTML = `
|
||||
<table class="preview-table" style="width:100%; border-collapse:collapse; min-width:max-content;">
|
||||
<thead style="position:sticky; top:0; z-index:10; background:#f8fafc; box-shadow:0 1px 0 var(--border-color);">
|
||||
<tr>
|
||||
<th style="padding:0.75rem 1rem; text-align:center; font-size:12px; border-bottom:1px solid var(--border-color); width:50px;">No.</th>
|
||||
${headers.map(h => `<th style="padding:0.75rem 1rem; text-align:left; font-size:12px; border-bottom:1px solid var(--border-color); color:var(--text-muted);">${h}</th>`).join('')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${data.map((row: any, idx: number) => `
|
||||
<tr style="border-bottom:1px solid #f1f5f9;">
|
||||
<td style="padding:0.75rem 1rem; text-align:center; font-size:13px; color:var(--text-muted);">${idx + 1}</td>
|
||||
${headers.map(h => `<td style="padding:0.75rem 1rem; font-size:13px;">${row[h] || '-'}</td>`).join('')}
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
tableWrapper.innerHTML = tableHTML;
|
||||
}
|
||||
|
||||
async function confirmUpload() {
|
||||
const confirmBtn = document.getElementById('btn-confirm-upload') as HTMLButtonElement;
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.innerHTML = '<i data-lucide="loader-2" class="animate-spin"></i> 저장 중...';
|
||||
createIcons({ icons: { Save } });
|
||||
}
|
||||
|
||||
try {
|
||||
const tabNames = Object.keys(parsedData);
|
||||
let successCount = 0;
|
||||
|
||||
for (const tab of tabNames) {
|
||||
const data = parsedData[tab];
|
||||
let endpoint = '';
|
||||
|
||||
const API_BASE = `http://${location.hostname}:3000`;
|
||||
if (tab === '개인PC') endpoint = `${API_BASE}/api/pc/batch`;
|
||||
else if (tab === '서버') endpoint = `${API_BASE}/api/server/batch`;
|
||||
else if (tab === '스토리지') endpoint = `${API_BASE}/api/storage/batch`;
|
||||
else if (tab === '전산비품') endpoint = `${API_BASE}/api/equip/batch`;
|
||||
else if (tab === '모바일기기') endpoint = `${API_BASE}/api/mobile/batch`;
|
||||
else if (tab === '구독SW') endpoint = `${API_BASE}/api/sw/sub/batch`;
|
||||
else if (tab === '영구SW') endpoint = `${API_BASE}/api/sw/perm/batch`;
|
||||
else if (tab === '클라우드') endpoint = `${API_BASE}/api/cloud/batch`;
|
||||
else if (tab === '도메인') endpoint = `${API_BASE}/api/ops/domain/batch`;
|
||||
|
||||
if (endpoint) {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (response.ok) {
|
||||
successCount++;
|
||||
} else {
|
||||
const errRes = await response.json();
|
||||
throw new Error(`[${tab}] ${errRes.error || '저장 실패'}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert(`카테고리 '${tab}' 저장 중 오류: ${e.message}`);
|
||||
throw e; // Stop processing further tabs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
if (onSuccessCallback) onSuccessCallback();
|
||||
closeModals();
|
||||
alert(`${successCount}개 카테고리의 데이터가 성공적으로 업로드되었습니다.`);
|
||||
} else {
|
||||
alert('데이터 업로드에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// 상세 에러는 내부 catch에서 이미 alert으로 띄움
|
||||
} finally {
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.innerHTML = '<i data-lucide="save"></i> 최종 데이터 저장하기';
|
||||
createIcons({ icons: { Save } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateBulkCodes() {
|
||||
const data = parsedData[currentTab];
|
||||
if (!data) return;
|
||||
|
||||
const generateBtn = document.getElementById('btn-bulk-generate-codes') as HTMLButtonElement;
|
||||
if (generateBtn) {
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.innerHTML = '<i data-lucide="refresh-ccw" class="animate-spin"></i> 생성 중...';
|
||||
createIcons({ icons: { RefreshCcw } });
|
||||
}
|
||||
|
||||
try {
|
||||
// Group rows by prefix (type + purchase_ym)
|
||||
const rowsToProcess = data.filter((r: any) => !r.자산코드);
|
||||
if (rowsToProcess.length === 0) {
|
||||
alert('이미 모든 항목에 자산코드가 부여되어 있습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const groups: Record<string, any[]> = {};
|
||||
rowsToProcess.forEach((r: any) => {
|
||||
const type = r.비품유형 || r.기기유형 || r.type || 'ETC';
|
||||
const typeCode = TYPE_PREFIX_MAP[type] || 'ETC';
|
||||
const purchaseYM = String(r.구매연월 || '').replace(/[^0-9]/g, '');
|
||||
if (purchaseYM.length < 6) {
|
||||
// Fallback or skip
|
||||
return;
|
||||
}
|
||||
const prefix = `${typeCode}-${purchaseYM.substring(0, 6)}-`;
|
||||
if (!groups[prefix]) groups[prefix] = [];
|
||||
groups[prefix].push(r);
|
||||
});
|
||||
|
||||
for (const prefix in groups) {
|
||||
const rows = groups[prefix];
|
||||
// Fetch current next code for this prefix
|
||||
const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}`);
|
||||
const result = await res.json();
|
||||
if (result.nextCode) {
|
||||
let baseNum = parseInt(result.nextCode.replace(prefix, ''));
|
||||
rows.forEach((r, idx) => {
|
||||
r.자산코드 = `${prefix}${(baseNum + idx).toString().padStart(4, '0')}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderCurrentTable();
|
||||
alert(`${rowsToProcess.length}건의 자산코드가 생성되었습니다.`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('자산코드 생성 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
if (generateBtn) {
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.innerHTML = '<i data-lucide="refresh-ccw"></i> 자산코드 일괄 생성';
|
||||
createIcons({ icons: { RefreshCcw } });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export const ASSET_SCHEMA = {
|
||||
HDD3: { key: 'hdd_3', db: 'hdd_3', ui: 'HDD3' },
|
||||
HDD4: { key: 'hdd_4', db: 'hdd_4', ui: 'HDD4' },
|
||||
MAINBOARD: { key: 'mainboard', db: 'mainboard', ui: '메인보드' },
|
||||
OS: { key: 'os', db: 'os', ui: 'OS' },
|
||||
IP_ADDR: { key: 'ip_address', db: 'ip_address', ui: 'IP 주소' },
|
||||
IP_ADDR2: { key: 'ip_address_2', db: 'ip_address_2', ui: 'IP 주소 2' },
|
||||
MAC_ADDR: { key: 'mac_address', db: 'mac_address', ui: 'MAC 주소' },
|
||||
|
||||
@@ -28,10 +28,11 @@ export function renderServerList(container: HTMLElement) {
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" data-sort="${ASSET_SCHEMA.CURRENT_DEPT.key}">${ASSET_SCHEMA.CURRENT_DEPT.ui}</th>
|
||||
<th data-sort="${ASSET_SCHEMA.ASSET_PURPOSE.key}">${ASSET_SCHEMA.ASSET_PURPOSE.ui}</th>
|
||||
<th style="width: 15%;" data-sort="${ASSET_SCHEMA.MODEL_NAME.key}">${ASSET_SCHEMA.MODEL_NAME.ui}</th>
|
||||
<th style="width: 15%;" data-sort="${ASSET_SCHEMA.ASSET_PURPOSE.key}">${ASSET_SCHEMA.ASSET_PURPOSE.ui}</th>
|
||||
<th class="text-center" style="width: 10%;" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">${ASSET_SCHEMA.ASSET_TYPE.ui}</th>
|
||||
<th style="width: 15%;">모델/메인보드</th>
|
||||
<th class="text-center" data-sort="${ASSET_SCHEMA.LOCATION.key}">${ASSET_SCHEMA.LOCATION.ui}</th>
|
||||
<th class="col-memo" style="width: 40%;" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
|
||||
<th class="col-memo" style="width: 35%;" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dynamic-tbody"></tbody>
|
||||
@@ -50,7 +51,7 @@ export function renderServerList(container: HTMLElement) {
|
||||
|
||||
tbody.innerHTML = '';
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="5" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="6" class="text-center" style="padding: 3rem; color: var(--text-muted);">${UI_TEXT.MESSAGES.NO_DATA}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,10 +63,16 @@ export function renderServerList(container: HTMLElement) {
|
||||
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
|
||||
|
||||
const modelOrMainboard = asset[ASSET_SCHEMA.MODEL_NAME.key]
|
||||
|| asset[ASSET_SCHEMA.ASSET_NAME.key]
|
||||
|| asset[ASSET_SCHEMA.MAINBOARD.key]
|
||||
|| '-';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="text-center">${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'-'}</td>
|
||||
<td>${formatInline(asset[ASSET_SCHEMA.ASSET_PURPOSE.key]||'-')}</td>
|
||||
<td>${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key]||asset[ASSET_SCHEMA.ASSET_NAME.key]||'-')}</td>
|
||||
<td class="text-center">${asset[ASSET_SCHEMA.ASSET_TYPE.key]||'-'}</td>
|
||||
<td>${formatInline(modelOrMainboard)}</td>
|
||||
<td class="text-center">${displayLoc}</td>
|
||||
<td class="col-memo">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
|
||||
`;
|
||||
|
||||
BIN
temp_db.xlsx
BIN
temp_db.xlsx
Binary file not shown.
Reference in New Issue
Block a user