From 14f61e24947fb92dd71ec0a7196a6e815f8e66da Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:54:26 +0000 Subject: (최겸)기술영업 RFQ 담당자 초대, 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/excel-export.tsx | 106 +++++++++++++++++++-- 1 file changed, 99 insertions(+), 7 deletions(-) (limited to 'lib/tech-vendor-possible-items/table/excel-export.tsx') diff --git a/lib/tech-vendor-possible-items/table/excel-export.tsx b/lib/tech-vendor-possible-items/table/excel-export.tsx index d3c4dea5..e6fcceed 100644 --- a/lib/tech-vendor-possible-items/table/excel-export.tsx +++ b/lib/tech-vendor-possible-items/table/excel-export.tsx @@ -5,7 +5,7 @@ import { format } from 'date-fns'; import { ko } from 'date-fns/locale'; /** - * 기술영업 벤더 가능 아이템 데이터를 Excel 파일로 내보내기 + * 기술영업 벤더 가능 아이템 데이터를 Excel 파일로 내보내기 (새로운 스키마 버전) */ export async function exportTechVendorPossibleItemsToExcel( data: TechVendorPossibleItemsData[] @@ -19,13 +19,18 @@ export async function exportTechVendorPossibleItemsToExcel( // 워크시트 생성 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 }, ]; @@ -53,7 +58,7 @@ export async function exportTechVendorPossibleItemsToExcel( }; }); - // 데이터 추가 + // 데이터 추가 (새로운 스키마 필드들 포함) data.forEach((item, index) => { // 벤더 타입 파싱 let vendorTypes = ''; @@ -68,8 +73,13 @@ export async function exportTechVendorPossibleItemsToExcel( 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 }), }); @@ -89,6 +99,15 @@ export async function exportTechVendorPossibleItemsToExcel( // 나머지 컬럼 왼쪽 정렬 cell.alignment = { vertical: 'middle', horizontal: 'left' }; } + + // 텍스트 줄바꿈 처리 (긴 텍스트 필드들) + if (colNumber >= 9 && colNumber <= 10) { // itemList, subItemList 컬럼 + cell.alignment = { + vertical: 'top', + horizontal: 'left', + wrapText: true + }; + } }); // 홀수 행 배경색 @@ -103,19 +122,29 @@ export async function exportTechVendorPossibleItemsToExcel( } }); - // 요약 정보 워크시트 생성 + // 요약 정보 워크시트 생성 (새로운 스키마 통계 포함) 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 })], ]; @@ -127,10 +156,13 @@ export async function exportTechVendorPossibleItemsToExcel( } 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 = 30; + summarySheet.getColumn(1).width = 40; summarySheet.getColumn(2).width = 20; // 파일 생성 및 다운로드 @@ -178,4 +210,64 @@ function getVendorTypeDistribution(data: TechVendorPossibleItemsData[]): [string 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]); } \ No newline at end of file -- cgit v1.2.3