summaryrefslogtreecommitdiff
path: root/lib/vendor-pool/service.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-25 22:04:56 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-25 22:04:56 +0900
commit2b59582194fc5c23140f52c42c793c324856a35e (patch)
tree0db8ef0e913b3a44dfd6e3e20fe92b8e4984aeba /lib/vendor-pool/service.ts
parent835df8ddc115ffa74414db2a4fab7efc0d0056a9 (diff)
(김준회) 벤더풀&AVL 구매 추가요청사항 반영
Diffstat (limited to 'lib/vendor-pool/service.ts')
-rw-r--r--lib/vendor-pool/service.ts687
1 files changed, 431 insertions, 256 deletions
diff --git a/lib/vendor-pool/service.ts b/lib/vendor-pool/service.ts
index 18d50ebd..f7637204 100644
--- a/lib/vendor-pool/service.ts
+++ b/lib/vendor-pool/service.ts
@@ -4,9 +4,12 @@ import { GetVendorPoolSchema } from "./validations";
import { VendorPool } from "./types";
import db from "@/db/db";
import { vendorPool } from "@/db/schema/avl/vendor-pool";
-import { eq, and, or, ilike, count, desc, sql } from "drizzle-orm";
+import { eq, and, or, ilike, count, desc, sql, inArray } from "drizzle-orm";
import { debugError } from "@/lib/debug-utils";
import { revalidateTag, unstable_cache } from "next/cache";
+import type { VendorPoolItem } from "./table/vendor-pool-table-columns";
+import { MATERIAL_GROUP_MASTER } from "@/db/schema/MDG/mdg";
+import { vendors } from "@/db/schema/vendors";
/**
* Vendor Pool 목록 조회
@@ -27,12 +30,10 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
whereConditions.push(
or(
ilike(vendorPool.constructionSector, searchTerm),
- ilike(vendorPool.designCategoryCode, searchTerm),
- ilike(vendorPool.designCategory, searchTerm),
+ ilike(vendorPool.discipline, searchTerm),
ilike(vendorPool.vendorName, searchTerm),
ilike(vendorPool.materialGroupCode, searchTerm),
ilike(vendorPool.materialGroupName, searchTerm),
- ilike(vendorPool.packageName, searchTerm),
ilike(vendorPool.avlVendorName, searchTerm),
ilike(vendorPool.similarVendorName, searchTerm)
)
@@ -61,32 +62,11 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
condition = ilike(vendorPool.htDivision, `%${filter.value}%`);
}
break;
- case 'designCategoryCode':
+ case 'discipline':
if (filter.operator === 'iLike') {
- condition = ilike(vendorPool.designCategoryCode, `%${filter.value}%`);
+ condition = ilike(vendorPool.discipline, `%${filter.value}%`);
} else if (filter.operator === 'eq') {
- condition = eq(vendorPool.designCategoryCode, filter.value as string);
- }
- break;
- case 'designCategory':
- if (filter.operator === 'iLike') {
- condition = ilike(vendorPool.designCategory, `%${filter.value}%`);
- } else if (filter.operator === 'eq') {
- condition = eq(vendorPool.designCategory, filter.value as string);
- }
- break;
- case 'packageCode':
- if (filter.operator === 'iLike') {
- condition = ilike(vendorPool.packageCode, `%${filter.value}%`);
- } else if (filter.operator === 'eq') {
- condition = eq(vendorPool.packageCode, filter.value as string);
- }
- break;
- case 'packageName':
- if (filter.operator === 'iLike') {
- condition = ilike(vendorPool.packageName, `%${filter.value}%`);
- } else if (filter.operator === 'eq') {
- condition = eq(vendorPool.packageName, filter.value as string);
+ condition = eq(vendorPool.discipline, filter.value as string);
}
break;
case 'materialGroupCode':
@@ -166,16 +146,6 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
condition = eq(vendorPool.lastModifier, filter.value as string);
}
break;
- case 'hasAvl':
- if (filter.operator === 'eq') {
- condition = eq(vendorPool.hasAvl, filter.value === 'true');
- }
- break;
- case 'isAgent':
- if (filter.operator === 'eq') {
- condition = eq(vendorPool.isAgent, filter.value === 'true');
- }
- break;
case 'isBlacklist':
if (filter.operator === 'eq') {
condition = eq(vendorPool.isBlacklist, filter.value === 'true');
@@ -222,21 +192,12 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
if (input.htDivision) {
whereConditions.push(eq(vendorPool.htDivision, input.htDivision));
}
- if (input.designCategoryCode) {
- whereConditions.push(ilike(vendorPool.designCategoryCode, `%${input.designCategoryCode}%`));
- }
- if (input.designCategory) {
- whereConditions.push(ilike(vendorPool.designCategory, `%${input.designCategory}%`));
+ if (input.discipline) {
+ whereConditions.push(ilike(vendorPool.discipline, `%${input.discipline}%`));
}
if (input.equipBulkDivision) {
whereConditions.push(eq(vendorPool.equipBulkDivision, input.equipBulkDivision));
}
- if (input.packageCode) {
- whereConditions.push(ilike(vendorPool.packageCode, `%${input.packageCode}%`));
- }
- if (input.packageName) {
- whereConditions.push(ilike(vendorPool.packageName, `%${input.packageName}%`));
- }
if (input.materialGroupCode) {
whereConditions.push(ilike(vendorPool.materialGroupCode, `%${input.materialGroupCode}%`));
}
@@ -255,16 +216,6 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
if (input.tier) {
whereConditions.push(ilike(vendorPool.tier, `%${input.tier}%`));
}
- if (input.hasAvl === "true") {
- whereConditions.push(eq(vendorPool.hasAvl, true));
- } else if (input.hasAvl === "false") {
- whereConditions.push(eq(vendorPool.hasAvl, false));
- }
- if (input.isAgent === "true") {
- whereConditions.push(eq(vendorPool.isAgent, true));
- } else if (input.isAgent === "false") {
- whereConditions.push(eq(vendorPool.isAgent, false));
- }
if (input.isBlacklist === "true") {
whereConditions.push(eq(vendorPool.isBlacklist, true));
} else if (input.isBlacklist === "false") {
@@ -325,31 +276,20 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
registrationDate: item.registrationDate ? item.registrationDate.toISOString().split('T')[0] : '',
lastModifiedDate: item.lastModifiedDate ? item.lastModifiedDate.toISOString().split('T')[0] : '',
// string 필드들의 null 처리
- packageCode: item.packageCode || '',
- packageName: item.packageName || '',
+ discipline: item.discipline || '',
materialGroupCode: item.materialGroupCode || '',
materialGroupName: item.materialGroupName || '',
- smCode: item.smCode || '',
similarMaterialNamePurchase: item.similarMaterialNamePurchase || '',
- similarMaterialNameOther: item.similarMaterialNameOther || '',
vendorCode: item.vendorCode || '',
vendorName: item.vendorName || '',
+ taxId: item.taxId || '',
faStatus: item.faStatus || '',
- faRemark: item.faRemark || '',
tier: item.tier || '',
- contractSignerCode: item.contractSignerCode || '',
- contractSignerName: item.contractSignerName || '',
headquarterLocation: item.headquarterLocation || '',
manufacturingLocation: item.manufacturingLocation || '',
avlVendorName: item.avlVendorName || '',
similarVendorName: item.similarVendorName || '',
purchaseOpinion: item.purchaseOpinion || '',
- picName: item.picName || '',
- picEmail: item.picEmail || '',
- picPhone: item.picPhone || '',
- agentName: item.agentName || '',
- agentEmail: item.agentEmail || '',
- agentPhone: item.agentPhone || '',
recentQuoteDate: item.recentQuoteDate || '',
recentQuoteNumber: item.recentQuoteNumber || '',
recentOrderDate: item.recentOrderDate || '',
@@ -358,24 +298,8 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
lastModifier: item.lastModifier || '',
// boolean 필드들을 적절히 처리
faTarget: item.faTarget ?? false,
- hasAvl: item.hasAvl ?? false,
- isAgent: item.isAgent ?? false,
isBlacklist: item.isBlacklist ?? false,
isBcc: item.isBcc ?? false,
- // 선종 적용 정보
- shipTypeCommon: item.shipTypeCommon ?? false,
- shipTypeAmax: item.shipTypeAmax ?? false,
- shipTypeSmax: item.shipTypeSmax ?? false,
- shipTypeVlcc: item.shipTypeVlcc ?? false,
- shipTypeLngc: item.shipTypeLngc ?? false,
- shipTypeCont: item.shipTypeCont ?? false,
- offshoreTypeCommon: item.offshoreTypeCommon ?? false,
- offshoreTypeFpso: item.offshoreTypeFpso ?? false,
- offshoreTypeFlng: item.offshoreTypeFlng ?? false,
- offshoreTypeFpu: item.offshoreTypeFpu ?? false,
- offshoreTypePlatform: item.offshoreTypePlatform ?? false,
- offshoreTypeWtiv: item.offshoreTypeWtiv ?? false,
- offshoreTypeGom: item.offshoreTypeGom ?? false,
}));
const pageCount = Math.ceil(totalCount[0].count / input.perPage);
@@ -404,6 +328,59 @@ export const getVendorPools = unstable_cache(
);
/**
+ * Vendor Pool 전체 데이터 조회 (페이지네이션 없음)
+ * 클라이언트 사이드 필터링/정렬을 위한 전체 데이터 로드
+ */
+export async function getAllVendorPools(): Promise<VendorPoolItem[]> {
+ try {
+ // 전체 데이터 조회 (limit 없음)
+ const data = await db
+ .select()
+ .from(vendorPool)
+ .orderBy(desc(vendorPool.registrationDate));
+
+ // 데이터 변환 (timestamp -> string)
+ const transformedData = data.map((item, index) => ({
+ ...item,
+ no: index + 1,
+ selected: false,
+ registrationDate: item.registrationDate ? item.registrationDate.toISOString().split('T')[0] : '',
+ lastModifiedDate: item.lastModifiedDate ? item.lastModifiedDate.toISOString().split('T')[0] : '',
+ // string 필드들의 null 처리
+ discipline: item.discipline || '',
+ materialGroupCode: item.materialGroupCode || '',
+ materialGroupName: item.materialGroupName || '',
+ similarMaterialNamePurchase: item.similarMaterialNamePurchase || '',
+ vendorCode: item.vendorCode || '',
+ vendorName: item.vendorName || '',
+ taxId: item.taxId || '',
+ faStatus: item.faStatus || '',
+ tier: item.tier || '',
+ headquarterLocation: item.headquarterLocation || '',
+ manufacturingLocation: item.manufacturingLocation || '',
+ avlVendorName: item.avlVendorName || '',
+ similarVendorName: item.similarVendorName || '',
+ purchaseOpinion: item.purchaseOpinion || '',
+ recentQuoteDate: item.recentQuoteDate || '',
+ recentQuoteNumber: item.recentQuoteNumber || '',
+ recentOrderDate: item.recentOrderDate || '',
+ recentOrderNumber: item.recentOrderNumber || '',
+ registrant: item.registrant || '',
+ lastModifier: item.lastModifier || '',
+ // boolean 필드들을 적절히 처리
+ faTarget: item.faTarget ?? false,
+ isBlacklist: item.isBlacklist ?? false,
+ isBcc: item.isBcc ?? false,
+ }));
+
+ return transformedData;
+ } catch (err) {
+ console.error("Error in getAllVendorPools:", err);
+ return [];
+ }
+}
+
+/**
* Vendor Pool 상세 정보 조회
*/
export async function getVendorPoolById(id: number): Promise<VendorPool | null> {
@@ -427,31 +404,20 @@ export async function getVendorPoolById(id: number): Promise<VendorPool | null>
registrationDate: item.registrationDate ? item.registrationDate.toISOString().split('T')[0] : '',
lastModifiedDate: item.lastModifiedDate ? item.lastModifiedDate.toISOString().split('T')[0] : '',
// string 필드들의 null 처리
- packageCode: item.packageCode || '',
- packageName: item.packageName || '',
+ discipline: item.discipline || '',
materialGroupCode: item.materialGroupCode || '',
materialGroupName: item.materialGroupName || '',
- smCode: item.smCode || '',
similarMaterialNamePurchase: item.similarMaterialNamePurchase || '',
- similarMaterialNameOther: item.similarMaterialNameOther || '',
vendorCode: item.vendorCode || '',
vendorName: item.vendorName || '',
+ taxId: item.taxId || '',
faStatus: item.faStatus || '',
- faRemark: item.faRemark || '',
tier: item.tier || '',
- contractSignerCode: item.contractSignerCode || '',
- contractSignerName: item.contractSignerName || '',
headquarterLocation: item.headquarterLocation || '',
manufacturingLocation: item.manufacturingLocation || '',
avlVendorName: item.avlVendorName || '',
similarVendorName: item.similarVendorName || '',
purchaseOpinion: item.purchaseOpinion || '',
- picName: item.picName || '',
- picEmail: item.picEmail || '',
- picPhone: item.picPhone || '',
- agentName: item.agentName || '',
- agentEmail: item.agentEmail || '',
- agentPhone: item.agentPhone || '',
recentQuoteDate: item.recentQuoteDate || '',
recentQuoteNumber: item.recentQuoteNumber || '',
recentOrderDate: item.recentOrderDate || '',
@@ -460,24 +426,8 @@ export async function getVendorPoolById(id: number): Promise<VendorPool | null>
lastModifier: item.lastModifier || '',
// boolean 필드들을 적절히 처리
faTarget: item.faTarget ?? false,
- hasAvl: item.hasAvl ?? false,
- isAgent: item.isAgent ?? false,
isBlacklist: item.isBlacklist ?? false,
isBcc: item.isBcc ?? false,
- // 선종 적용 정보
- shipTypeCommon: item.shipTypeCommon ?? false,
- shipTypeAmax: item.shipTypeAmax ?? false,
- shipTypeSmax: item.shipTypeSmax ?? false,
- shipTypeVlcc: item.shipTypeVlcc ?? false,
- shipTypeLngc: item.shipTypeLngc ?? false,
- shipTypeCont: item.shipTypeCont ?? false,
- offshoreTypeCommon: item.offshoreTypeCommon ?? false,
- offshoreTypeFpso: item.offshoreTypeFpso ?? false,
- offshoreTypeFlng: item.offshoreTypeFlng ?? false,
- offshoreTypeFpu: item.offshoreTypeFpu ?? false,
- offshoreTypePlatform: item.offshoreTypePlatform ?? false,
- offshoreTypeWtiv: item.offshoreTypeWtiv ?? false,
- offshoreTypeGom: item.offshoreTypeGom ?? false,
};
return transformedData;
@@ -579,37 +529,25 @@ export async function createVendorPool(data: Omit<VendorPool, 'id' | 'registrati
htDivision: data.htDivision,
// 설계 정보
- designCategoryCode: data.designCategoryCode,
- designCategory: data.designCategory,
+ discipline: data.discipline,
equipBulkDivision: data.equipBulkDivision,
- // 패키지 정보
- packageCode: data.packageCode,
- packageName: data.packageName,
-
// 자재그룹 정보
materialGroupCode: data.materialGroupCode,
materialGroupName: data.materialGroupName,
// 자재 관련 정보
- smCode: data.smCode,
similarMaterialNamePurchase: data.similarMaterialNamePurchase,
- similarMaterialNameOther: data.similarMaterialNameOther,
// 협력업체 정보
vendorCode: data.vendorCode,
vendorName: data.vendorName,
+ taxId: data.taxId,
// 사업 및 인증 정보
faTarget: data.faTarget ?? false,
faStatus: data.faStatus,
- faRemark: data.faRemark,
tier: data.tier,
- isAgent: data.isAgent ?? false,
-
- // 계약 정보
- contractSignerCode: data.contractSignerCode,
- contractSignerName: data.contractSignerName,
// 위치 정보
headquarterLocation: data.headquarterLocation,
@@ -618,38 +556,12 @@ export async function createVendorPool(data: Omit<VendorPool, 'id' | 'registrati
// AVL 관련 정보
avlVendorName: data.avlVendorName,
similarVendorName: data.similarVendorName,
- hasAvl: data.hasAvl ?? false,
// 상태 정보
isBlacklist: data.isBlacklist ?? false,
isBcc: data.isBcc ?? false,
purchaseOpinion: data.purchaseOpinion,
- // AVL 적용 선종(조선)
- shipTypeCommon: data.shipTypeCommon ?? false,
- shipTypeAmax: data.shipTypeAmax ?? false,
- shipTypeSmax: data.shipTypeSmax ?? false,
- shipTypeVlcc: data.shipTypeVlcc ?? false,
- shipTypeLngc: data.shipTypeLngc ?? false,
- shipTypeCont: data.shipTypeCont ?? false,
-
- // AVL 적용 선종(해양)
- offshoreTypeCommon: data.offshoreTypeCommon ?? false,
- offshoreTypeFpso: data.offshoreTypeFpso ?? false,
- offshoreTypeFlng: data.offshoreTypeFlng ?? false,
- offshoreTypeFpu: data.offshoreTypeFpu ?? false,
- offshoreTypePlatform: data.offshoreTypePlatform ?? false,
- offshoreTypeWtiv: data.offshoreTypeWtiv ?? false,
- offshoreTypeGom: data.offshoreTypeGom ?? false,
-
- // eVCP 미등록 정보
- picName: data.picName,
- picEmail: data.picEmail,
- picPhone: data.picPhone,
- agentName: data.agentName,
- agentEmail: data.agentEmail,
- agentPhone: data.agentPhone,
-
// 업체 실적 현황
recentQuoteDate: data.recentQuoteDate,
recentQuoteNumber: data.recentQuoteNumber,
@@ -687,32 +599,20 @@ export async function createVendorPool(data: Omit<VendorPool, 'id' | 'registrati
registrationDate: createdItem.registrationDate ? createdItem.registrationDate.toISOString().split('T')[0] : '',
lastModifiedDate: createdItem.lastModifiedDate ? createdItem.lastModifiedDate.toISOString().split('T')[0] : '',
// string 필드들의 null 처리
- packageCode: createdItem.packageCode || '',
- packageName: createdItem.packageName || '',
+ discipline: createdItem.discipline || '',
materialGroupCode: createdItem.materialGroupCode || '',
materialGroupName: createdItem.materialGroupName || '',
- smCode: createdItem.smCode || '',
similarMaterialNamePurchase: createdItem.similarMaterialNamePurchase || '',
- similarMaterialNameOther: createdItem.similarMaterialNameOther || '',
vendorCode: createdItem.vendorCode || '',
vendorName: createdItem.vendorName || '',
taxId: createdItem.taxId || '',
faStatus: createdItem.faStatus || '',
- faRemark: createdItem.faRemark || '',
tier: createdItem.tier || '',
- contractSignerCode: createdItem.contractSignerCode || '',
- contractSignerName: createdItem.contractSignerName || '',
headquarterLocation: createdItem.headquarterLocation || '',
manufacturingLocation: createdItem.manufacturingLocation || '',
avlVendorName: createdItem.avlVendorName || '',
similarVendorName: createdItem.similarVendorName || '',
purchaseOpinion: createdItem.purchaseOpinion || '',
- picName: createdItem.picName || '',
- picEmail: createdItem.picEmail || '',
- picPhone: createdItem.picPhone || '',
- agentName: createdItem.agentName || '',
- agentEmail: createdItem.agentEmail || '',
- agentPhone: createdItem.agentPhone || '',
recentQuoteDate: createdItem.recentQuoteDate || '',
recentQuoteNumber: createdItem.recentQuoteNumber || '',
recentOrderDate: createdItem.recentOrderDate || '',
@@ -721,24 +621,8 @@ export async function createVendorPool(data: Omit<VendorPool, 'id' | 'registrati
lastModifier: createdItem.lastModifier || '',
// boolean 필드들을 적절히 처리
faTarget: createdItem.faTarget ?? false,
- hasAvl: createdItem.hasAvl ?? false,
- isAgent: createdItem.isAgent ?? false,
isBlacklist: createdItem.isBlacklist ?? false,
isBcc: createdItem.isBcc ?? false,
- // 선종 적용 정보
- shipTypeCommon: createdItem.shipTypeCommon ?? false,
- shipTypeAmax: createdItem.shipTypeAmax ?? false,
- shipTypeSmax: createdItem.shipTypeSmax ?? false,
- shipTypeVlcc: createdItem.shipTypeVlcc ?? false,
- shipTypeLngc: createdItem.shipTypeLngc ?? false,
- shipTypeCont: createdItem.shipTypeCont ?? false,
- offshoreTypeCommon: createdItem.offshoreTypeCommon ?? false,
- offshoreTypeFpso: createdItem.offshoreTypeFpso ?? false,
- offshoreTypeFlng: createdItem.offshoreTypeFlng ?? false,
- offshoreTypeFpu: createdItem.offshoreTypeFpu ?? false,
- offshoreTypePlatform: createdItem.offshoreTypePlatform ?? false,
- offshoreTypeWtiv: createdItem.offshoreTypeWtiv ?? false,
- offshoreTypeGom: createdItem.offshoreTypeGom ?? false,
};
// debugSuccess('Vendor Pool 생성 완료', { result: transformedData });
@@ -791,37 +675,25 @@ export async function updateVendorPool(id: number, data: Partial<VendorPool>): P
if (data.htDivision !== undefined) updateData.htDivision = data.htDivision;
// 설계 정보
- if (data.designCategoryCode !== undefined) updateData.designCategoryCode = data.designCategoryCode;
- if (data.designCategory !== undefined) updateData.designCategory = data.designCategory;
+ if (data.discipline !== undefined) updateData.discipline = data.discipline;
if (data.equipBulkDivision !== undefined) updateData.equipBulkDivision = data.equipBulkDivision;
- // 패키지 정보
- if (data.packageCode !== undefined) updateData.packageCode = data.packageCode;
- if (data.packageName !== undefined) updateData.packageName = data.packageName;
-
// 자재그룹 정보
if (data.materialGroupCode !== undefined) updateData.materialGroupCode = data.materialGroupCode;
if (data.materialGroupName !== undefined) updateData.materialGroupName = data.materialGroupName;
// 자재 관련 정보
- if (data.smCode !== undefined) updateData.smCode = data.smCode;
if (data.similarMaterialNamePurchase !== undefined) updateData.similarMaterialNamePurchase = data.similarMaterialNamePurchase;
- if (data.similarMaterialNameOther !== undefined) updateData.similarMaterialNameOther = data.similarMaterialNameOther;
// 협력업체 정보
if (data.vendorCode !== undefined) updateData.vendorCode = data.vendorCode;
if (data.vendorName !== undefined) updateData.vendorName = data.vendorName;
+ if (data.taxId !== undefined) updateData.taxId = data.taxId;
// 사업 및 인증 정보
if (data.faTarget !== undefined) updateData.faTarget = data.faTarget;
if (data.faStatus !== undefined) updateData.faStatus = data.faStatus;
- if (data.faRemark !== undefined) updateData.faRemark = data.faRemark;
if (data.tier !== undefined) updateData.tier = data.tier;
- if (data.isAgent !== undefined) updateData.isAgent = data.isAgent;
-
- // 계약 정보
- if (data.contractSignerCode !== undefined) updateData.contractSignerCode = data.contractSignerCode;
- if (data.contractSignerName !== undefined) updateData.contractSignerName = data.contractSignerName;
// 위치 정보
if (data.headquarterLocation !== undefined) updateData.headquarterLocation = data.headquarterLocation;
@@ -830,38 +702,12 @@ export async function updateVendorPool(id: number, data: Partial<VendorPool>): P
// AVL 관련 정보
if (data.avlVendorName !== undefined) updateData.avlVendorName = data.avlVendorName;
if (data.similarVendorName !== undefined) updateData.similarVendorName = data.similarVendorName;
- if (data.hasAvl !== undefined) updateData.hasAvl = data.hasAvl;
// 상태 정보
if (data.isBlacklist !== undefined) updateData.isBlacklist = data.isBlacklist;
if (data.isBcc !== undefined) updateData.isBcc = data.isBcc;
if (data.purchaseOpinion !== undefined) updateData.purchaseOpinion = data.purchaseOpinion;
- // AVL 적용 선종(조선)
- if (data.shipTypeCommon !== undefined) updateData.shipTypeCommon = data.shipTypeCommon;
- if (data.shipTypeAmax !== undefined) updateData.shipTypeAmax = data.shipTypeAmax;
- if (data.shipTypeSmax !== undefined) updateData.shipTypeSmax = data.shipTypeSmax;
- if (data.shipTypeVlcc !== undefined) updateData.shipTypeVlcc = data.shipTypeVlcc;
- if (data.shipTypeLngc !== undefined) updateData.shipTypeLngc = data.shipTypeLngc;
- if (data.shipTypeCont !== undefined) updateData.shipTypeCont = data.shipTypeCont;
-
- // AVL 적용 선종(해양)
- if (data.offshoreTypeCommon !== undefined) updateData.offshoreTypeCommon = data.offshoreTypeCommon;
- if (data.offshoreTypeFpso !== undefined) updateData.offshoreTypeFpso = data.offshoreTypeFpso;
- if (data.offshoreTypeFlng !== undefined) updateData.offshoreTypeFlng = data.offshoreTypeFlng;
- if (data.offshoreTypeFpu !== undefined) updateData.offshoreTypeFpu = data.offshoreTypeFpu;
- if (data.offshoreTypePlatform !== undefined) updateData.offshoreTypePlatform = data.offshoreTypePlatform;
- if (data.offshoreTypeWtiv !== undefined) updateData.offshoreTypeWtiv = data.offshoreTypeWtiv;
- if (data.offshoreTypeGom !== undefined) updateData.offshoreTypeGom = data.offshoreTypeGom;
-
- // eVCP 미등록 정보
- if (data.picName !== undefined) updateData.picName = data.picName;
- if (data.picEmail !== undefined) updateData.picEmail = data.picEmail;
- if (data.picPhone !== undefined) updateData.picPhone = data.picPhone;
- if (data.agentName !== undefined) updateData.agentName = data.agentName;
- if (data.agentEmail !== undefined) updateData.agentEmail = data.agentEmail;
- if (data.agentPhone !== undefined) updateData.agentPhone = data.agentPhone;
-
// 업체 실적 현황
if (data.recentQuoteDate !== undefined) updateData.recentQuoteDate = data.recentQuoteDate;
if (data.recentQuoteNumber !== undefined) updateData.recentQuoteNumber = data.recentQuoteNumber;
@@ -898,32 +744,20 @@ export async function updateVendorPool(id: number, data: Partial<VendorPool>): P
registrationDate: updatedItem.registrationDate ? updatedItem.registrationDate.toISOString().split('T')[0] : '',
lastModifiedDate: updatedItem.lastModifiedDate ? updatedItem.lastModifiedDate.toISOString().split('T')[0] : '',
// string 필드들의 null 처리
- packageCode: updatedItem.packageCode || '',
- packageName: updatedItem.packageName || '',
+ discipline: updatedItem.discipline || '',
materialGroupCode: updatedItem.materialGroupCode || '',
materialGroupName: updatedItem.materialGroupName || '',
- smCode: updatedItem.smCode || '',
similarMaterialNamePurchase: updatedItem.similarMaterialNamePurchase || '',
- similarMaterialNameOther: updatedItem.similarMaterialNameOther || '',
vendorCode: updatedItem.vendorCode || '',
vendorName: updatedItem.vendorName || '',
taxId: updatedItem.taxId || '',
faStatus: updatedItem.faStatus || '',
- faRemark: updatedItem.faRemark || '',
tier: updatedItem.tier || '',
- contractSignerCode: updatedItem.contractSignerCode || '',
- contractSignerName: updatedItem.contractSignerName || '',
headquarterLocation: updatedItem.headquarterLocation || '',
manufacturingLocation: updatedItem.manufacturingLocation || '',
avlVendorName: updatedItem.avlVendorName || '',
similarVendorName: updatedItem.similarVendorName || '',
purchaseOpinion: updatedItem.purchaseOpinion || '',
- picName: updatedItem.picName || '',
- picEmail: updatedItem.picEmail || '',
- picPhone: updatedItem.picPhone || '',
- agentName: updatedItem.agentName || '',
- agentEmail: updatedItem.agentEmail || '',
- agentPhone: updatedItem.agentPhone || '',
recentQuoteDate: updatedItem.recentQuoteDate || '',
recentQuoteNumber: updatedItem.recentQuoteNumber || '',
recentOrderDate: updatedItem.recentOrderDate || '',
@@ -932,24 +766,8 @@ export async function updateVendorPool(id: number, data: Partial<VendorPool>): P
lastModifier: updatedItem.lastModifier || 'system',
// boolean 필드들을 적절히 처리
faTarget: updatedItem.faTarget ?? false,
- hasAvl: updatedItem.hasAvl ?? false,
- isAgent: updatedItem.isAgent ?? false,
isBlacklist: updatedItem.isBlacklist ?? false,
isBcc: updatedItem.isBcc ?? false,
- // 선종 적용 정보
- shipTypeCommon: updatedItem.shipTypeCommon ?? false,
- shipTypeAmax: updatedItem.shipTypeAmax ?? false,
- shipTypeSmax: updatedItem.shipTypeSmax ?? false,
- shipTypeVlcc: updatedItem.shipTypeVlcc ?? false,
- shipTypeLngc: updatedItem.shipTypeLngc ?? false,
- shipTypeCont: updatedItem.shipTypeCont ?? false,
- offshoreTypeCommon: updatedItem.offshoreTypeCommon ?? false,
- offshoreTypeFpso: updatedItem.offshoreTypeFpso ?? false,
- offshoreTypeFlng: updatedItem.offshoreTypeFlng ?? false,
- offshoreTypeFpu: updatedItem.offshoreTypeFpu ?? false,
- offshoreTypePlatform: updatedItem.offshoreTypePlatform ?? false,
- offshoreTypeWtiv: updatedItem.offshoreTypeWtiv ?? false,
- offshoreTypeGom: updatedItem.offshoreTypeGom ?? false,
};
// debugSuccess('Vendor Pool 업데이트 완료', { id, result: transformedData });
@@ -994,7 +812,7 @@ export async function deleteVendorPool(id: number): Promise<boolean> {
// debugLog('Vendor Pool 삭제 시작', { id });
// 데이터베이스에서 삭제
- const result = await db
+ await db
.delete(vendorPool)
.where(eq(vendorPool.id, id));
@@ -1032,3 +850,360 @@ export async function deleteVendorPool(id: number): Promise<boolean> {
return false;
}
}
+
+export type ImportResultItem = {
+ rowNumber: number;
+ status: 'success' | 'error' | 'duplicate' | 'warning';
+ message: string;
+ data?: any;
+}
+
+export type ImportResult = {
+ totalRows: number;
+ successCount: number;
+ errorCount: number;
+ duplicateCount: number;
+ items: ImportResultItem[];
+}
+
+// Boolean 값 파싱 (서버 사이드용 - 클라이언트 유틸과 유사하게 동작)
+function parseBooleanServer(value: any): boolean {
+ if (typeof value === 'boolean') return value;
+ const strValue = String(value).toLowerCase().trim();
+ return strValue === 'true' || strValue === '1' || strValue === 'yes' ||
+ strValue === 'o' || strValue === 'y' || strValue === '참';
+}
+
+/**
+ * Vendor Pool 일괄 입력 처리 (Bulk Import)
+ * - 한 번에 여러 행을 입력받아 처리
+ * - Bulk Lookup으로 성능 최적화
+ */
+export async function processBulkImport(rows: Record<string, any>[], registrant: string): Promise<ImportResult> {
+ const result: ImportResult = {
+ totalRows: rows.length,
+ successCount: 0,
+ errorCount: 0,
+ duplicateCount: 0,
+ items: []
+ };
+
+ if (rows.length === 0) {
+ return result;
+ }
+
+ try {
+ // 1. Lookup을 위한 고유 코드 추출
+ const materialGroupCodes = new Set<string>();
+ const vendorCodes = new Set<string>();
+
+ rows.forEach(row => {
+ if (row.materialGroupCode) materialGroupCodes.add(String(row.materialGroupCode).trim());
+ if (row.vendorCode) vendorCodes.add(String(row.vendorCode).trim());
+ });
+
+ // 2. Bulk Fetch (DB 조회)
+ const materialGroupMap = new Map<string, string>();
+ if (materialGroupCodes.size > 0) {
+ const materialGroups = await db
+ .select({
+ code: MATERIAL_GROUP_MASTER.materialGroupCode,
+ name: MATERIAL_GROUP_MASTER.materialGroupDescription
+ })
+ .from(MATERIAL_GROUP_MASTER)
+ .where(inArray(MATERIAL_GROUP_MASTER.materialGroupCode, Array.from(materialGroupCodes)));
+
+ materialGroups.forEach(mg => {
+ if (mg.code && mg.name) {
+ materialGroupMap.set(mg.code.trim(), mg.name);
+ }
+ });
+ }
+
+ const vendorMap = new Map<string, string>();
+ if (vendorCodes.size > 0) {
+ const vendorList = await db
+ .select({
+ code: vendors.vendorCode,
+ name: vendors.vendorName
+ })
+ .from(vendors)
+ .where(inArray(vendors.vendorCode, Array.from(vendorCodes)));
+
+ vendorList.forEach(v => {
+ if (v.code && v.name) {
+ vendorMap.set(v.code.trim(), v.name);
+ }
+ });
+ }
+
+ // 2.5. 중복 검사를 위한 기존 데이터 조회
+ const targetVendorNames = new Set<string>();
+ rows.forEach(row => {
+ let vName = row.vendorName;
+ if (!vName && row.vendorCode) {
+ // vendorMap에서 조회
+ vName = vendorMap.get(String(row.vendorCode).trim());
+ }
+ if (vName) {
+ targetVendorNames.add(String(vName).trim());
+ }
+ });
+
+ const existingRecordsMap = new Map<string, number>();
+ if (targetVendorNames.size > 0) {
+ const existingRecords = await db
+ .select({
+ id: vendorPool.id,
+ constructionSector: vendorPool.constructionSector,
+ htDivision: vendorPool.htDivision,
+ discipline: vendorPool.discipline,
+ materialGroupCode: vendorPool.materialGroupCode,
+ vendorName: vendorPool.vendorName
+ })
+ .from(vendorPool)
+ .where(inArray(vendorPool.vendorName, Array.from(targetVendorNames)));
+
+ existingRecords.forEach(rec => {
+ // Key: constructionSector|htDivision|discipline|materialGroupCode|vendorName (trim 처리)
+ const cs = rec.constructionSector.trim();
+ const ht = rec.htDivision.trim();
+ const d = rec.discipline ? rec.discipline.trim() : '';
+ const m = rec.materialGroupCode ? rec.materialGroupCode.trim() : '';
+ const v = rec.vendorName ? rec.vendorName.trim() : '';
+ const key = `${cs}|${ht}|${d}|${m}|${v}`;
+ existingRecordsMap.set(key, rec.id);
+ });
+ }
+
+ // 3. 데이터 처리 및 검증
+ const validInsertRows: any[] = [];
+ const validUpdateRows: { id: number; data: any; rowNumber: number }[] = [];
+ const currentTimestamp = new Date();
+
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+ const rowNumber = i + 1;
+ const vendorPoolData: any = {};
+
+ // 기본 필드 매핑 및 타입 변환
+ const booleanFields = ['faTarget', 'isBlacklist', 'isBcc'];
+
+ Object.keys(row).forEach(key => {
+ const value = row[key];
+ if (booleanFields.includes(key)) {
+ vendorPoolData[key] = parseBooleanServer(value);
+ } else if (value === '' || value === undefined || value === null) {
+ vendorPoolData[key] = null;
+ } else {
+ vendorPoolData[key] = String(value);
+ }
+ });
+
+ // Enrichment (자동완성)
+ if (vendorPoolData.materialGroupCode && !vendorPoolData.materialGroupName) {
+ const mappedName = materialGroupMap.get(String(vendorPoolData.materialGroupCode).trim());
+ if (mappedName) {
+ vendorPoolData.materialGroupName = mappedName;
+ }
+ }
+
+ if (vendorPoolData.vendorCode && !vendorPoolData.vendorName) {
+ const mappedName = vendorMap.get(String(vendorPoolData.vendorCode).trim());
+ if (mappedName) {
+ vendorPoolData.vendorName = mappedName;
+ }
+ }
+
+ // 필수 필드 검증 (1차 검증)
+ // 키 필드가 null이면 실패 처리
+ const keyFields = [
+ { key: 'constructionSector', label: '공사부문' },
+ { key: 'htDivision', label: 'H/T구분' },
+ { key: 'discipline', label: '설계공종' },
+ { key: 'materialGroupCode', label: '자재그룹코드' }
+ ];
+
+ const missingKeyFields = keyFields
+ .filter(field => !vendorPoolData[field.key])
+ .map(field => field.label);
+
+ // vendorName은 vendorCode가 있으면 자동완성되므로, enrichment 후에 체크
+ if (!vendorPoolData.vendorName) {
+ missingKeyFields.push('협력업체명');
+ }
+
+ if (missingKeyFields.length > 0) {
+ result.errorCount++;
+ result.items.push({
+ rowNumber,
+ status: 'error',
+ message: `필수 키 필드 누락: ${missingKeyFields.join(', ')}`,
+ data: {
+ vendorName: vendorPoolData.vendorName,
+ materialGroupName: vendorPoolData.materialGroupName,
+ discipline: vendorPoolData.discipline,
+ }
+ });
+ continue;
+ }
+
+ // 데이터 형식 검증
+ const validationErrors: string[] = [];
+
+ 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) {
+ result.errorCount++;
+ result.items.push({
+ rowNumber,
+ status: 'error',
+ message: `검증 실패: ${validationErrors.join(', ')}`,
+ data: {
+ vendorName: vendorPoolData.vendorName,
+ materialGroupName: vendorPoolData.materialGroupName,
+ discipline: vendorPoolData.discipline,
+ }
+ });
+ continue;
+ }
+
+ // 메타데이터 추가
+ vendorPoolData.lastModifier = registrant;
+ vendorPoolData.lastModifiedDate = currentTimestamp;
+
+ // 기본값 처리
+ if (vendorPoolData.faTarget === undefined) vendorPoolData.faTarget = false;
+ if (vendorPoolData.isBlacklist === undefined) vendorPoolData.isBlacklist = false;
+ if (vendorPoolData.isBcc === undefined) vendorPoolData.isBcc = false;
+
+ // 중복 검사 (2차 검증)
+ // [공사부문, H/T, 설계공종, 자재그룹코드, 협력업체명]
+ const checkConstructionSector = String(vendorPoolData.constructionSector).trim();
+ const checkHtDivision = String(vendorPoolData.htDivision).trim();
+ const checkDiscipline = String(vendorPoolData.discipline).trim();
+ const checkMaterialGroupCode = String(vendorPoolData.materialGroupCode).trim();
+ const checkVendorName = String(vendorPoolData.vendorName).trim();
+
+ const duplicateKey = `${checkConstructionSector}|${checkHtDivision}|${checkDiscipline}|${checkMaterialGroupCode}|${checkVendorName}`;
+
+ if (existingRecordsMap.has(duplicateKey)) {
+ const existingId = existingRecordsMap.get(duplicateKey)!;
+ validUpdateRows.push({
+ id: existingId,
+ data: vendorPoolData,
+ rowNumber
+ });
+ } else {
+ // 신규 등록 (3차 검증 - Insert)
+ vendorPoolData.registrant = registrant;
+ vendorPoolData.registrationDate = currentTimestamp;
+
+ validInsertRows.push({
+ rowNumber,
+ data: vendorPoolData
+ });
+ }
+ }
+
+ // 4. Bulk Execution
+
+ // 4.1 Updates (Sequential to avoid deadlock, though simple updates usually fine)
+ // 업데이트는 개별적으로 수행해야 함 (값들이 다를 수 있으므로)
+ for (const updateItem of validUpdateRows) {
+ try {
+ await db.update(vendorPool)
+ .set(updateItem.data)
+ .where(eq(vendorPool.id, updateItem.id));
+
+ result.duplicateCount++; // 업데이트된 건수를 중복(업데이트) 카운트로 처리
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ result.errorCount++;
+ result.items.push({
+ rowNumber: updateItem.rowNumber,
+ status: 'error',
+ message: `데이터 업데이트 실패: ${errorMsg}`,
+ data: {
+ vendorName: updateItem.data.vendorName
+ }
+ });
+ }
+ }
+
+ // 4.2 Inserts (Batch)
+ if (validInsertRows.length > 0) {
+ // 500개씩 나누어 처리 (Batch Insert)
+ const BATCH_SIZE = 500;
+ for (let i = 0; i < validInsertRows.length; i += BATCH_SIZE) {
+ const batch = validInsertRows.slice(i, i + BATCH_SIZE);
+ const batchData = batch.map(item => item.data);
+
+ try {
+ await db.insert(vendorPool).values(batchData);
+ result.successCount += batch.length;
+ } catch (err) {
+ // 배치 실패 시 개별 재시도
+ console.error("Batch insert error, falling back to individual insert:", err);
+
+ // Fallback to individual insert for this batch to identify errors
+ for (const item of batch) {
+ try {
+ await db.insert(vendorPool).values(item.data);
+ result.successCount++;
+ } catch (innerErr) {
+ const innerErrorMsg = innerErr instanceof Error ? innerErr.message : String(innerErr);
+
+ if (innerErrorMsg.includes('unique_vendor_pool_combination') ||
+ innerErrorMsg.includes('duplicate key value')) {
+ // DB 레벨에서 중복 발생 시 (거의 발생 안해야 함, 위에서 체크했으므로)
+ // 하지만 동시성 이슈 등으로 발생 가능
+ result.errorCount++;
+ result.items.push({
+ rowNumber: item.rowNumber,
+ status: 'error',
+ message: `중복 데이터 발생 (동시성 이슈 가능성): ${innerErrorMsg}`,
+ data: {
+ vendorName: item.data.vendorName
+ }
+ });
+ } else {
+ result.errorCount++;
+ result.items.push({
+ rowNumber: item.rowNumber,
+ status: 'error',
+ message: `데이터 저장 실패: ${innerErrorMsg}`,
+ data: {
+ vendorName: item.data.vendorName,
+ materialGroupName: item.data.materialGroupName,
+ discipline: item.data.discipline,
+ }
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // 캐시 무효화
+ if (result.successCount > 0 || result.duplicateCount > 0) {
+ revalidateTag('vendor-pool-list');
+ revalidateTag('vendor-pool-stats');
+ }
+
+ return result;
+
+ } catch (error) {
+ console.error("Process bulk import error:", error);
+ throw error;
+ }
+}