diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-25 22:04:56 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-25 22:04:56 +0900 |
| commit | 2b59582194fc5c23140f52c42c793c324856a35e (patch) | |
| tree | 0db8ef0e913b3a44dfd6e3e20fe92b8e4984aeba /lib/vendor-pool/service.ts | |
| parent | 835df8ddc115ffa74414db2a4fab7efc0d0056a9 (diff) | |
(김준회) 벤더풀&AVL 구매 추가요청사항 반영
Diffstat (limited to 'lib/vendor-pool/service.ts')
| -rw-r--r-- | lib/vendor-pool/service.ts | 687 |
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; + } +} |
