"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); }