diff options
Diffstat (limited to 'lib/vendor-pool')
| -rw-r--r-- | lib/vendor-pool/service.ts | 35 | ||||
| -rw-r--r-- | lib/vendor-pool/table/vendor-pool-excel-import-button.tsx | 39 | ||||
| -rw-r--r-- | lib/vendor-pool/table/vendor-pool-table.tsx | 49 |
3 files changed, 110 insertions, 13 deletions
diff --git a/lib/vendor-pool/service.ts b/lib/vendor-pool/service.ts index 3d83e3aa..97a0bcee 100644 --- a/lib/vendor-pool/service.ts +++ b/lib/vendor-pool/service.ts @@ -763,6 +763,23 @@ export async function createVendorPool(data: Omit<VendorPool, 'id' | 'registrati } catch (err) { debugError('Vendor Pool 생성 실패', { error: err, inputData: data }); console.error("Error in createVendorPool:", err); + + // Unique 제약 조건 위반 감지 + const errorMessage = err instanceof Error ? err.message : String(err); + if (errorMessage.includes('unique_vendor_pool_combination') || + errorMessage.includes('duplicate key value') || + errorMessage.includes('violates unique constraint')) { + debugError('Unique 제약 조건 위반 감지', { + constructionSector: data.constructionSector, + htDivision: data.htDivision, + materialGroupCode: data.materialGroupCode, + vendorName: data.vendorName, + error: err + }); + // Unique 제약 위반의 경우 특별한 에러 객체를 throw + throw new Error('DUPLICATE_VENDOR_POOL'); + } + return null; } } @@ -958,6 +975,24 @@ export async function updateVendorPool(id: number, data: Partial<VendorPool>): P } catch (err) { debugError('Vendor Pool 업데이트 실패', { error: err, id, updateData: data }); console.error("Error in updateVendorPool:", err); + + // Unique 제약 조건 위반 감지 + const errorMessage = err instanceof Error ? err.message : String(err); + if (errorMessage.includes('unique_vendor_pool_combination') || + errorMessage.includes('duplicate key value') || + errorMessage.includes('violates unique constraint')) { + debugError('Unique 제약 조건 위반 감지', { + id, + constructionSector: data.constructionSector, + htDivision: data.htDivision, + materialGroupCode: data.materialGroupCode, + vendorName: data.vendorName, + error: err + }); + // Unique 제약 위반의 경우 특별한 에러 객체를 throw + throw new Error('DUPLICATE_VENDOR_POOL'); + } + return null; } } 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 0f51170f..e07987b3 100644 --- a/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx +++ b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx @@ -133,17 +133,18 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { // 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 { - // 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 === '') { @@ -169,23 +170,23 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { // 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++; @@ -210,6 +211,16 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { 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; } @@ -221,8 +232,9 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { // Show results if (successCount > 0) { toast.success(`${successCount}개 항목이 성공적으로 가져와졌습니다.`); + if (errorCount > 0) { - toast.warning(`${errorCount}개 항목 가져오기에 실패했습니다. 콘솔에서 자세한 오류를 확인하세요.`); + toast.warning(`${errorCount}개 항목 가져오기에 실패했습니다.`); } // Call the success callback to refresh data onSuccess?.(); @@ -230,6 +242,13 @@ export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) { 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."); diff --git a/lib/vendor-pool/table/vendor-pool-table.tsx b/lib/vendor-pool/table/vendor-pool-table.tsx index 46a0588d..2696b354 100644 --- a/lib/vendor-pool/table/vendor-pool-table.tsx +++ b/lib/vendor-pool/table/vendor-pool-table.tsx @@ -198,6 +198,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP setIsSaving(true) let successCount = 0 let errorCount = 0 + let duplicateErrors: string[] = [] try { // 각 항목의 변경사항을 순차적으로 저장 @@ -218,6 +219,13 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP } } catch (error) { console.error(`항목 ${id} 저장 실패:`, error) + + // Unique 제약 조건 위반 감지 + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage === 'DUPLICATE_VENDOR_POOL') { + const changes = pendingChanges[id] + duplicateErrors.push(`항목 ${id}: 공사부문(${changes.constructionSector}), H/T(${changes.htDivision}), 자재그룹코드(${changes.materialGroupCode}), 협력업체명(${changes.vendorName})`) + } errorCount++ } } @@ -233,8 +241,17 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP onRefresh?.() // 데이터 새로고침 } - if (errorCount > 0) { - toast.error(`${errorCount}개 항목 저장에 실패했습니다.`) + // 중복 에러가 있는 경우 개별적으로 표시 + if (duplicateErrors.length > 0) { + duplicateErrors.forEach(errorMsg => { + toast.error(`중복된 항목입니다. ${errorMsg}`) + }) + } + + // 일반적인 에러 메시지 (중복 에러 제외) + const generalErrorCount = errorCount - duplicateErrors.length + if (generalErrorCount > 0) { + toast.error(`${generalErrorCount}개 항목 저장에 실패했습니다.`) } } catch (error) { console.error("Batch save error:", error) @@ -352,7 +369,14 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP } } catch (error) { console.error("빈 행 저장 실패:", error) - toast.error("저장 중 오류가 발생했습니다.") + + // Unique 제약 조건 위반 감지 + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage === 'DUPLICATE_VENDOR_POOL') { + toast.error(`중복된 항목입니다. (공사부문: ${finalData.constructionSector}, H/T: ${finalData.htDivision}, 자재그룹코드: ${finalData.materialGroupCode}, 협력업체명: ${finalData.vendorName})`) + } else { + toast.error("저장 중 오류가 발생했습니다.") + } } finally { setIsSaving(false) } @@ -448,6 +472,25 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP const advancedFilterFields: DataTableAdvancedFilterField<VendorPoolItem>[] = [ { + id: "constructionSector", + label: "공사부문", + type: "select", + options: [ + { label: "조선", value: "조선" }, + { label: "해양", value: "해양" }, + ] + }, + { + id: "htDivision", + label: "H/T구분", + type: "select", + options: [ + { label: "H", value: "H" }, + { label: "T", value: "T" }, + { label: "공통", value: "공통" }, + ] + }, + { id: "designCategoryCode", label: "설계기능코드", type: "text", |
