diff --git a/batch_reformat_codes.js b/batch_reformat_codes.js new file mode 100644 index 0000000..d0ba02f --- /dev/null +++ b/batch_reformat_codes.js @@ -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(); diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index babf962..207406f 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -107,10 +107,18 @@ const HW_MODAL_HTML = `
시스템 사양
-
+
+
+ + +
+
+ + +
@@ -139,18 +147,14 @@ const HW_MODAL_HTML = `
-
+
-
+
-
- - -
@@ -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 || ''); diff --git a/src/components/Modal/UploadPreviewModal.ts b/src/components/Modal/UploadPreviewModal.ts deleted file mode 100644 index d83c17a..0000000 --- a/src/components/Modal/UploadPreviewModal.ts +++ /dev/null @@ -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 = ` - -`; - -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 = ` - ${tab} - ${parsedData[tab].length} - `; - - 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 = '
표시할 데이터가 없습니다.
'; - 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 = ` - - - - - ${headers.map(h => ``).join('')} - - - - ${data.map((row: any, idx: number) => ` - - - ${headers.map(h => ``).join('')} - - `).join('')} - -
No.${h}
${idx + 1}${row[h] || '-'}
- `; - - tableWrapper.innerHTML = tableHTML; -} - -async function confirmUpload() { - const confirmBtn = document.getElementById('btn-confirm-upload') as HTMLButtonElement; - if (confirmBtn) { - confirmBtn.disabled = true; - confirmBtn.innerHTML = ' 저장 중...'; - 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 = ' 최종 데이터 저장하기'; - 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 = ' 생성 중...'; - 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 = {}; - 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 = ' 자산코드 일괄 생성'; - createIcons({ icons: { RefreshCcw } }); - } - } -} diff --git a/src/core/schema.ts b/src/core/schema.ts index dfeac0f..4da06c3 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -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 주소' }, diff --git a/src/views/List/ServerListView.ts b/src/views/List/ServerListView.ts index a9126db..f90de9b 100644 --- a/src/views/List/ServerListView.ts +++ b/src/views/List/ServerListView.ts @@ -28,10 +28,11 @@ export function renderServerList(container: HTMLElement) { ${ASSET_SCHEMA.CURRENT_DEPT.ui} - ${ASSET_SCHEMA.ASSET_PURPOSE.ui} - ${ASSET_SCHEMA.MODEL_NAME.ui} + ${ASSET_SCHEMA.ASSET_PURPOSE.ui} + ${ASSET_SCHEMA.ASSET_TYPE.ui} + 모델/메인보드 ${ASSET_SCHEMA.LOCATION.ui} - ${ASSET_SCHEMA.MEMO.ui} + ${ASSET_SCHEMA.MEMO.ui} @@ -50,7 +51,7 @@ export function renderServerList(container: HTMLElement) { tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; 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 = ` ${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||'-'} ${formatInline(asset[ASSET_SCHEMA.ASSET_PURPOSE.key]||'-')} - ${formatInline(asset[ASSET_SCHEMA.MODEL_NAME.key]||asset[ASSET_SCHEMA.ASSET_NAME.key]||'-')} + ${asset[ASSET_SCHEMA.ASSET_TYPE.key]||'-'} + ${formatInline(modelOrMainboard)} ${displayLoc} ${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')} `; diff --git a/temp_db.xlsx b/temp_db.xlsx deleted file mode 100644 index 11c3e17..0000000 Binary files a/temp_db.xlsx and /dev/null differ