summaryrefslogtreecommitdiff
path: root/lib/vendor-pool/excel-utils.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-24 17:36:08 +0900
committerjoonhoekim <26rote@gmail.com>2025-09-24 17:36:08 +0900
commitbf2db28586569499e44b58999f2e0f33ed4cdeb5 (patch)
tree9ef9305829fdec30ec7a442f2ba0547a62dba7a9 /lib/vendor-pool/excel-utils.ts
parent1bda7f20f113737f4af32495e7ff24f6022dc283 (diff)
(김준회) 구매 요청사항 반영 - vendor-pool 및 avl detail (이진용 프로)
Diffstat (limited to 'lib/vendor-pool/excel-utils.ts')
-rw-r--r--lib/vendor-pool/excel-utils.ts310
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);
+}