diff options
Diffstat (limited to 'lib/soap/ecc/mapper')
| -rw-r--r-- | lib/soap/ecc/mapper/bidding-and-pr-mapper.ts | 158 |
1 files changed, 104 insertions, 54 deletions
diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts index adbb3e1e..1a5089eb 100644 --- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts @@ -21,7 +21,8 @@ import { PR_INFORMATION_T_BID_HEADER, PR_INFORMATION_T_BID_ITEM, } from '@/db/schema/ECC/ecc'; -import { inArray, max, sql, eq } from 'drizzle-orm'; +import { inArray, max, sql, eq, like } from 'drizzle-orm'; +import { users } from '@/db/schema/users'; import { findUserInfoByEKGRP, findProjectInfoByPSPID, @@ -48,22 +49,22 @@ export type PrItemForBiddingData = typeof prItemsForBidding.$inferInsert; /** * Bidding 코드 생성 함수 (배치 처리용) - * 형식: BID{EKGRP}{00001} + * 형식: E/F/G{담당자코드3자리}{년도2자리}{시퀀스4자리}-01 * 기존 ANFNR은 기존 biddingNumber 사용, 새로운 ANFNR만 새 코드 생성 */ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<string, string>> { try { debugLog('Bidding 코드 배치 생성 시작', { headerCount: eccHeaders.length }); - + const biddingCodeMap = new Map<string, string>(); - + // 1) 먼저 기존 ANFNR들의 biddingNumber 조회 const anfnrList = eccHeaders.map(h => h.ANFNR).filter(Boolean); if (anfnrList.length > 0) { const existingResult = await db - .select({ - ANFNR: biddings.ANFNR, - biddingNumber: biddings.biddingNumber + .select({ + ANFNR: biddings.ANFNR, + biddingNumber: biddings.biddingNumber }) .from(biddings) .where(inArray(biddings.ANFNR, anfnrList)); @@ -75,62 +76,108 @@ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<str } } } - + // 2) 새로운 ANFNR들만 필터링 (기존에 없는 것들) - const newHeaders = eccHeaders.filter(header => + const newHeaders = eccHeaders.filter(header => header.ANFNR && !biddingCodeMap.has(header.ANFNR) ); - + if (newHeaders.length === 0) { - debugSuccess('모든 ANFNR이 기존에 존재함', { - totalCodes: biddingCodeMap.size + debugSuccess('모든 ANFNR이 기존에 존재함', { + totalCodes: biddingCodeMap.size }); return biddingCodeMap; } - - // 3) 새로운 ANFNR들을 EKGRP별로 그룹핑 - const ekgrpGroups = new Map<string, ECCBidHeader[]>(); + + // 3) 새로운 ANFNR들을 담당자별로 그룹핑 (EKGRP 기반) + const managerGroups = new Map<string, ECCBidHeader[]>(); for (const header of newHeaders) { - const ekgrp = header.EKGRP || 'UNKNOWN'; - if (!ekgrpGroups.has(ekgrp)) { - ekgrpGroups.set(ekgrp, []); + // EKGRP를 직접 사용하여 3자리 담당자 코드 생성 + let managerCode = '000'; + const ekgrp = header.EKGRP || ''; + + if (ekgrp.length >= 3) { + managerCode = ekgrp.substring(0, 3).toUpperCase(); + } else if (ekgrp.length > 0) { + managerCode = ekgrp.padEnd(3, '0').toUpperCase(); } - ekgrpGroups.get(ekgrp)!.push(header); + + if (!managerGroups.has(managerCode)) { + managerGroups.set(managerCode, []); + } + managerGroups.get(managerCode)!.push(header); } - - // 4) EKGRP별로 새 코드 생성 - for (const [ekgrp, headers] of ekgrpGroups) { - // 해당 EKGRP의 현재 최대 시퀀스 조회 - const maxResult = await db - .select({ - maxBiddingNumber: max(biddings.biddingNumber) + + // 계약 타입별 접두사 설정 (ECC에서는 기본적으로 일반계약으로 가정) + const contractType = 'general'; + const typePrefix = { + 'general': 'E', // 일반계약 + 'unit_price': 'F', // 단가계약 + 'sale': 'G', // 매각계약 + }; + const prefix = typePrefix[contractType as keyof typeof typePrefix] || 'E'; + + // 현재 년도 2자리 + const currentYear = new Date().getFullYear().toString().slice(-2); + + // 4) 담당자별로 새 코드 생성 + for (const [managerCode, headers] of managerGroups) { + const yearPrefix = `${prefix}${managerCode}${currentYear}`; + + // 해당 담당자의 현재 최대 시퀀스 조회 + const prefixLength = yearPrefix.length + 4; + const result = await db + .select({ + maxNumber: sql<string>`MAX(LEFT(${biddings.biddingNumber}, ${prefixLength}))` }) .from(biddings) - .where(sql`${biddings.biddingNumber} LIKE ${`B${ekgrp}%`}`); - - let nextSeq = 1; - if (maxResult[0]?.maxBiddingNumber) { - const prefix = `B${ekgrp}`; - const currentCode = maxResult[0].maxBiddingNumber; - if (currentCode.startsWith(prefix)) { - const seqPart = currentCode.substring(prefix.length); - const currentSeq = parseInt(seqPart, 10); - if (!isNaN(currentSeq)) { - nextSeq = currentSeq + 1; + .where(like(biddings.biddingNumber, `${yearPrefix}%`)); + + // 시퀀스 생성 헬퍼 함수 + const generateNextSequence = (lastSequence: string | null): string => { + if (!lastSequence) return '0001'; + const seqNum = parseInt(lastSequence, 10) || 0; + return (seqNum + 1).toString().padStart(4, '0'); + }; + + const nextSequence = generateNextSequence(result[0]?.maxNumber?.slice(-4) || null); + + // 동일 담당자 내에서 순차적으로 새 코드 생성 + for (const header of headers) { + const biddingNumber = `${yearPrefix}${nextSequence}-01`; + + // 중복 확인 및 재시도 (동시성 문제 방지) + let finalBiddingNumber = biddingNumber; + let attempts = 0; + const maxRetries = 5; + + while (attempts < maxRetries) { + const existing = await db + .select({ id: biddings.id }) + .from(biddings) + .where(eq(biddings.biddingNumber, finalBiddingNumber)) + .limit(1); + + if (existing.length === 0) { + break; // 중복 없음 + } + + // 중복이 발견되면 시퀀스 증가 + const currentSeq = parseInt(finalBiddingNumber.slice(-6, -3), 10); + const newSeq = (currentSeq + 1).toString().padStart(4, '0'); + finalBiddingNumber = `${yearPrefix}${newSeq}-01`; + attempts++; + + if (attempts >= maxRetries) { + throw new Error(`Failed to generate unique bidding number after ${maxRetries} attempts`); } } - } - - // 동일 EKGRP 내에서 순차적으로 새 코드 생성 - for (const header of headers) { - const seqString = nextSeq.toString().padStart(5, '0'); - const biddingCode = `B${ekgrp}${seqString}`; - biddingCodeMap.set(header.ANFNR || '', biddingCode); - nextSeq++; // 다음 시퀀스로 증가 + + biddingCodeMap.set(header.ANFNR || '', finalBiddingNumber); } } - - debugSuccess('Bidding 코드 배치 생성 완료', { + + debugSuccess('Bidding 코드 배치 생성 완료', { totalCodes: biddingCodeMap.size, newCodes: newHeaders.length, existingCodes: eccHeaders.length - newHeaders.length @@ -138,13 +185,15 @@ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<str return biddingCodeMap; } catch (error) { debugError('Bidding 코드 배치 생성 중 오류 발생', { error }); - - // 오류 발생시 폴백: ANFNR 기반 코드 생성 + + // 오류 발생시 폴백: 기본 형식으로 코드 생성 const fallbackMap = new Map<string, string>(); + const currentYear = new Date().getFullYear().toString().slice(-2); + eccHeaders.forEach((header, index) => { - const ekgrp = header.EKGRP || 'UNKNOWN'; - const seqString = (index + 1).toString().padStart(5, '0'); - fallbackMap.set(header.ANFNR, `B${ekgrp}${seqString}`); + const seqString = (index + 1).toString().padStart(4, '0'); + const ekgrp = header.EKGRP || '000'; + fallbackMap.set(header.ANFNR, `E${ekgrp.substring(0, 3).toUpperCase()}${currentYear}${seqString}-01`); }); return fallbackMap; } @@ -195,6 +244,7 @@ export async function mapECCBiddingHeaderToBidding( // 매핑 const mappedData: BiddingData = { biddingNumber, // 생성된 Bidding 코드 + originalBiddingNumber: eccHeader.ANFNR || null, // 원입찰번호 revision: 0, // 기본 리비전 0 (I/F 해서 가져온 건 보낸 적 없으므로 0 고정) projectName, // 첫번째 PR Item의 PSPID로 찾은 프로젝트 이름 itemName, // 첫번째 PR Item의 MATNR로 조회한 자재명 @@ -204,13 +254,13 @@ export async function mapECCBiddingHeaderToBidding( // 계약 정보 - ECC에서 제공되지 않으므로 기본값 설정 contractType: 'general', // 일반계약 기본값 (notNull) biddingType: 'equipment', // 입찰유형 기본값 (notNull) - awardCount: 'single', // 낙찰수 기본값 (notNull) + awardCount: 'single', // 낙찰업체 수 기본값 (notNull) contractStartDate: null, // ECC에서 제공 X contractEndDate: null, // ECC에서 제공 X // 일정 관리 - ECC에서 제공되지 않음 preQuoteDate: null, - biddingRegistrationDate: null, + biddingRegistrationDate: new Date().toISOString(), // 입찰등록일 I/F 시점 등록(1120 이시원 프로 요청) submissionStartDate: null, submissionEndDate: null, evaluationDate: null, |
