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 | 371 |
1 files changed, 18 insertions, 353 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 index cb39419d..3378c832 100644 --- a/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx +++ b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx @@ -9,39 +9,26 @@ 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 { processBulkImport } 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' import { debugLog, debugError, debugWarn, debugSuccess, debugProcess } from '@/lib/debug-utils' -import { enrichVendorPoolData } from '../enrichment-service' -import { ImportResultDialog, ImportResult, ImportResultItem } from './import-result-dialog' -import { ImportProgressDialog } from './import-progress-dialog' +import { ImportResult } from './import-result-dialog' interface ImportExcelProps { - onSuccess?: () => void + onImportComplete: (result: ImportResult) => void } -export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { +export function ImportVendorPoolButton({ onImportComplete }: ImportExcelProps) { const fileInputRef = useRef<HTMLInputElement>(null) const [isImporting, setIsImporting] = React.useState(false) const { data: session } = useSession() - const [importResult, setImportResult] = React.useState<ImportResult | null>(null) - const [showResultDialog, setShowResultDialog] = React.useState(false) - // Progress 상태 - const [showProgressDialog, setShowProgressDialog] = React.useState(false) - const [totalRows, setTotalRows] = React.useState(0) - const [processedRows, setProcessedRows] = React.useState(0) - - // 헬퍼 함수들은 excel-utils에서 import - const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0] if (!file) { @@ -87,6 +74,7 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { if (!worksheet) { debugError('[Import] 워크시트를 찾을 수 없습니다.') toast.error("No worksheet found in the spreadsheet") + setIsImporting(false) return } debugLog('[Import] 워크시트 확인 완료:', { @@ -133,7 +121,7 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { // Process data rows debugProcess('[Import] 데이터 행 처리 시작') - const rows: any[] = []; + const rows: Record<string, any>[] = []; const startRow = headerRowIndex + 1; let skippedRows = 0; @@ -191,337 +179,28 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { return } - // Progress Dialog 표시 - setTotalRows(rows.length) - setProcessedRows(0) - setShowProgressDialog(true) - - // Process each row - debugProcess('[Import] 데이터베이스 저장 시작') - let successCount = 0; - let errorCount = 0; - let duplicateCount = 0; - const resultItems: ImportResultItem[] = []; // 실패한 건만 포함 - - // Create promises for all vendor pool creation operations - const promises = rows.map(async (row, rowIndex) => { - // Excel 컬럼 설정을 기반으로 데이터 매핑 (catch 블록에서도 사용하기 위해 밖에서 선언) - const vendorPoolData: any = {}; - - try { - debugLog(`[Import] 행 ${rowIndex + 1}/${rows.length} 처리 시작 - 원본 데이터:`, row) - - 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'; - - debugLog(`[Import] 행 ${rowIndex + 1} 데이터 매핑 완료 - 전체 필드:`, { - '공사부문': vendorPoolData.constructionSector, - 'H/T구분': vendorPoolData.htDivision, - '설계기능코드': vendorPoolData.designCategoryCode, - '설계기능': vendorPoolData.designCategory, - 'Equip/Bulk구분': vendorPoolData.equipBulkDivision, - '협력업체코드': vendorPoolData.vendorCode, - '협력업체명': vendorPoolData.vendorName, - '자재그룹코드': vendorPoolData.materialGroupCode, - '자재그룹명': vendorPoolData.materialGroupName, - '계약서명주체코드': vendorPoolData.contractSignerCode, - '계약서명주체명': vendorPoolData.contractSignerName, - '사업자번호': vendorPoolData.taxId, - '패키지코드': vendorPoolData.packageCode, - '패키지명': vendorPoolData.packageName - }) - - // 코드를 기반으로 자동완성 수행 - debugProcess(`[Import] 행 ${rowIndex + 1} enrichment 시작`); - const enrichmentResult = await enrichVendorPoolData({ - designCategoryCode: vendorPoolData.designCategoryCode, - designCategory: vendorPoolData.designCategory, - materialGroupCode: vendorPoolData.materialGroupCode, - materialGroupName: vendorPoolData.materialGroupName, - vendorCode: vendorPoolData.vendorCode, - vendorName: vendorPoolData.vendorName, - contractSignerCode: vendorPoolData.contractSignerCode, - contractSignerName: vendorPoolData.contractSignerName, - }); - - // enrichment 결과 적용 - if (enrichmentResult.enrichedFields.length > 0) { - debugSuccess(`[Import] 행 ${rowIndex + 1} enrichment 완료 - 자동완성된 필드: ${enrichmentResult.enrichedFields.join(', ')}`); - - // enrichment된 데이터를 vendorPoolData에 반영 - if (enrichmentResult.enriched.designCategory) { - vendorPoolData.designCategory = enrichmentResult.enriched.designCategory; - } - if (enrichmentResult.enriched.materialGroupName) { - vendorPoolData.materialGroupName = enrichmentResult.enriched.materialGroupName; - } - if (enrichmentResult.enriched.vendorName) { - vendorPoolData.vendorName = enrichmentResult.enriched.vendorName; - } - if (enrichmentResult.enriched.contractSignerName) { - vendorPoolData.contractSignerName = enrichmentResult.enriched.contractSignerName; - } - } - - // enrichment 경고 메시지 로깅만 (resultItems에 추가하지 않음) - if (enrichmentResult.warnings.length > 0) { - debugWarn(`[Import] 행 ${rowIndex + 1} enrichment 경고:`, enrichmentResult.warnings); - enrichmentResult.warnings.forEach(warning => { - console.warn(`Row ${rowIndex + 1}: ${warning}`); - }); - } - - // Validate required fields (필수 필드 검증) - // 자동완성 가능한 필드는 코드가 있으면 명칭은 optional로 처리 - const requiredFieldsCheck = { - constructionSector: { value: vendorPoolData.constructionSector, label: '공사부문' }, - htDivision: { value: vendorPoolData.htDivision, label: 'H/T구분' }, - designCategoryCode: { value: vendorPoolData.designCategoryCode, label: '설계기능코드' } - }; - - // 설계기능: 설계기능코드가 없으면 설계기능명 필수 - if (!vendorPoolData.designCategoryCode && !vendorPoolData.designCategory) { - requiredFieldsCheck['designCategory'] = { value: vendorPoolData.designCategory, label: '설계기능' }; - } - - // 협력업체명: 협력업체코드가 없으면 협력업체명 필수 - if (!vendorPoolData.vendorCode && !vendorPoolData.vendorName) { - requiredFieldsCheck['vendorName'] = { value: vendorPoolData.vendorName, label: '협력업체명' }; - } - - const missingRequiredFields = Object.entries(requiredFieldsCheck) - .filter(([_, field]) => !field.value) - .map(([key, field]) => `${field.label}(${key})`); - - if (missingRequiredFields.length > 0) { - debugError(`[Import] 행 ${rowIndex + 1} 필수 필드 누락 [${missingRequiredFields.length}개]:`, { - missingFields: missingRequiredFields, - currentData: vendorPoolData - }); - console.error(`Missing required fields in row ${rowIndex + 1}:`, missingRequiredFields.join(', ')); - errorCount++; - resultItems.push({ - rowNumber: rowIndex + 1, - status: 'error', - message: `필수 필드 누락: ${missingRequiredFields.join(', ')}`, - data: { - vendorName: vendorPoolData.vendorName, - materialGroupName: vendorPoolData.materialGroupName, - designCategory: vendorPoolData.designCategory, - } - }); - - // Progress 업데이트 - setProcessedRows(prev => prev + 1); - - return null; - } - - debugSuccess(`[Import] 행 ${rowIndex + 1} 필수 필드 검증 통과`); - - // 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) { - debugError(`[Import] 행 ${rowIndex + 1} 검증 실패 [${validationErrors.length}개 오류]:`, { - errors: validationErrors, - problematicFields: { - designCategoryCode: vendorPoolData.designCategoryCode, - equipBulkDivision: vendorPoolData.equipBulkDivision, - constructionSector: vendorPoolData.constructionSector, - htDivision: vendorPoolData.htDivision - } - }); - console.error(`Validation errors in row ${rowIndex + 1}:`, validationErrors.join(' | ')); - errorCount++; - resultItems.push({ - rowNumber: rowIndex + 1, - status: 'error', - message: `검증 실패: ${validationErrors.join(', ')}`, - data: { - vendorName: vendorPoolData.vendorName, - materialGroupName: vendorPoolData.materialGroupName, - designCategory: vendorPoolData.designCategory, - } - }); - - // Progress 업데이트 - setProcessedRows(prev => prev + 1); - - return null; - } - - debugSuccess(`[Import] 행 ${rowIndex + 1} 형식 검증 통과`); - - if (!session || !session.user || !session.user.id) { - debugError(`[Import] 행 ${rowIndex + 1} 세션 오류: 로그인 정보 없음`); - toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.") - return - } - - // Create the vendor pool entry - debugProcess(`[Import] 행 ${rowIndex + 1} 데이터베이스 저장 시도`); - const result = await createVendorPool(vendorPoolData as any) - - if (!result) { - debugError(`[Import] 행 ${rowIndex + 1} 저장 실패: createVendorPool returned null`); - console.error(`Failed to import row - createVendorPool returned null:`, vendorPoolData); - errorCount++; - resultItems.push({ - rowNumber: rowIndex + 1, - status: 'error', - message: '데이터 저장 실패', - data: { - vendorName: vendorPoolData.vendorName, - materialGroupName: vendorPoolData.materialGroupName, - designCategory: vendorPoolData.designCategory, - } - }); - - // Progress 업데이트 - setProcessedRows(prev => prev + 1); - - return null; - } - - debugSuccess(`[Import] 행 ${rowIndex + 1} 저장 성공 (ID: ${result.id})`); - successCount++; - // 성공한 건은 resultItems에 추가하지 않음 - - // Progress 업데이트 - setProcessedRows(prev => prev + 1); - - return result; - } catch (error) { - debugError(`[Import] 행 ${rowIndex + 1} 처리 중 예외 발생:`, error); - console.error("Error processing row:", error, row); - - // Unique 제약 조건 위반 감지 (중복 데이터) - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage === 'DUPLICATE_VENDOR_POOL') { - debugWarn(`[Import] 행 ${rowIndex + 1} 중복 데이터 감지:`, { - constructionSector: vendorPoolData.constructionSector, - htDivision: vendorPoolData.htDivision, - materialGroupCode: vendorPoolData.materialGroupCode, - vendorName: vendorPoolData.vendorName - }); - duplicateCount++; - // 중복 건은 resultItems에 추가하지 않음 - - // Progress 업데이트 - setProcessedRows(prev => prev + 1); - - return null; - } - - // 다른 에러의 경우 에러 카운트 증가 - errorCount++; - resultItems.push({ - rowNumber: rowIndex + 1, - status: 'error', - message: `처리 중 오류 발생: ${errorMessage}`, - data: { - vendorName: vendorPoolData.vendorName, - materialGroupName: vendorPoolData.materialGroupName, - designCategory: vendorPoolData.designCategory, - } - }); - - // Progress 업데이트 - setProcessedRows(prev => prev + 1); - - return null; - } - }); - - // Wait for all operations to complete - debugProcess('[Import] 모든 Promise 완료 대기 중...') - await Promise.all(promises); - debugSuccess('[Import] 모든 데이터 처리 완료') - - debugLog('[Import] 최종 결과:', { - totalRows: rows.length, - successCount, - errorCount, - duplicateCount + // 서버로 Bulk Import 요청 + debugProcess('[Import] 서버 Bulk Import 요청 시작') + toast.info(`${rows.length}개의 데이터를 처리하고 있습니다...`) + + const registrant = session?.user?.name || 'system'; + const result = await processBulkImport(rows, registrant); + + debugSuccess('[Import] 서버 처리 완료', { + success: result.successCount, + error: result.errorCount }) - // Progress Dialog 닫기 - setShowProgressDialog(false) - - // Import 결과 Dialog 데이터 생성 (실패한 건만 포함) - const result: ImportResult = { - totalRows: rows.length, - successCount, - errorCount, - duplicateCount, - items: resultItems // 실패한 건만 포함됨 - } - - // Show results - if (successCount > 0) { - debugSuccess(`[Import] 임포트 성공: ${successCount}개 항목`); - toast.success(`${successCount}개 항목이 성공적으로 가져와졌습니다.`); - - if (errorCount > 0) { - debugWarn(`[Import] 일부 실패: ${errorCount}개 항목`); - } - // Call the success callback to refresh data - onSuccess?.(); - } else if (errorCount > 0) { - debugError(`[Import] 모든 항목 실패: ${errorCount}개`); - toast.error(`모든 ${errorCount}개 항목 가져오기에 실패했습니다. 데이터 형식을 확인하세요.`); - } - - if (duplicateCount > 0) { - debugWarn(`[Import] 중복 데이터: ${duplicateCount}개`); - toast.warning(`${duplicateCount}개의 중복 데이터가 감지되었습니다.`); - } - - // Import 결과 Dialog 표시 - setImportResult(result); - setShowResultDialog(true); + // 결과 처리 및 콜백 호출 + onImportComplete(result) } catch (error) { debugError('[Import] 전체 임포트 프로세스 실패:', error); console.error("Import error:", error); toast.error("Error importing data. Please check file format."); - setShowProgressDialog(false); } finally { debugLog('[Import] 임포트 프로세스 종료'); setIsImporting(false); - setShowProgressDialog(false); // Reset the file input if (fileInputRef.current) { fileInputRef.current.value = ''; @@ -554,20 +233,6 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { {isImporting ? "Importing..." : "Import"} </span> </Button> - - {/* Import Progress Dialog */} - <ImportProgressDialog - open={showProgressDialog} - totalRows={totalRows} - processedRows={processedRows} - /> - - {/* Import 결과 Dialog */} - <ImportResultDialog - open={showResultDialog} - onOpenChange={setShowResultDialog} - result={importResult} - /> </> ) } |
