diff options
Diffstat (limited to 'lib/vendor-pool/excel-utils.ts')
| -rw-r--r-- | lib/vendor-pool/excel-utils.ts | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/lib/vendor-pool/excel-utils.ts b/lib/vendor-pool/excel-utils.ts new file mode 100644 index 00000000..8dd8743c --- /dev/null +++ b/lib/vendor-pool/excel-utils.ts @@ -0,0 +1,310 @@ +"use client" + +import ExcelJS from 'exceljs' +import { saveAs } from 'file-saver' +import type { VendorPoolItem } from './table/vendor-pool-table-columns' + +// Excel 컬럼 정의 +export interface ExcelColumnConfig { + accessorKey: string + header: string + width?: number + type?: 'text' | 'number' | 'boolean' | 'date' +} + +// vendor-pool Excel 컬럼 매핑 +export const vendorPoolExcelColumns: ExcelColumnConfig[] = [ + { accessorKey: 'constructionSector', header: '조선/해양', width: 15 }, + { accessorKey: 'htDivision', header: 'H/T구분', width: 15 }, + { accessorKey: 'designCategoryCode', header: '설계기능코드', width: 20 }, + { accessorKey: 'designCategory', header: '설계기능(공종)', width: 25 }, + { accessorKey: 'equipBulkDivision', header: 'Equip/Bulk', width: 15 }, + { accessorKey: 'packageCode', header: '패키지코드', width: 20 }, + { accessorKey: 'packageName', header: '패키지명', width: 25 }, + { accessorKey: 'materialGroupCode', header: '자재그룹코드', width: 20 }, + { accessorKey: 'materialGroupName', header: '자재그룹명', width: 30 }, + { accessorKey: 'smCode', header: 'SM Code', width: 15 }, + { accessorKey: 'similarMaterialNamePurchase', header: '유사자재명(구매)', width: 25 }, + { accessorKey: 'similarMaterialNameOther', header: '유사자재명(기타)', width: 25 }, + { accessorKey: 'vendorCode', header: '협력업체코드', width: 20 }, + { accessorKey: 'vendorName', header: '협력업체명', width: 25 }, + { accessorKey: 'taxId', header: '사업자번호', width: 20 }, + { accessorKey: 'faTarget', header: 'FA대상', width: 15, type: 'boolean' }, + { accessorKey: 'faStatus', header: 'FA현황', width: 15 }, + { accessorKey: 'tier', header: '등급', width: 15 }, + { accessorKey: 'isAgent', header: 'Agent여부', width: 15, type: 'boolean' }, + { accessorKey: 'contractSignerCode', header: '계약서명주체코드', width: 20 }, + { accessorKey: 'contractSignerName', header: '계약서명주체명', width: 25 }, + { accessorKey: 'headquarterLocation', header: '본사위치', width: 20 }, + { accessorKey: 'manufacturingLocation', header: '제작/선적지', width: 20 }, + { accessorKey: 'avlVendorName', header: 'AVL등재업체명', width: 25 }, + { accessorKey: 'similarVendorName', header: '유사업체명', width: 25 }, + { accessorKey: 'hasAvl', header: 'AVL보유', width: 15, type: 'boolean' }, + { accessorKey: 'isBlacklist', header: '블랙리스트', width: 15, type: 'boolean' }, + { accessorKey: 'isBcc', header: 'BCC', width: 15, type: 'boolean' }, + { accessorKey: 'purchaseOpinion', header: '구매의견', width: 30 }, + // 선종 + { accessorKey: 'shipTypeCommon', header: '선종공통', width: 15, type: 'boolean' }, + { accessorKey: 'shipTypeAmax', header: 'A-MAX', width: 15, type: 'boolean' }, + { accessorKey: 'shipTypeSmax', header: 'S-MAX', width: 15, type: 'boolean' }, + { accessorKey: 'shipTypeVlcc', header: 'VLCC', width: 15, type: 'boolean' }, + { accessorKey: 'shipTypeLngc', header: 'LNGC', width: 15, type: 'boolean' }, + { accessorKey: 'shipTypeCont', header: '컨테이너선', width: 15, type: 'boolean' }, + // 해양플랜트 + { accessorKey: 'offshoreTypeCommon', header: '해양플랜트공통', width: 20, type: 'boolean' }, + { accessorKey: 'offshoreTypeFpso', header: 'FPSO', width: 15, type: 'boolean' }, + { accessorKey: 'offshoreTypeFlng', header: 'FLNG', width: 15, type: 'boolean' }, + { accessorKey: 'offshoreTypeFpu', header: 'FPU', width: 15, type: 'boolean' }, + { accessorKey: 'offshoreTypePlatform', header: '플랫폼', width: 15, type: 'boolean' }, + { accessorKey: 'offshoreTypeWtiv', header: 'WTIV', width: 15, type: 'boolean' }, + { accessorKey: 'offshoreTypeGom', header: 'GOM', width: 15, type: 'boolean' }, + // 담당자 정보 + { accessorKey: 'picName', header: '담당자명', width: 20 }, + { accessorKey: 'picEmail', header: '담당자이메일', width: 30 }, + { accessorKey: 'picPhone', header: '담당자연락처', width: 20 }, + // 대행사 정보 + { accessorKey: 'agentName', header: '대행사명', width: 20 }, + { accessorKey: 'agentEmail', header: '대행사이메일', width: 30 }, + { accessorKey: 'agentPhone', header: '대행사연락처', width: 20 }, + // 최근 거래 정보 + { accessorKey: 'recentQuoteDate', header: '최근견적일', width: 20 }, + { accessorKey: 'recentQuoteNumber', header: '최근견적번호', width: 25 }, + { accessorKey: 'recentOrderDate', header: '최근발주일', width: 20 }, + { accessorKey: 'recentOrderNumber', header: '최근발주번호', width: 25 }, +] + +// 값 변환 헬퍼 함수 +const formatCellValue = (value: unknown, type?: string): string => { + if (value === null || value === undefined) return '' + + switch (type) { + case 'boolean': + return value ? 'TRUE' : 'FALSE' + case 'date': + if (value instanceof Date) { + return value.toISOString().split('T')[0] + } + return String(value) + default: + return String(value) + } +} + +// Excel 템플릿 생성 +export async function createVendorPoolTemplate(filename?: string) { + const workbook = new ExcelJS.Workbook() + const worksheet = workbook.addWorksheet('VendorPool Template') + + // 1. 안내 텍스트 추가 (첫 번째 행) + const instructionText = '벤더풀 데이터 입력 템플릿 - 입력 방법은 "입력 가이드" 시트를 참조하세요' + worksheet.getCell(1, 1).value = instructionText + worksheet.getCell(1, 1).font = { bold: true, color: { argb: 'FF0066CC' } } + worksheet.getCell(1, 1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFCC00' } + } + // 첫 번째 행을 여러 컬럼에 걸쳐 병합 + worksheet.mergeCells(1, 1, 1, Math.min(vendorPoolExcelColumns.length, 10)) + + // 2. 헤더 행 추가 (두 번째 행) + const headerRow = worksheet.addRow(vendorPoolExcelColumns.map(col => col.header)) + headerRow.font = { bold: true } + headerRow.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + } + + // 3. 컬럼 너비 설정 + vendorPoolExcelColumns.forEach((col, index) => { + const column = worksheet.getColumn(index + 1) + column.width = col.width || 15 + + // 헤더 셀 스타일 설정 (두 번째 행) + const headerCell = worksheet.getCell(2, index + 1) + headerCell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + } + headerCell.alignment = { vertical: 'middle', horizontal: 'center' } + }) + + // 3. 데이터 입력을 위한 빈 행 한 개 추가 (사용자가 바로 입력할 수 있도록) + worksheet.addRow(vendorPoolExcelColumns.map(() => '')) + + // 5. 가이드 워크시트 추가 + const guideWorksheet = workbook.addWorksheet('입력 가이드') + + // 가이드 제목 + guideWorksheet.getCell(1, 1).value = '벤더풀 데이터 입력 가이드' + guideWorksheet.getCell(1, 1).font = { bold: true, size: 16, color: { argb: 'FF0066CC' } } + + // 가이드 내용 + const guideContent = [ + '', + '■ 필수 입력 필드 (빨간색 * 표시)', + ' - 조선/해양: "조선" 또는 "해양"', + ' - H/T구분: "H", "T", 또는 "공통"', + ' - 설계기능코드: 2자리 이하 영문코드 (예: EL, ME)', + ' - 설계기능(공종): 설계기능 한글명 (예: 전장, 기관)', + ' - Equip/Bulk: "E" 또는 "B" (1자리)', + ' - 협력업체명: 업체 한글명', + ' - 자재그룹코드/명: 해당 자재그룹 정보', + ' - 등급: "Tier 1", "Tier 2", "Tier 3" 등', + ' - 계약서명주체명: 계약 주체 업체명', + ' - 본사위치/제작선적지: 국가명', + ' - AVL등재업체명: AVL에 등재된 업체명', + '', + '■ Boolean (참/거짓) 필드 입력법', + ' - TRUE, true, 1, Y, y, O, o → 참', + ' - FALSE, false, 0, N, n → 거짓', + ' - 빈 값은 기본적으로 거짓(false)으로 처리', + '', + '■ 주의사항', + ' - 첫 번째 행의 안내 텍스트는 삭제하지 마세요', + ' - 헤더 행(2번째 행)은 수정하지 마세요', + ' - 데이터는 3번째 행부터 입력하세요', + '', + '■ 문제 해결', + ' - 필드 길이 초과 오류: 해당 필드의 글자 수를 확인하세요', + ' - 필수 필드 누락: 빨간색 * 표시 필드를 모두 입력했는지 확인하세요', + ' - Boolean 값 오류: TRUE/FALSE 형태로 입력했는지 확인하세요' + ] + + guideContent.forEach((content, index) => { + const cell = guideWorksheet.getCell(index + 2, 1) + cell.value = content + if (content.startsWith('■')) { + cell.font = { bold: true, color: { argb: 'FF333333' } } + } else if (content.startsWith(' -')) { + cell.font = { color: { argb: 'FF666666' } } + } + }) + + // 가이드 워크시트 컬럼 너비 설정 + guideWorksheet.getColumn(1).width = 80 + + // 파일 저장 + const buffer = await workbook.xlsx.writeBuffer() + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }) + + const defaultFilename = `vendor-pool-template-${new Date().toISOString().split('T')[0]}.xlsx` + saveAs(blob, filename || defaultFilename) +} + +// Excel 데이터 내보내기 +export async function exportVendorPoolToExcel( + data: VendorPoolItem[], + filename?: string, + includeIds: boolean = true +) { + const workbook = new ExcelJS.Workbook() + const worksheet = workbook.addWorksheet('VendorPool Data') + + // ID 제외할 컬럼들 필터링 + const columnsToExport = includeIds + ? vendorPoolExcelColumns + : vendorPoolExcelColumns.filter(col => col.accessorKey !== 'id') + + // 헤더 행 추가 + const headerRow = worksheet.addRow(columnsToExport.map(col => col.header)) + headerRow.font = { bold: true } + headerRow.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + } + + // 데이터 행 추가 + data.forEach(item => { + const rowData = columnsToExport.map(col => { + const value = item[col.accessorKey as keyof VendorPoolItem] + return formatCellValue(value, col.type) + }) + worksheet.addRow(rowData) + }) + + // 컬럼 너비 및 스타일 설정 + columnsToExport.forEach((col, index) => { + const column = worksheet.getColumn(index + 1) + column.width = col.width || 15 + + // 헤더 셀 스타일 설정 + const headerCell = worksheet.getCell(1, index + 1) + headerCell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + } + headerCell.alignment = { vertical: 'middle', horizontal: 'center' } + }) + + // 데이터 셀 테두리 추가 + for (let row = 2; row <= worksheet.rowCount; row++) { + for (let col = 1; col <= columnsToExport.length; col++) { + const cell = worksheet.getCell(row, col) + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + } + } + } + + // 파일 저장 + const buffer = await workbook.xlsx.writeBuffer() + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }) + + const defaultFilename = `vendor-pool-${new Date().toISOString().split('T')[0]}.xlsx` + saveAs(blob, filename || defaultFilename) +} + +// Excel 컬럼 헤더로 accessorKey 찾기 +export function getAccessorKeyByHeader(header: string): string | undefined { + const column = vendorPoolExcelColumns.find(col => col.header === header) + return column?.accessorKey +} + +// accessorKey로 Excel 헤더 찾기 +export function getHeaderByAccessorKey(accessorKey: string): string | undefined { + const column = vendorPoolExcelColumns.find(col => col.accessorKey === accessorKey) + return column?.header +} + +// Boolean 값 파싱 +export function parseBoolean(value: string): boolean { + const lowerValue = value.toLowerCase().trim() + return lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes' || + lowerValue === 'o' || lowerValue === 'y' || lowerValue === '참' +} + +// Excel 셀 값을 문자열로 변환 +export function getCellValueAsString(cell: ExcelJS.Cell): string { + if (!cell || cell.value === undefined || cell.value === null) return ''; + + if (typeof cell.value === 'string') return cell.value.trim(); + if (typeof cell.value === 'number') return cell.value.toString(); + + // Handle rich text + if (typeof cell.value === 'object' && cell.value && 'richText' in cell.value) { + const richTextValue = cell.value as { richText: Array<{ text: string }> }; + return richTextValue.richText.map((rt) => rt.text).join(''); + } + + // Handle dates + if (cell.value instanceof Date) { + return cell.value.toISOString().split('T')[0]; + } + + // Fallback + return String(cell.value); +} |
