diff options
Diffstat (limited to 'lib/vendor-pool/table/vendor-pool-excel-import-button.tsx')
| -rw-r--r-- | lib/vendor-pool/table/vendor-pool-excel-import-button.tsx | 258 |
1 files changed, 258 insertions, 0 deletions
diff --git a/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx new file mode 100644 index 00000000..704d4aff --- /dev/null +++ b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx @@ -0,0 +1,258 @@ +/** + * 특정 컬럼들 복합키로 묶어 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' + +interface ImportExcelProps { + onSuccess?: () => void +} + +export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { + const fileInputRef = useRef<HTMLInputElement>(null) + const [isImporting, setIsImporting] = React.useState(false) + const { data: session, status } = useSession() + + // 헬퍼 함수들은 excel-utils에서 import + + const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0] + if (!file) return + + setIsImporting(true) + + try { + // Read the Excel file using ExcelJS + const data = await file.arrayBuffer() + const workbook = new ExcelJS.Workbook() + await workbook.xlsx.load(data) + + // 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<string, number> = {}; + + 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<string, any> = {}; + 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; + + // Create promises for all vendor pool creation operations + const promises = rows.map(async (row) => { + try { + // Excel 컬럼 설정을 기반으로 데이터 매핑 + const vendorPoolData: any = {}; + + 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); + 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}개 항목 가져오기에 실패했습니다. 데이터 형식을 확인하세요.`); + } + + } 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 ( + <> + <Input + type="file" + ref={fileInputRef} + onChange={handleImport} + accept=".xlsx,.xls" + className="hidden" + /> + <Button + variant="outline" + size="sm" + onClick={() => fileInputRef.current?.click()} + disabled={isImporting} + className="gap-2" + > + {isImporting ? ( + <Loader className="size-4 animate-spin" aria-hidden="true" /> + ) : ( + <Upload className="size-4" aria-hidden="true" /> + )} + <span className="hidden sm:inline"> + {isImporting ? "Importing..." : "Import"} + </span> + </Button> + </> + ) +} |
