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.tsx322
1 files changed, 302 insertions, 20 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 e07987b3..cb39419d 100644
--- a/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
+++ b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
@@ -19,6 +19,10 @@ import {
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'
interface ImportExcelProps {
onSuccess?: () => void
@@ -28,40 +32,68 @@ export function ImportVendorPoolButton({ onSuccess }: 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) return
+ if (!file) {
+ debugWarn('[Import] 파일이 선택되지 않았습니다.')
+ return
+ }
+
+ debugLog('[Import] 파일 임포트 시작:', {
+ fileName: file.name,
+ fileSize: file.size,
+ fileType: file.type
+ })
setIsImporting(true)
try {
// DRM 복호화 처리
+ debugProcess('[Import] DRM 복호화 시작')
toast.info("파일을 복호화하고 있습니다...")
let decryptedData: ArrayBuffer
try {
decryptedData = await decryptWithServerAction(file)
+ debugSuccess('[Import] DRM 복호화 성공')
toast.success("파일 복호화가 완료되었습니다.")
} catch (drmError) {
+ debugWarn('[Import] DRM 복호화 실패, 원본 파일로 진행:', drmError)
console.warn("DRM 복호화 실패, 원본 파일로 진행합니다:", drmError)
toast.warning("DRM 복호화에 실패했습니다. 원본 파일로 진행합니다.")
decryptedData = await file.arrayBuffer()
+ debugLog('[Import] 원본 파일 ArrayBuffer 로드 완료, size:', decryptedData.byteLength)
}
// 복호화된 데이터로 ExcelJS 워크북 로드
+ debugProcess('[Import] ExcelJS 워크북 로드 시작')
toast.info("엑셀 파일을 분석하고 있습니다...")
const workbook = new ExcelJS.Workbook()
await workbook.xlsx.load(decryptedData)
+ debugSuccess('[Import] ExcelJS 워크북 로드 완료')
// Get the first worksheet
const worksheet = workbook.getWorksheet(1)
if (!worksheet) {
+ debugError('[Import] 워크시트를 찾을 수 없습니다.')
toast.error("No worksheet found in the spreadsheet")
return
}
+ debugLog('[Import] 워크시트 확인 완료:', {
+ name: worksheet.name,
+ rowCount: worksheet.rowCount,
+ columnCount: worksheet.columnCount
+ })
// Check if there's an instruction row (템플릿 안내 텍스트가 있는지 확인)
const firstRowText = getCellValueAsString(worksheet.getRow(1).getCell(1));
@@ -71,10 +103,17 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
(worksheet.getRow(1).getCell(1).value !== null &&
worksheet.getRow(1).getCell(2).value === null);
+ debugLog('[Import] 첫 번째 행 확인:', {
+ firstRowText,
+ hasInstructionRow
+ })
+
// Get header row index (row 2 if there's an instruction row, otherwise row 1)
const headerRowIndex = hasInstructionRow ? 2 : 1;
+ debugLog('[Import] 헤더 행 인덱스:', headerRowIndex)
// Get column headers and their indices
+ debugProcess('[Import] 컬럼 헤더 매핑 시작')
const headerRow = worksheet.getRow(headerRowIndex);
const columnIndices: Record<string, number> = {};
@@ -87,15 +126,25 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
}
});
+ debugLog('[Import] 컬럼 매핑 완료:', {
+ mappedColumns: Object.keys(columnIndices).length,
+ columnIndices
+ })
+
// Process data rows
+ debugProcess('[Import] 데이터 행 처리 시작')
const rows: any[] = [];
const startRow = headerRowIndex + 1;
+ let skippedRows = 0;
for (let i = startRow; i <= worksheet.rowCount; i++) {
const row = worksheet.getRow(i);
// Skip empty rows
- if (row.cellCount === 0) continue;
+ if (row.cellCount === 0) {
+ skippedRows++;
+ continue;
+ }
// Check if this is likely an empty template row (빈 템플릿 행 건너뛰기)
let hasAnyData = false;
@@ -105,7 +154,10 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
break;
}
}
- if (!hasAnyData) continue;
+ if (!hasAnyData) {
+ skippedRows++;
+ continue;
+ }
const rowData: Record<string, any> = {};
let hasData = false;
@@ -121,26 +173,44 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
if (hasData) {
rows.push(rowData);
+ } else {
+ skippedRows++;
}
}
+ debugLog('[Import] 데이터 행 처리 완료:', {
+ totalRows: worksheet.rowCount - headerRowIndex,
+ validRows: rows.length,
+ skippedRows
+ })
+
if (rows.length === 0) {
+ debugWarn('[Import] 유효한 데이터가 없습니다.')
toast.error("No data found in the spreadsheet")
setIsImporting(false)
return
}
+ // Progress Dialog 표시
+ setTotalRows(rows.length)
+ setProcessedRows(0)
+ setShowProgressDialog(true)
+
// Process each row
+ debugProcess('[Import] 데이터베이스 저장 시작')
let successCount = 0;
let errorCount = 0;
- let duplicateErrors: string[] = [];
+ let duplicateCount = 0;
+ const resultItems: ImportResultItem[] = []; // 실패한 건만 포함
// Create promises for all vendor pool creation operations
- const promises = rows.map(async (row) => {
+ 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] || '';
@@ -159,16 +229,112 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
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);
+ 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
+ // Validate field lengths and formats (필드 길이 및 형식 검증)
const validationErrors: string[] = [];
if (vendorPoolData.designCategoryCode && vendorPoolData.designCategoryCode.length > 2) {
@@ -188,72 +354,174 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
}
if (validationErrors.length > 0) {
- console.error("Validation errors:", validationErrors, vendorPoolData);
+ 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') {
- duplicateErrors.push(`공사부문(${vendorPoolData.constructionSector}), H/T(${vendorPoolData.htDivision}), 자재그룹코드(${vendorPoolData.materialGroupCode}), 협력업체명(${vendorPoolData.vendorName})`);
- // 중복인 경우 에러 카운트를 증가시키지 않고 건너뜀 (전체 import 중단하지 않음)
+ 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
+ })
+
+ // 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) {
- toast.warning(`${errorCount}개 항목 가져오기에 실패했습니다.`);
+ debugWarn(`[Import] 일부 실패: ${errorCount}개 항목`);
}
// Call the success callback to refresh data
onSuccess?.();
} else if (errorCount > 0) {
+ debugError(`[Import] 모든 항목 실패: ${errorCount}개`);
toast.error(`모든 ${errorCount}개 항목 가져오기에 실패했습니다. 데이터 형식을 확인하세요.`);
}
- // 중복 데이터가 있었던 경우 개별적으로 표시 (성공/실패와 별개로 처리)
- if (duplicateErrors.length > 0) {
- duplicateErrors.forEach(errorMsg => {
- toast.warning(`중복 데이터로 건너뜀: ${errorMsg}`);
- });
+ if (duplicateCount > 0) {
+ debugWarn(`[Import] 중복 데이터: ${duplicateCount}개`);
+ toast.warning(`${duplicateCount}개의 중복 데이터가 감지되었습니다.`);
}
+ // Import 결과 Dialog 표시
+ setImportResult(result);
+ setShowResultDialog(true);
+
} 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 = '';
@@ -286,6 +554,20 @@ 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}
+ />
</>
)
}