summaryrefslogtreecommitdiff
path: root/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
diff options
context:
space:
mode:
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.tsx371
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}
- />
</>
)
}