"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' required?: boolean } // vendor-pool Excel 컬럼 매핑 export const vendorPoolExcelColumns: ExcelColumnConfig[] = [ { accessorKey: 'constructionSector', header: '조선/해양', width: 15, required: true }, { accessorKey: 'htDivision', header: 'H/T구분', width: 15, required: true }, { accessorKey: 'designCategoryCode', header: '설계기능코드', width: 20, required: true }, { 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: 'faTarget', header: 'FA대상', width: 15, type: 'boolean' }, { accessorKey: 'faStatus', header: 'FA현황', width: 15 }, { accessorKey: 'tier', header: '등급', width: 15, required: true }, { accessorKey: 'isAgent', header: 'Agent여부', width: 15, type: 'boolean' }, { accessorKey: 'contractSignerCode', header: '계약서명주체코드', width: 20 }, { accessorKey: 'contractSignerName', header: '계약서명주체명', width: 25 }, // 코드로 자동완성 가능 { accessorKey: 'headquarterLocation', header: '본사위치', width: 20, required: true }, { accessorKey: 'manufacturingLocation', header: '제작/선적지', width: 20, required: true }, { accessorKey: 'avlVendorName', header: 'AVL등재업체명', width: 25, required: true }, { 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.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' } // 필수값인 경우 빨간색으로 표시 if (col.required) { headerCell.font = { bold: true, color: { argb: 'FFFF0000' } } // 빨간색 } else { headerCell.font = { bold: true } } }) // 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구분, 설계기능코드', ' - 등급, 본사위치, 제작/선적지, AVL등재업체명', '', '■ 자동완성 기능 (코드 입력 시)', ' 1. 코드가 있는 경우 → 코드만 입력하면 명칭 자동완성', ' • 설계기능코드 → 설계기능명', ' • 자재그룹코드 → 자재그룹명', ' • 협력업체코드 → 협력업체명', ' • 계약서명주체코드 → 계약서명주체명', '', ' 2. 코드가 없는 경우 → 명칭 직접 입력', '', '■ Boolean 필드', ' - TRUE/true/1/Y/O → 참', ' - FALSE/false/0/N 또는 빈 값 → 거짓', '', '■ 입력 규칙', ' - 조선/해양: "조선" 또는 "해양"', ' - H/T구분: "H", "T", "공통"', ' - 설계기능코드: 2자리 이하', ' - Equip/Bulk: "E", "B", "S" (1자리)', ' - 등급: "Tier 1", "Tier 2", "등급 외"' ] 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); }