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: 시스템 사양 -->
|
<!-- Group 3: 시스템 사양 -->
|
||||||
<div class="form-section-title">시스템 사양</div>
|
<div class="form-section-title">시스템 사양</div>
|
||||||
<div class="form-group full-width">
|
<div class="form-group">
|
||||||
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
|
<label>${ASSET_SCHEMA.MODEL_NAME.ui}</label>
|
||||||
<input type="text" id="hw-model_name" name="model_name" />
|
<input type="text" id="hw-model_name" name="model_name" />
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label>${ASSET_SCHEMA.CPU.ui}</label>
|
<label>${ASSET_SCHEMA.CPU.ui}</label>
|
||||||
<input type="text" id="hw-cpu" name="cpu" />
|
<input type="text" id="hw-cpu" name="cpu" />
|
||||||
@@ -139,18 +147,14 @@ const HW_MODAL_HTML = `
|
|||||||
<label>HDD 2</label>
|
<label>HDD 2</label>
|
||||||
<input type="text" id="hw-hdd_2" name="hdd_2" />
|
<input type="text" id="hw-hdd_2" name="hdd_2" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group pc-only">
|
<div class="form-group">
|
||||||
<label>HDD 3</label>
|
<label>HDD 3</label>
|
||||||
<input type="text" id="hw-hdd_3" name="hdd_3" />
|
<input type="text" id="hw-hdd_3" name="hdd_3" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group pc-only">
|
<div class="form-group">
|
||||||
<label>HDD 4</label>
|
<label>HDD 4</label>
|
||||||
<input type="text" id="hw-hdd_4" name="hdd_4" />
|
<input type="text" id="hw-hdd_4" name="hdd_4" />
|
||||||
</div>
|
</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">
|
<div class="form-group pc-only">
|
||||||
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
|
<label>${ASSET_SCHEMA.MAC_ADDR.ui}</label>
|
||||||
<input type="text" id="hw-mac_address" name="mac_address" />
|
<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_3', asset.hdd_3 || '');
|
||||||
setFieldValue('hw-hdd_4', asset.hdd_4 || '');
|
setFieldValue('hw-hdd_4', asset.hdd_4 || '');
|
||||||
setFieldValue('hw-mainboard', asset.mainboard || '');
|
setFieldValue('hw-mainboard', asset.mainboard || '');
|
||||||
|
setFieldValue('hw-os', asset.os || '');
|
||||||
setFieldValue('hw-mac_address', asset.mac_address || '');
|
setFieldValue('hw-mac_address', asset.mac_address || '');
|
||||||
|
|
||||||
setFieldValue('hw-ip_address', asset.ip_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' },
|
HDD3: { key: 'hdd_3', db: 'hdd_3', ui: 'HDD3' },
|
||||||
HDD4: { key: 'hdd_4', db: 'hdd_4', ui: 'HDD4' },
|
HDD4: { key: 'hdd_4', db: 'hdd_4', ui: 'HDD4' },
|
||||||
MAINBOARD: { key: 'mainboard', db: 'mainboard', ui: '메인보드' },
|
MAINBOARD: { key: 'mainboard', db: 'mainboard', ui: '메인보드' },
|
||||||
|
OS: { key: 'os', db: 'os', ui: 'OS' },
|
||||||
IP_ADDR: { key: 'ip_address', db: 'ip_address', ui: 'IP 주소' },
|
IP_ADDR: { key: 'ip_address', db: 'ip_address', ui: 'IP 주소' },
|
||||||
IP_ADDR2: { key: 'ip_address_2', db: 'ip_address_2', ui: 'IP 주소 2' },
|
IP_ADDR2: { key: 'ip_address_2', db: 'ip_address_2', ui: 'IP 주소 2' },
|
||||||
MAC_ADDR: { key: 'mac_address', db: 'mac_address', ui: 'MAC 주소' },
|
MAC_ADDR: { key: 'mac_address', db: 'mac_address', ui: 'MAC 주소' },
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ export function renderServerList(container: HTMLElement) {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center" data-sort="${ASSET_SCHEMA.CURRENT_DEPT.key}">${ASSET_SCHEMA.CURRENT_DEPT.ui}</th>
|
<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.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 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="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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="dynamic-tbody"></tbody>
|
<tbody id="dynamic-tbody"></tbody>
|
||||||
@@ -50,7 +51,7 @@ export function renderServerList(container: HTMLElement) {
|
|||||||
|
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
if (filtered.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +63,16 @@ export function renderServerList(container: HTMLElement) {
|
|||||||
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
const detail = asset[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||||
const displayLoc = detail ? `${loc}(${detail})` : (loc || '-');
|
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 = `
|
tr.innerHTML = `
|
||||||
<td class="text-center">${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'-'}</td>
|
<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.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="text-center">${displayLoc}</td>
|
||||||
<td class="col-memo">${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</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