import * as XLSX from 'xlsx'; import { ASSET_SCHEMA } from './schema'; /** * ITAM 엑셀 핸들러 (Database Synchronized Edition) * 데이터베이스 실제 스키마 컬럼과 엑셀 헤더를 1:1로 일치시킵니다. */ export interface HardwareAsset { [key: string]: any; id: string; } export interface SoftwareAsset { [key: string]: any; id: string; } export interface SWUser { id: string; sw_id: string; user_name: string; dept: string; corp: string; [key: string]: any; } export interface HardwareLog { id: string; assetId?: string; asset_id?: string; date?: string; log_date?: string; created_at?: string; details: string; user?: string; log_user?: string; event_type?: string; } export interface MasterAssetData { pc: HardwareAsset[]; server: HardwareAsset[]; storage: HardwareAsset[]; network: HardwareAsset[]; equipment: HardwareAsset[]; survey: HardwareAsset[]; pcParts: HardwareAsset[]; swInternal: SoftwareAsset[]; swExternal: SoftwareAsset[]; cloud: SoftwareAsset[]; domain: any[]; vip: HardwareAsset[]; officeSupplies: HardwareAsset[]; cost: any[]; swUsers: SWUser[]; logs: HardwareLog[]; [key: string]: any; } /** * DB 컬럼 순서 및 구성 정의 (실제 DB 스키마 dump 기준) */ const DB_MAPPING: Record = { pc: [ 'ASSET_TYPE', 'HW_STATUS', 'CURRENT_DEPT', 'PREV_DEPT', 'USER_POSITION', 'EMP_NO', 'CURRENT_USER', 'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'HDD3', 'HDD4', 'MAC_ADDR', 'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO', 'MAINBOARD' ], server: [ 'ASSET_TYPE', 'MODEL_NAME', 'ASSET_PURPOSE', 'HW_STATUS', 'CURRENT_DEPT', 'CPU', 'RAM', 'GPU', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP_ADDR', 'REMOTE_TOOL', 'REMOTE_ID', 'REMOTE_PW', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO', 'PREV_DEPT', 'MANAGER_SUB', 'IP_ADDR2', 'MONITORING', 'HDD3', 'HDD4', 'EMP_NO' ], storage: [ 'ASSET_TYPE', 'HW_STATUS', 'VOLUME', 'MODEL_NAME', 'EMP_NO', 'CURRENT_USER', 'SERIAL_NUM', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO', 'CURRENT_DEPT', 'PREV_DEPT' ], network: [ 'PURCHASE_CORP', 'HW_STATUS', 'CURRENT_DEPT', 'PREV_DEPT', 'EMP_NO', 'CURRENT_USER', 'ASSET_TYPE', 'ASSET_MFR', 'MODEL_NAME', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO' ], survey: [ // asset_survey (공간정보장비) 'HW_STATUS', 'ASSET_NAME', 'LOCATION', 'LOC_DETAIL', 'EMP_NO', 'CURRENT_USER', 'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO' ], pcParts: [ 'HW_STATUS', 'ASSET_TYPE', 'ASSET_MFR', 'MODEL_NAME', 'VOLUME', 'EMP_NO', 'CURRENT_USER', 'MONITOR_INCH', 'LOCATION', 'LOC_DETAIL', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO' ], equipment: [ 'HW_STATUS', 'ASSET_STATUS', 'ASSET_TYPE', 'ASSET_MFR', 'EMP_NO', 'CURRENT_USER', 'MODEL_NAME', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO' ], officeSupplies: [ // asset_office_supplies (시설자산) 'HW_STATUS', 'ASSET_TYPE', 'ASSET_MFR', 'MODEL_NAME', 'EMP_NO', 'CURRENT_USER', 'ASSET_COUNT', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'MEMO' ], swInternal: [ 'SW_FIELD', 'DEV_OBJ', 'SW_STATUS', 'SW_TYPE', 'MANAGER_MAIN', 'DEV_MGR', 'PLANNING_MGR', 'SALES_MGR', 'PURCHASE_CORP', 'MEMO' ], swExternal: [ 'PRODUCT_NAME', 'SW_TYPE', 'SW_STATUS', 'SW_FIELD', 'CURRENT_DEPT', 'PREV_DEPT', 'MANAGER_MAIN', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'EMAIL_ACCOUNT', 'MEMO', 'EMP_NO', 'CURRENT_USER' ], cloud: [ 'ASSET_PURPOSE', 'PURCHASE_METHOD', 'PURCHASE_VENDOR', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'MANAGER_MAIN', 'MANAGER_SUB', 'MEMO', 'SW_ID', 'SW_PW' ], domain: [ 'DOMAIN_ADDR', 'ASSET_PURPOSE', 'PURCHASE_VENDOR', 'ASSET_TYPE', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'MANAGER_MAIN', 'MANAGER_SUB', 'MEMO' ], cost: [ 'ASSET_TYPE', 'ASSET_PURPOSE', 'LOCATION', 'LOC_DETAIL', 'MANAGER_MAIN', 'MANAGER_SUB', 'PURCHASE_CORP', 'PURCHASE_DATE', 'PURCHASE_AMOUNT', 'PURCHASE_VENDOR', 'EMAIL_ACCOUNT', 'EMAIL_PW', 'MEMO', 'EMP_NO', 'CURRENT_USER' ], vip: [ // asset_vip (선물) 'ASSET_NAME', 'MODEL_NAME', 'LOCATION', 'LOC_DETAIL', 'PURCHASE_CORP', 'PURCHASE_DATE', 'EXPIRED_DATE', 'PURCHASE_VENDOR', 'MEMO' ] }; export function downloadTemplate() { const wb = XLSX.utils.book_new(); const tabConfigs = [ { name: 'PC', key: 'pc' }, { name: '서버', key: 'server' }, { name: '스토리지', key: 'storage' }, { name: '공간정보장비', key: 'survey' }, { name: 'PC부품', key: 'pcParts' }, { name: '네트워크', key: 'network' }, { name: '업무지원장비', key: 'equipment' }, { name: '내부SW', key: 'swInternal' }, { name: '외부SW', key: 'swExternal' }, { name: '클라우드', key: 'cloud' }, { name: '도메인', key: 'domain' }, { name: '비용관리', key: 'cost' }, { name: '선물', key: 'vip' }, { name: '시설자산', key: 'officeSupplies' } ]; tabConfigs.forEach(config => { const keys = DB_MAPPING[config.key]; const headers = keys.map(k => ASSET_SCHEMA[k].ui); const ws = XLSX.utils.aoa_to_sheet([headers]); ws['!cols'] = Array(headers.length).fill({ wch: 20 }); XLSX.utils.book_append_sheet(wb, ws, config.name); }); XLSX.writeFile(wb, 'itam_template_db_aligned.xlsx'); } export function exportToExcel(masterData: MasterAssetData) { const wb = XLSX.utils.book_new(); const exportConfigs = [ { name: 'PC', list: masterData.pc, key: 'pc' }, { name: '서버', list: masterData.server, key: 'server' }, { name: '스토리지', list: masterData.storage, key: 'storage' }, { name: '공간정보장비', list: masterData.survey || [], key: 'survey' }, { name: 'PC부품', list: masterData.pcParts || [], key: 'pcParts' }, { name: '네트워크', list: masterData.network || [], key: 'network' }, { name: '업무지원장비', list: masterData.equipment || [], key: 'equipment' }, { name: '내부SW', list: masterData.swInternal, key: 'swInternal' }, { name: '외부SW', list: masterData.swExternal, key: 'swExternal' }, { name: '클라우드', list: masterData.cloud || [], key: 'cloud' }, { name: '도메인', list: masterData.domain || [], key: 'domain' }, { name: '비용관리', list: masterData.cost || [], key: 'cost' }, { name: '선물', list: masterData.vip || [], key: 'vip' }, { name: '시설자산', list: masterData.officeSupplies || [], key: 'officeSupplies' } ]; exportConfigs.forEach(config => { const schemaKeys = DB_MAPPING[config.key]; const headers = schemaKeys.map(k => ASSET_SCHEMA[k].ui); const rows = config.list.map(asset => schemaKeys.map(k => { const dbField = ASSET_SCHEMA[k].db; return asset[dbField] || asset[ASSET_SCHEMA[k].key] || ''; }) ); const ws = XLSX.utils.aoa_to_sheet([headers, ...rows]); XLSX.utils.book_append_sheet(wb, ws, config.name); }); XLSX.writeFile(wb, `itam_export_${new Date().toISOString().split('T')[0]}.xlsx`); } export function formatExcelDate(val: any): string { if (!val) return ''; if (typeof val === 'number') { const date = new Date(Math.round((val - 25569) * 86400 * 1000)); return date.toISOString().split('T')[0]; } if (typeof val === 'string') { return val.replace(/\./g, '-').trim(); } return String(val); } export async function parseExcel(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { try { const workbook = XLSX.read(e.target?.result, { type: 'array' }); const parsedData: any = {}; workbook.SheetNames.forEach(sheetName => { const ws = workbook.Sheets[sheetName]; const rows = XLSX.utils.sheet_to_json(ws, { defval: "" }) as any[]; const list: any[] = []; rows.forEach(r => { const data: any = { id: Math.random().toString(36).substring(2, 9) }; // Set default category based on sheet name data['category'] = sheetName; Object.keys(r).forEach(label => { const schemaEntry = Object.values(ASSET_SCHEMA).find(s => s.ui === label); const key = schemaEntry ? schemaEntry.db : label; let val = r[label]; if (label.includes('일자') || label.includes('연월') || label.includes('만료일') || label.includes('시작일')) { val = formatExcelDate(val); } data[key] = val; }); list.push(data); }); // Sheet Name Mapping back to state keys const nameMap: Record = { 'PC': 'pc', '서버': 'server', '스토리지': 'storage', '공간정보장비': 'survey', 'PC부품': 'pcParts', '네트워크': 'network', '업무지원장비': 'equipment', '내부SW': 'swInternal', '외부SW': 'swExternal', '클라우드': 'cloud', '도메인': 'domain', '비용관리': 'cost', '선물': 'vip', '시설자산': 'officeSupplies' }; const stateKey = nameMap[sheetName] || sheetName; if (list.length > 0) parsedData[stateKey] = list; }); resolve(parsedData); } catch (err) { reject(err); } }; reader.readAsArrayBuffer(file); }); }