278 lines
10 KiB
TypeScript
278 lines
10 KiB
TypeScript
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<string, (keyof typeof ASSET_SCHEMA)[]> = {
|
|
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<any> {
|
|
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<string, string> = {
|
|
'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);
|
|
});
|
|
}
|