diff options
Diffstat (limited to 'lib/soap/ecc/mapper/pcr-mapper.ts')
| -rw-r--r-- | lib/soap/ecc/mapper/pcr-mapper.ts | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/lib/soap/ecc/mapper/pcr-mapper.ts b/lib/soap/ecc/mapper/pcr-mapper.ts new file mode 100644 index 00000000..2733d6d4 --- /dev/null +++ b/lib/soap/ecc/mapper/pcr-mapper.ts @@ -0,0 +1,353 @@ +/** + * PCR (Purchase Change Request) 데이터를 ECC에서 수신하여 + * pcrPo, pcrPr 비즈니스 테이블로 매핑하는 mapper + * + * === ZMM_PCR 매핑 정보 === + * + * [pcrPo 매핑] + * - vendorId: LIFNR (공급업체) 조회 + * - changeType: PCR_TYPE (물량/Spec 변경 Type) + * - details: ZZSPEC (Specification) + * - project: PSPID (프로젝트 코드 문자열) + * - pcrRequestDate: PCR_REQ_DATE (PCR 요청일자) + * - poContractNumber: EBELN (구매오더) + * - revItemNumber: EBELP (구매오더 품번) + * - purchaseContractManager: EKGRP로 조회한 담당자명 (Oracle → employeeNumber → users.name) + * - pcrCreator: NAME (설계담당명) + * - poContractAmountBefore: NETWR (PO 금액) + * - poContractAmountAfter: ECC에서 제공 안됨 (TODO: NETPR * QTY_A로 계산 가능) + * - contractCurrency: WAERS (PO 통화) + * - pcrReason: REQUEST_RSN (Request 사유) + * - detailsReason: REQUEST_CD (Request 코드) + * + * [pcrPr 매핑] + * - materialNumber: MATNR (자재번호) + * - materialDetails: MAKTX (자재명) + * - quantityBefore/After: QTY_B/A (변경 전/후 수량) + * - weightBefore/After: T_WEIGHT_B/A (변경 전/후 Total 중량) + * - subcontractorWeightBefore/After: S_WEIGHT_B/A (변경 전/후 사급 중량) + * - supplierWeightBefore/After: C_WEIGHT_B/A (변경 전/후 도급 중량) + * - specDrawingBefore: ZZSPEC (Specification) + * - specDrawingAfter: ECC에서 제공 안됨 + * - initialPoContractDate: ZACC_DT (구매담당자 PR 접수일) + * - specChangeDate: ERDAT (물량 변경일) + * - poContractModifiedDate: ZAEDAT (도면변경일) + * - confirmationDate: LFDAT (PR 납품일) + * - designManager: NAME (설계담당명) + * - poContractNumber: EBELN (구매오더) + * + * [미사용 ZMM_PCR 필드] + * - BANFN, BNFPO: 구매요청, 구매요청 품번 (PO 기준이므로 미사용) + * - ZSPEC_NUM: POS (pcrPr 스키마에 해당 컬럼 없음) + * - MEINS: 단위 (pcrPr 스키마에 해당 컬럼 없음) + * - MEINS_W: 중량 단위 (pcrPr 스키마에 해당 컬럼 없음) + * - NETPR, PEINH: PO 단가, Price Unit (금액 계산에 사용 가능하나 현재 미사용) + * - POSID: WBS (PSPID 사용) + * - EKGRP: 구매그룹 코드 (EKNAM 사용) + * - DEPTCD, DEPTNM, EMPID: 설계부서/부서명/담당자ID (NAME 사용) + * - NAME1: 공급업체명 (LIFNR로 조회하므로 미사용) + * - ZPROC_IND: PR 상태 (미사용) + * - WERKS: PLANT (미사용) + * - DOKNR, DOKAR, DOKTL, DOKVR: ECC 내부 도면 정보 (미사용) + * - ZCHG_NO: ECC 내부 PR 수정번호 (미사용) + */ + +import { debugLog, debugSuccess, debugError } from '@/lib/debug-utils'; +import db from '@/db/db'; +import { pcrPo, pcrPr } from '@/db/schema/pcr'; +import { ZMM_PCR } from '@/db/schema/ECC/ecc'; +import { eq } from 'drizzle-orm'; +import { + findVendorIdByLIFNR, + findUserInfoByEKGRP, +} from './common-mapper-utils'; +import { parseSAPDate, parseNumericString } from '@/lib/soap/utils'; + +// ECC 데이터 타입 정의 +export type ECCPcrData = typeof ZMM_PCR.$inferInsert; + +// 비즈니스 테이블 데이터 타입 정의 +export type PcrPoData = typeof pcrPo.$inferInsert; +export type PcrPrData = typeof pcrPr.$inferInsert; + +/** + * PCR_TYPE을 원본 그대로 반환 + * ECC에서 'DE' 등 예상하지 못한 다양한 코드가 들어올 수 있으므로 + * 별도 매핑 없이 원본 값을 그대로 사용 + */ +function getPcrType(pcrType: string | null | undefined): string | null { + if (!pcrType || !pcrType.trim()) return null; + return pcrType.trim(); +} + + + + +/** + * ECC PCR 데이터를 pcrPo 테이블로 매핑 + * 같은 PCR_REQ를 가진 여러 레코드 중 대표 레코드(첫번째)를 기준으로 매핑 + */ +export async function mapECCPcrToPcrPo( + eccPcrRecords: ECCPcrData[] +): Promise<PcrPoData> { + debugLog('ECC PCR -> pcrPo 매핑 시작', { + pcrReq: eccPcrRecords[0]?.PCR_REQ, + recordCount: eccPcrRecords.length + }); + + // 대표 레코드 (첫번째) + const representative = eccPcrRecords[0]; + if (!representative) { + throw new Error('매핑할 PCR 레코드가 없습니다.'); + } + + // 협력업체 ID 조회 + const vendorId = await findVendorIdByLIFNR(representative.LIFNR); + + // 담당자 정보 조회 (EKGRP 기반) + const userInfo = await findUserInfoByEKGRP(representative.EKGRP || null); + const inChargeUserId = userInfo?.userId || 1; // 기본값: 시스템 사용자 + const inChargeUserName = userInfo?.userName || null; // 담당자 이름 + + // PCR 요청일자 파싱 + const pcrRequestDate = parseSAPDate(representative.PCR_REQ_DATE); + if (!pcrRequestDate) { + throw new Error('PCR_REQ_DATE가 유효하지 않습니다.'); + } + + // 금액 매핑 + // NETWR: PO 금액을 변경 전 금액으로 사용 + // 변경 후 금액은 ECC에서 별도 필드로 제공되지 않음 + // 필요시 NETPR(PO 단가) * QTY_A(변경 후 수량) 등으로 계산 가능. 우선 I/F 값을 쓰도록 함 + const poContractAmountBefore = parseNumericString(representative.NETWR); + + // 매핑 + const mappedData: PcrPoData = { + vendorId, // LIFNR로 조회한 vendor ID + pcrApprovalStatus: '승인대기', // 기본값: 승인대기 + changeType: getPcrType(representative.PCR_TYPE) || 'OTHER', // PCR_TYPE 원본 값 사용 (Q, W, S, QW, DE 등) + details: representative.ZZSPEC || null, // ZZSPEC: Specification + project: representative.PSPID || null, // PSPID: 프로젝트 코드 (문자열) + pcrRequestDate, // PCR_REQ_DATE: PCR 요청일자 + poContractNumber: representative.EBELN || '', // EBELN: 구매오더 + revItemNumber: representative.EBELP || null, // EBELP: 구매오더 품번 + purchaseContractManager: inChargeUserName, // EKGRP로 조회한 담당자명 + pcrCreator: representative.NAME || null, // NAME: 설계담당명 (PCR 생성자) + poContractAmountBefore, // NETWR: PO 금액 + poContractAmountAfter: null, // ECC에서 제공되지 않음. TODO: 필요시 NETPR * QTY_A로 계산 + contractCurrency: representative.WAERS || 'KRW', // WAERS: PO 통화 + pcrReason: representative.REQUEST_CD || null, // REQUEST_CD: Request 코드 + detailsReason: representative.REQUEST_RSN || null, // REQUEST_RSN: Request 사유 + rejectionReason: null, // ECC에서 제공되지 않음 + pcrResponseDate: null, // ECC에서 제공되지 않음 (응답일은 나중에 업데이트) + createdBy: inChargeUserId, // EKGRP로 조회한 담당자 ID (Oracle → employeeNumber → users.id) + updatedBy: inChargeUserId, // EKGRP로 조회한 담당자 ID + createdAt: new Date(), + updatedAt: new Date(), + }; + + debugSuccess('ECC PCR -> pcrPo 매핑 완료', { + pcrReq: representative.PCR_REQ, + poContractNumber: mappedData.poContractNumber, + }); + + return mappedData; +} + +/** + * ECC PCR 데이터를 pcrPr 테이블로 매핑 + */ +export async function mapECCPcrToPcrPr( + eccPcr: ECCPcrData +): Promise<PcrPrData> { + debugLog('ECC PCR -> pcrPr 매핑 시작', { + pcrReq: eccPcr.PCR_REQ, + pcrReqSeq: eccPcr.PCR_REQ_SEQ, + }); + + // 담당자 정보 조회 (EKGRP 기반) + const userInfo = await findUserInfoByEKGRP(eccPcr.EKGRP || null); + const inChargeUserId = userInfo?.userId || 1; // 기본값: 시스템 사용자 + + const mappedData: PcrPrData = { + materialNumber: eccPcr.MATNR || '', // MATNR: 자재번호 + materialDetails: eccPcr.MAKTX || null, // MAKTX: 자재명 + quantityBefore: parseNumericString(eccPcr.QTY_B), // QTY_B: 변경 전 수량 + quantityAfter: parseNumericString(eccPcr.QTY_A), // QTY_A: 변경 후 수량 + weightBefore: parseNumericString(eccPcr.T_WEIGHT_B), // T_WEIGHT_B: 변경 전 Total 중량 + weightAfter: parseNumericString(eccPcr.T_WEIGHT_A), // T_WEIGHT_A: 변경 후 Total 중량 + subcontractorWeightBefore: parseNumericString(eccPcr.S_WEIGHT_B), // S_WEIGHT_B: 변경 전 사급 중량 + subcontractorWeightAfter: parseNumericString(eccPcr.S_WEIGHT_A), // S_WEIGHT_A: 변경 후 사급 중량 + supplierWeightBefore: parseNumericString(eccPcr.C_WEIGHT_B), // C_WEIGHT_B: 변경 전 도급 중량 + supplierWeightAfter: parseNumericString(eccPcr.C_WEIGHT_A), // C_WEIGHT_A: 변경 후 도급 중량 + specDrawingBefore: eccPcr.ZZSPEC || null, // 스펙도면 이전/이후는 제공해주는 게 없음 + specDrawingAfter: eccPcr.ZZSPEC || null, // 일단 ZZSPEC에서 문자열 + initialPoContractDate: parseSAPDate(eccPcr.ZACC_DT), // ZACC_DT: 구매담당자 PR 접수일 + specChangeDate: parseSAPDate(eccPcr.ERDAT), // ERDAT: 물량 변경일 + poContractModifiedDate: parseSAPDate(eccPcr.ZAEDAT), // ZAEDAT: 도면변경일 (PO/계약 수정일로 사용) + confirmationDate: parseSAPDate(eccPcr.LFDAT), // LFDAT: PR 납품일 + designManager: eccPcr.NAME || null, // NAME: 설계담당명 + poContractNumber: eccPcr.EBELN || '', // EBELN: 구매오더 + createdBy: inChargeUserId, // EKGRP로 조회한 담당자 ID (Oracle → employeeNumber → users.id) + updatedBy: inChargeUserId, // EKGRP로 조회한 담당자 ID + createdAt: new Date(), + updatedAt: new Date(), + }; + + debugSuccess('ECC PCR -> pcrPr 매핑 완료', { + pcrReq: eccPcr.PCR_REQ, + pcrReqSeq: eccPcr.PCR_REQ_SEQ, + materialNumber: mappedData.materialNumber, + }); + + return mappedData; +} + +/** + * ECC PCR 데이터를 pcrPo/pcrPr 테이블로 일괄 매핑 및 저장 + */ +export async function mapAndSaveECCPcrData( + eccPcrRecords: ECCPcrData[] +): Promise<{ success: boolean; message: string; processedCount: number }> { + debugLog('ECC PCR 데이터 일괄 매핑 및 저장 시작', { + recordCount: eccPcrRecords.length, + }); + + try { + const result = await db.transaction(async (tx) => { + // PCR_REQ별로 그룹핑 + const pcrGroups = new Map<string, ECCPcrData[]>(); + + for (const eccPcr of eccPcrRecords) { + const pcrReq = eccPcr.PCR_REQ; + if (!pcrReq) { + debugError('PCR_REQ가 없는 레코드는 건너뜀', { eccPcr }); + continue; + } + + if (!pcrGroups.has(pcrReq)) { + pcrGroups.set(pcrReq, []); + } + pcrGroups.get(pcrReq)!.push(eccPcr); + } + + debugLog(`PCR_REQ 그룹 수: ${pcrGroups.size}`); + + let processedCount = 0; + + // PCR_REQ별로 처리 + for (const [pcrReq, pcrRecords] of pcrGroups) { + try { + // 1) pcrPo 매핑 및 저장 + const pcrPoData = await mapECCPcrToPcrPo(pcrRecords); + + // 기존에 동일한 poContractNumber가 있는지 확인 + const existingPcrPo = await tx + .select({ id: pcrPo.id }) + .from(pcrPo) + .where(eq(pcrPo.poContractNumber, pcrPoData.poContractNumber)) + .limit(1); + + if (existingPcrPo.length > 0) { + // 기존 레코드 업데이트 + debugLog(`기존 PCR_PO 업데이트: ${pcrPoData.poContractNumber}`); + await tx + .update(pcrPo) + .set({ + ...pcrPoData, + updatedAt: new Date(), + }) + .where(eq(pcrPo.id, existingPcrPo[0].id)); + } else { + // 신규 레코드 삽입 + debugLog(`신규 PCR_PO 삽입: ${pcrPoData.poContractNumber}`); + await tx + .insert(pcrPo) + .values(pcrPoData); + } + + // 2) pcrPr 매핑 및 저장 (각 PCR_REQ_SEQ별로) + const pcrPrDataList: PcrPrData[] = []; + + for (const eccPcr of pcrRecords) { + const pcrPrData = await mapECCPcrToPcrPr(eccPcr); + pcrPrDataList.push(pcrPrData); + } + + // 기존 pcrPr 레코드 삭제 (동일한 poContractNumber) + await tx + .delete(pcrPr) + .where(eq(pcrPr.poContractNumber, pcrPoData.poContractNumber)); + + // 신규 pcrPr 레코드 삽입 + if (pcrPrDataList.length > 0) { + await tx.insert(pcrPr).values(pcrPrDataList); + debugLog(`PCR_PR ${pcrPrDataList.length}개 삽입 완료`); + } + + processedCount++; + } catch (error) { + debugError(`PCR_REQ ${pcrReq} 처리 중 오류 발생`, { error }); + // 트랜잭션 롤백을 위해 에러를 다시 throw + throw error; + } + } + + return { processedCount }; + }); + + debugSuccess('ECC PCR 데이터 일괄 처리 완료', { + processedCount: result.processedCount, + }); + + return { + success: true, + message: `${result.processedCount}개의 PCR 데이터가 성공적으로 처리되었습니다.`, + processedCount: result.processedCount, + }; + } catch (error) { + debugError('ECC PCR 데이터 처리 중 오류 발생', error); + return { + success: false, + message: + error instanceof Error + ? error.message + : '알 수 없는 오류가 발생했습니다.', + processedCount: 0, + }; + } +} + +/** + * ECC PCR 데이터 유효성 검증 + */ +export function validateECCPcrData( + eccPcrRecords: ECCPcrData[] +): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + for (const record of eccPcrRecords) { + // 필수 필드 검증 + if (!record.PCR_REQ) { + errors.push(`필수 필드 누락: PCR_REQ`); + } + if (!record.PCR_REQ_SEQ) { + errors.push(`필수 필드 누락: PCR_REQ_SEQ (PCR_REQ: ${record.PCR_REQ})`); + } + if (!record.PCR_REQ_DATE) { + errors.push(`필수 필드 누락: PCR_REQ_DATE (PCR_REQ: ${record.PCR_REQ})`); + } + if (!record.EBELN) { + errors.push(`필수 필드 누락: EBELN (PCR_REQ: ${record.PCR_REQ})`); + } + if (!record.EBELP) { + errors.push(`필수 필드 누락: EBELP (PCR_REQ: ${record.PCR_REQ})`); + } + } + + return { + isValid: errors.length === 0, + errors, + }; +} + |
