import * as ExcelJS from 'exceljs'; import { saveAs } from "file-saver"; import type { TechVendorPossibleItemsData } from '../service'; import { format } from 'date-fns'; import { ko } from 'date-fns/locale'; /** * 기술영업 벤더 가능 아이템 데이터를 Excel 파일로 내보내기 (새로운 스키마 버전) */ export async function exportTechVendorPossibleItemsToExcel( data: TechVendorPossibleItemsData[] ) { try { // 워크북 생성 const workbook = new ExcelJS.Workbook(); workbook.creator = 'Tech Vendor Possible Items Management System'; workbook.created = new Date(); // 워크시트 생성 const worksheet = workbook.addWorksheet('기술영업 벤더 가능 아이템'); // 컬럼 헤더 정의 및 스타일 적용 (새로운 스키마에 맞춰) worksheet.columns = [ { header: '번호', key: 'id', width: 10 }, { header: '벤더코드', key: 'vendorCode', width: 15 }, { header: '벤더명', key: 'vendorName', width: 25 }, { header: '벤더이메일', key: 'vendorEmail', width: 30 }, { header: '벤더타입', key: 'techVendorType', width: 20 }, { header: '아이템코드', key: 'itemCode', width: 20 }, { header: '공종', key: 'workType', width: 15 }, { header: '선종', key: 'shipTypes', width: 20 }, { header: '아이템리스트', key: 'itemList', width: 30 }, { header: '서브아이템리스트', key: 'subItemList', width: 30 }, { header: '생성일시', key: 'createdAt', width: 20 }, ]; // 헤더 스타일 적용 const headerRow = worksheet.getRow(1); headerRow.eachCell((cell) => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE6F3FF' } }; cell.font = { bold: true, color: { argb: 'FF1F4E79' } }; cell.border = { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' } }; cell.alignment = { vertical: 'middle', horizontal: 'center' }; }); // 데이터 추가 (새로운 스키마 필드들 포함) data.forEach((item, index) => { // 벤더 타입 파싱 let vendorTypes = ''; try { const parsed = JSON.parse(item.techVendorType || "[]"); vendorTypes = Array.isArray(parsed) ? parsed.join(', ') : item.techVendorType; } catch { vendorTypes = item.techVendorType; } const row = worksheet.addRow({ id: item.id, vendorCode: item.vendorCode || '-', vendorName: item.vendorName, vendorEmail: item.vendorEmail || '-', techVendorType: vendorTypes, itemCode: item.itemCode, workType: item.workType || '-', shipTypes: item.shipTypes || '-', itemList: item.itemList || '-', subItemList: item.subItemList || '-', createdAt: format(item.createdAt, 'yyyy-MM-dd HH:mm', { locale: ko }), }); // 데이터 행 스타일 row.eachCell((cell, colNumber) => { cell.border = { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' } }; if (colNumber === 1) { // ID 컬럼 가운데 정렬 cell.alignment = { vertical: 'middle', horizontal: 'center' }; } else { // 나머지 컬럼 왼쪽 정렬 cell.alignment = { vertical: 'middle', horizontal: 'left' }; } // 텍스트 줄바꿈 처리 (긴 텍스트 필드들) if (colNumber >= 9 && colNumber <= 10) { // itemList, subItemList 컬럼 cell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true }; } }); // 홀수 행 배경색 if (index % 2 === 1) { row.eachCell((cell) => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF8F9FA' } }; }); } }); // 요약 정보 워크시트 생성 (새로운 스키마 통계 포함) const summarySheet = workbook.addWorksheet('요약 정보'); const summaryData = [ ['기술영업 벤더 가능 아이템 현황 (새로운 스키마)', ''], ['', ''], ['📊 기본 통계:', ''], ['총 항목 수:', data.length.toLocaleString()], ['고유 벤더 수:', new Set(data.map(item => item.vendorId)).size.toLocaleString()], ['고유 아이템 수:', new Set(data.map(item => item.itemCode)).size.toLocaleString()], ['', ''], ['🏢 벤더 타입별 분포:', ''], ...getVendorTypeDistribution(data), ['', ''], ['⚙️ 공종별 분포:', ''], ...getWorkTypeDistribution(data), ['', ''], ['🚢 선종별 분포:', ''], ...getShipTypeDistribution(data), ['', ''], ['📈 데이터 완성도:', ''], ...getDataCompleteness(data), ['', ''], ['내보내기 일시:', format(new Date(), 'yyyy-MM-dd HH:mm:ss', { locale: ko })], ]; summaryData.forEach((rowData, index) => { const row = summarySheet.addRow(rowData); if (index === 0) { // 제목 스타일 row.getCell(1).font = { bold: true, size: 16, color: { argb: 'FF1F4E79' } }; } else if (typeof rowData[0] === 'string' && rowData[0].includes(':') && rowData[1] === '') { // 섹션 제목 스타일 row.getCell(1).font = { bold: true, color: { argb: 'FF1F4E79' } }; } else if (typeof rowData[0] === 'string' && rowData[0].includes('📊') || rowData[0].includes('🏢') || rowData[0].includes('⚙️') || rowData[0].includes('🚢') || rowData[0].includes('📈')) { // 이모지 섹션 제목 스타일 row.getCell(1).font = { bold: true, color: { argb: 'FF1F4E79' } }; } }); summarySheet.getColumn(1).width = 40; summarySheet.getColumn(2).width = 20; // 파일 생성 및 다운로드 const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); const fileName = `기술영업_벤더_가능_아이템_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`; saveAs(blob, fileName); return { success: true }; } catch (error) { console.error("Excel 내보내기 중 오류:", error); return { success: false, error: error instanceof Error ? error.message : "내보내기 중 오류가 발생했습니다." }; } } /** * 벤더 타입별 분포 계산 */ function getVendorTypeDistribution(data: TechVendorPossibleItemsData[]): [string, string][] { const typeCount = new Map(); data.forEach(item => { try { const parsed = JSON.parse(item.techVendorType || "[]"); const types = Array.isArray(parsed) ? parsed : [item.techVendorType]; types.forEach(type => { if (type) { typeCount.set(type, (typeCount.get(type) || 0) + 1); } }); } catch { if (item.techVendorType) { typeCount.set(item.techVendorType, (typeCount.get(item.techVendorType) || 0) + 1); } } }); return Array.from(typeCount.entries()) .sort((a, b) => b[1] - a[1]) .map(([type, count]) => [` - ${type}`, count.toLocaleString()]); } /** * 공종별 분포 계산 */ function getWorkTypeDistribution(data: TechVendorPossibleItemsData[]): [string, string][] { const workTypeCount = new Map(); data.forEach(item => { const workType = item.workType || '미분류'; workTypeCount.set(workType, (workTypeCount.get(workType) || 0) + 1); }); return Array.from(workTypeCount.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) // 상위 10개만 표시 .map(([type, count]) => [` - ${type}`, count.toLocaleString()]); } /** * 선종별 분포 계산 */ function getShipTypeDistribution(data: TechVendorPossibleItemsData[]): [string, string][] { const shipTypeCount = new Map(); data.forEach(item => { if (item.shipTypes) { // 여러 선종이 콤마로 구분되어 있을 수 있음 const shipTypes = item.shipTypes.split(',').map(s => s.trim()); shipTypes.forEach(shipType => { if (shipType) { shipTypeCount.set(shipType, (shipTypeCount.get(shipType) || 0) + 1); } }); } else { shipTypeCount.set('미분류', (shipTypeCount.get('미분류') || 0) + 1); } }); return Array.from(shipTypeCount.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) // 상위 10개만 표시 .map(([type, count]) => [` - ${type}`, count.toLocaleString()]); } /** * 데이터 완성도 계산 */ function getDataCompleteness(data: TechVendorPossibleItemsData[]): [string, string][] { const total = data.length; const completeness = [ ['벤더이메일 있음', `${data.filter(item => item.vendorEmail).length}/${total} (${((data.filter(item => item.vendorEmail).length / total) * 100).toFixed(1)}%)`], ['공종 있음', `${data.filter(item => item.workType).length}/${total} (${((data.filter(item => item.workType).length / total) * 100).toFixed(1)}%)`], ['선종 있음', `${data.filter(item => item.shipTypes).length}/${total} (${((data.filter(item => item.shipTypes).length / total) * 100).toFixed(1)}%)`], ['아이템리스트 있음', `${data.filter(item => item.itemList).length}/${total} (${((data.filter(item => item.itemList).length / total) * 100).toFixed(1)}%)`], ['서브아이템리스트 있음', `${data.filter(item => item.subItemList).length}/${total} (${((data.filter(item => item.subItemList).length / total) * 100).toFixed(1)}%)`], ]; return completeness.map(([label, stat]) => [` - ${label}`, stat]); }