summaryrefslogtreecommitdiff
path: root/lib/vendor-pool
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-14 15:13:25 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-14 15:13:25 +0900
commit598a40808b45413b373334092c3b60cab97d09b6 (patch)
treebb0ad859c243d6de834839fb91f26e58dc993eb5 /lib/vendor-pool
parent2665c5916e75a0d6f16098a09ab3c187266dd936 (diff)
(김준회) 벤더풀 유니크 처리 및 import 개선
Diffstat (limited to 'lib/vendor-pool')
-rw-r--r--lib/vendor-pool/service.ts35
-rw-r--r--lib/vendor-pool/table/vendor-pool-excel-import-button.tsx39
-rw-r--r--lib/vendor-pool/table/vendor-pool-table.tsx49
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",