summaryrefslogtreecommitdiff
path: root/lib/soap/ecc/mapper
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-24 02:52:34 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-24 02:52:34 +0000
commit58d700b925967bfe470c944b380b02b2140cab8a (patch)
treee445dc37215f048759542e198cf6c5f41ebb0d5c /lib/soap/ecc/mapper
parent26365ef08588d53b8c5d9c7cfaefb244536e6743 (diff)
(최겸) 구매 기술영업 결재 개발, 입찰 수정
Diffstat (limited to 'lib/soap/ecc/mapper')
-rw-r--r--lib/soap/ecc/mapper/bidding-and-pr-mapper.ts158
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,