/** * 특정 컬럼들 복합키로 묶어 UPDATE 처리해야 함. */ "use client" import React, { useRef } from 'react' import ExcelJS from 'exceljs' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Upload, Loader } from 'lucide-react' import { createVendorPool } from '../service' import { Input } from '@/components/ui/input' import { useSession } from "next-auth/react" import { getCellValueAsString, parseBoolean, getAccessorKeyByHeader, vendorPoolExcelColumns } from '../excel-utils' import { decryptWithServerAction } from '@/components/drm/drmUtils' interface ImportExcelProps { onSuccess?: () => void } export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { const fileInputRef = useRef(null) const [isImporting, setIsImporting] = React.useState(false) const { data: session } = useSession() // 헬퍼 함수들은 excel-utils에서 import const handleImport = async (event: React.ChangeEvent) => { const file = event.target.files?.[0] if (!file) return setIsImporting(true) try { // DRM 복호화 처리 toast.info("파일을 복호화하고 있습니다...") let decryptedData: ArrayBuffer try { decryptedData = await decryptWithServerAction(file) toast.success("파일 복호화가 완료되었습니다.") } catch (drmError) { console.warn("DRM 복호화 실패, 원본 파일로 진행합니다:", drmError) toast.warning("DRM 복호화에 실패했습니다. 원본 파일로 진행합니다.") decryptedData = await file.arrayBuffer() } // 복호화된 데이터로 ExcelJS 워크북 로드 toast.info("엑셀 파일을 분석하고 있습니다...") const workbook = new ExcelJS.Workbook() await workbook.xlsx.load(decryptedData) // Get the first worksheet const worksheet = workbook.getWorksheet(1) if (!worksheet) { toast.error("No worksheet found in the spreadsheet") return } // Check if there's an instruction row (템플릿 안내 텍스트가 있는지 확인) const firstRowText = getCellValueAsString(worksheet.getRow(1).getCell(1)); const hasInstructionRow = firstRowText.includes('벤더풀 데이터 입력 템플릿') || firstRowText.includes('입력 가이드 시트') || firstRowText.includes('입력 가이드') || (worksheet.getRow(1).getCell(1).value !== null && worksheet.getRow(1).getCell(2).value === null); // Get header row index (row 2 if there's an instruction row, otherwise row 1) const headerRowIndex = hasInstructionRow ? 2 : 1; // Get column headers and their indices const headerRow = worksheet.getRow(headerRowIndex); const columnIndices: Record = {}; headerRow.eachCell((cell, colNumber) => { const header = getCellValueAsString(cell); // Excel 헤더를 통해 accessorKey 찾기 const accessorKey = getAccessorKeyByHeader(header); if (accessorKey) { columnIndices[accessorKey] = colNumber; } }); // Process data rows const rows: any[] = []; const startRow = headerRowIndex + 1; for (let i = startRow; i <= worksheet.rowCount; i++) { const row = worksheet.getRow(i); // Skip empty rows if (row.cellCount === 0) continue; // Check if this is likely an empty template row (빈 템플릿 행 건너뛰기) let hasAnyData = false; for (let col = 1; col <= row.cellCount; col++) { if (getCellValueAsString(row.getCell(col)).trim()) { hasAnyData = true; break; } } if (!hasAnyData) continue; const rowData: Record = {}; let hasData = false; // Map the data using accessorKey indices Object.entries(columnIndices).forEach(([accessorKey, colIndex]) => { const value = getCellValueAsString(row.getCell(colIndex)); if (value) { rowData[accessorKey] = value; hasData = true; } }); if (hasData) { rows.push(rowData); } } if (rows.length === 0) { toast.error("No data found in the spreadsheet") setIsImporting(false) return } // Process each row let successCount = 0; let errorCount = 0; let duplicateErrors: string[] = []; // Create promises for all vendor pool creation operations const promises = rows.map(async (row) => { // Excel 컬럼 설정을 기반으로 데이터 매핑 (catch 블록에서도 사용하기 위해 밖에서 선언) const vendorPoolData: any = {}; try { vendorPoolExcelColumns.forEach(column => { const { accessorKey, type } = column; const value = row[accessorKey] || ''; if (type === 'boolean') { vendorPoolData[accessorKey] = parseBoolean(String(value)); } else if (value === '') { // 빈 문자열은 null로 설정 (스키마에 맞게) vendorPoolData[accessorKey] = null; } else { vendorPoolData[accessorKey] = String(value); } }); // 현재 사용자 정보 추가 vendorPoolData.registrant = session?.user?.name || 'system'; vendorPoolData.lastModifier = session?.user?.name || 'system'; // Validate required fields if (!vendorPoolData.constructionSector || !vendorPoolData.htDivision || !vendorPoolData.designCategory || !vendorPoolData.vendorName || !vendorPoolData.designCategoryCode || !vendorPoolData.equipBulkDivision) { console.error("Missing required fields", vendorPoolData); errorCount++; return null; } // Validate field lengths and formats const validationErrors: string[] = []; if (vendorPoolData.designCategoryCode && vendorPoolData.designCategoryCode.length > 2) { validationErrors.push(`설계기능코드는 2자리 이하여야 합니다: ${vendorPoolData.designCategoryCode}`); } if (vendorPoolData.equipBulkDivision && vendorPoolData.equipBulkDivision.length > 1) { validationErrors.push(`Equip/Bulk 구분은 1자리여야 합니다: ${vendorPoolData.equipBulkDivision}`); } if (vendorPoolData.constructionSector && !['조선', '해양'].includes(vendorPoolData.constructionSector)) { validationErrors.push(`공사부문은 '조선' 또는 '해양'이어야 합니다: ${vendorPoolData.constructionSector}`); } if (vendorPoolData.htDivision && !['H', 'T', '공통'].includes(vendorPoolData.htDivision)) { validationErrors.push(`H/T구분은 'H', 'T' 또는 '공통'이어야 합니다: ${vendorPoolData.htDivision}`); } if (validationErrors.length > 0) { console.error("Validation errors:", validationErrors, vendorPoolData); errorCount++; return null; } if (!session || !session.user || !session.user.id) { toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.") return } // Create the vendor pool entry const result = await createVendorPool(vendorPoolData as any) if (!result) { console.error(`Failed to import row - createVendorPool returned null:`, vendorPoolData); errorCount++; return null; } successCount++; return result; } catch (error) { console.error("Error processing row:", error, row); // Unique 제약 조건 위반 감지 (중복 데이터) const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage === 'DUPLICATE_VENDOR_POOL') { duplicateErrors.push(`공사부문(${vendorPoolData.constructionSector}), H/T(${vendorPoolData.htDivision}), 자재그룹코드(${vendorPoolData.materialGroupCode}), 협력업체명(${vendorPoolData.vendorName})`); // 중복인 경우 에러 카운트를 증가시키지 않고 건너뜀 (전체 import 중단하지 않음) return null; } // 다른 에러의 경우 에러 카운트 증가 errorCount++; return null; } }); // Wait for all operations to complete await Promise.all(promises); // Show results if (successCount > 0) { toast.success(`${successCount}개 항목이 성공적으로 가져와졌습니다.`); if (errorCount > 0) { toast.warning(`${errorCount}개 항목 가져오기에 실패했습니다.`); } // Call the success callback to refresh data onSuccess?.(); } else if (errorCount > 0) { toast.error(`모든 ${errorCount}개 항목 가져오기에 실패했습니다. 데이터 형식을 확인하세요.`); } // 중복 데이터가 있었던 경우 개별적으로 표시 (성공/실패와 별개로 처리) if (duplicateErrors.length > 0) { duplicateErrors.forEach(errorMsg => { toast.warning(`중복 데이터로 건너뜀: ${errorMsg}`); }); } } catch (error) { console.error("Import error:", error); toast.error("Error importing data. Please check file format."); } finally { setIsImporting(false); // Reset the file input if (fileInputRef.current) { fileInputRef.current.value = ''; } } } return ( <> ) }