diff options
| -rw-r--r-- | app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts | 20 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/common-mapper-utils.ts | 113 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/pcr-mapper.ts | 353 | ||||
| -rw-r--r-- | lib/soap/utils.ts | 49 |
4 files changed, 520 insertions, 15 deletions
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts index 623fa29e..2e6f1729 100644 --- a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts +++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts @@ -13,6 +13,9 @@ import { import { bulkUpsert } from "@/lib/soap/batch-utils"; +import { + mapAndSaveECCPcrData, +} from '@/lib/soap/ecc/mapper/pcr-mapper'; type PCRData = typeof ZMM_PCR.$inferInsert; @@ -65,12 +68,23 @@ export async function POST(request: NextRequest) { } } - // 5) 데이터베이스 저장 + // 5) 데이터베이스 저장 (ZMM_PCR 수신 테이블) await saveToDatabase(processedData); - console.log(`🎉 처리 완료: ${processedData.length}개 PCR 데이터`); + console.log(`🎉 ZMM_PCR 저장 완료: ${processedData.length}개 PCR 데이터`); + + // 6) 비즈니스 테이블로 매핑 및 저장 (pcrPo, pcrPr) + console.log('📋 비즈니스 테이블 매핑 시작...'); + const mappingResult = await mapAndSaveECCPcrData(processedData); + + if (mappingResult.success) { + console.log(`✅ 비즈니스 테이블 매핑 완료: ${mappingResult.processedCount}개 PCR`); + } else { + console.error(`❌ 비즈니스 테이블 매핑 실패: ${mappingResult.message}`); + // 매핑 실패해도 수신 테이블에는 저장되었으므로 경고만 출력 + } - // 6) 성공 응답 반환 - 각 PCR 데이터에 대해 ZMM_RT 객체 생성 + // 7) 성공 응답 반환 - 각 PCR 데이터에 대해 ZMM_RT 객체 생성 const responseZmmRtList = processedData.map((pcrData) => ({ PCR_REQ: pcrData.PCR_REQ || '', PCR_REQ_SEQ: pcrData.PCR_REQ_SEQ || '', diff --git a/lib/soap/ecc/mapper/common-mapper-utils.ts b/lib/soap/ecc/mapper/common-mapper-utils.ts index 8558f058..cf5e0fdb 100644 --- a/lib/soap/ecc/mapper/common-mapper-utils.ts +++ b/lib/soap/ecc/mapper/common-mapper-utils.ts @@ -12,14 +12,19 @@ import { debugLog, debugSuccess, debugError } from '@/lib/debug-utils'; import db from '@/db/db'; -import { users } from '@/db/schema'; +import { users, vendors } from '@/db/schema'; import { projects } from '@/db/schema/projects'; import { EQUP_MASTER_MATL_CHARASGN } from '@/db/schema/MDG/mdg'; import { eq } from 'drizzle-orm'; +import { oracleKnex } from '@/lib/oracle-db/db'; /** * 담당자 정보 조회 함수 (EKGRP 기반) - * EKGRP(구매그룹코드)를 users 테이블의 userCode와 매칭하여 사용자 정보 반환 + * + * 조회 순서: + * 1. Oracle DB에서 구매그룹코드(EKGRP)로 사번(EMPLOYEE_NUMBER) 조회 + * 2. 사번으로 users 테이블의 employeeNumber와 매칭하여 사용자 정보 조회 + * 3. 폴백: users 테이블의 userCode = EKGRP로 직접 조회 */ export async function findUserInfoByEKGRP(EKGRP: string | null): Promise<{ userId: number; @@ -35,8 +40,63 @@ export async function findUserInfoByEKGRP(EKGRP: string | null): Promise<{ return null; } - // users 테이블에서 userCode로 직접 조회 - const userResult = await db + // 1. Oracle DB에서 구매그룹코드로 사번 조회 + try { + debugLog('Oracle DB에서 구매그룹코드로 사번 조회 시도', { EKGRP }); + + const oracleResult = await oracleKnex.raw(` + SELECT + CD.USR_DF_CHAR_9 AS EMPLOYEE_NUMBER + FROM CMCTB_CDNM NM + JOIN CMCTB_CD CD + ON NM.CD_CLF = CD.CD_CLF + AND NM.CD = CD.CD + AND NM.CD2 = CD.CD3 + WHERE NM.CD_CLF = 'MMA070' + AND CD.CD = :ekgrp + AND CD.DEL_YN != 'Y' + `, { ekgrp: EKGRP }); + + const rows = (oracleResult.rows || oracleResult) as Array<Record<string, unknown>>; + + if (rows && rows.length > 0 && rows[0].EMPLOYEE_NUMBER) { + const employeeNumber = String(rows[0].EMPLOYEE_NUMBER); + debugLog('Oracle에서 사번 조회 성공', { EKGRP, employeeNumber }); + + // 2. 사번으로 users 테이블에서 사용자 조회 + const userByEmployeeNumber = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + phone: users.phone + }) + .from(users) + .where(eq(users.employeeNumber, employeeNumber)) + .limit(1); + + if (userByEmployeeNumber.length > 0) { + const userInfo = { + userId: userByEmployeeNumber[0].id, + userName: userByEmployeeNumber[0].name, + userEmail: userByEmployeeNumber[0].email, + userPhone: userByEmployeeNumber[0].phone + }; + debugSuccess('사번으로 담당자 찾음 (Oracle 경로)', { EKGRP, employeeNumber, userInfo }); + return userInfo; + } else { + debugLog('사번에 해당하는 사용자를 찾을 수 없음, 폴백 시도', { employeeNumber }); + } + } else { + debugLog('Oracle에서 구매그룹코드에 해당하는 사번을 찾을 수 없음, 폴백 시도', { EKGRP }); + } + } catch (oracleError) { + debugError('Oracle 조회 중 오류, 폴백 시도', { EKGRP, error: oracleError }); + } + + // 3. 폴백: users 테이블에서 userCode로 직접 조회 + debugLog('폴백: userCode로 직접 조회 시도', { EKGRP }); + const userByUserCode = await db .select({ id: users.id, name: users.name, @@ -47,18 +107,18 @@ export async function findUserInfoByEKGRP(EKGRP: string | null): Promise<{ .where(eq(users.userCode, EKGRP)) .limit(1); - if (userResult.length === 0) { - debugError('EKGRP에 해당하는 사용자를 찾을 수 없음', { EKGRP }); + if (userByUserCode.length === 0) { + debugError('EKGRP에 해당하는 사용자를 찾을 수 없음 (모든 경로 실패)', { EKGRP }); return null; } const userInfo = { - userId: userResult[0].id, - userName: userResult[0].name, - userEmail: userResult[0].email, - userPhone: userResult[0].phone + userId: userByUserCode[0].id, + userName: userByUserCode[0].name, + userEmail: userByUserCode[0].email, + userPhone: userByUserCode[0].phone }; - debugSuccess('담당자 찾음', { EKGRP, userInfo }); + debugSuccess('담당자 찾음 (폴백 경로)', { EKGRP, userInfo }); return userInfo; } catch (error) { debugError('담당자 찾기 중 오류 발생', { EKGRP, error }); @@ -211,3 +271,34 @@ export interface ProjectInfo { id: number; name: string; } + +/** + * 협력업체 코드(LIFNR)로 vendorId 찾기 + * LIFNR = 벤더코드 (ex. A0001234) + * vendors 테이블의 vendorCode 필드와 비교하여 vendorId를 찾음 + */ +export async function findVendorIdByLIFNR(lifnr: string | null | undefined): Promise<number | null> { + if (!lifnr || !lifnr.trim()) { + debugLog('LIFNR이 없음'); + return null; + } + + try { + // vendorCode 또는 vendorSapCode로 조회 + const vendor = await db + .select({ id: vendors.id }) + .from(vendors) + .where(eq(vendors.vendorCode, lifnr.trim())) + .limit(1); + + if (vendor.length > 0) { + return vendor[0].id; + } + + debugLog(`LIFNR ${lifnr}에 해당하는 vendor를 찾을 수 없음`); + return null; + } catch (error) { + debugError('Vendor 조회 중 오류 발생', { lifnr, error }); + return null; + } +}
\ No newline at end of file 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, + }; +} + diff --git a/lib/soap/utils.ts b/lib/soap/utils.ts index 6c09edbe..57e3b280 100644 --- a/lib/soap/utils.ts +++ b/lib/soap/utils.ts @@ -6,6 +6,7 @@ import { eq } from "drizzle-orm"; import db from "@/db/db"; import { soapLogs, type LogDirection, type SoapLogInsert } from "@/db/schema/SOAP/soap"; import { XMLBuilder } from 'fast-xml-parser'; // for object→XML 변환 +import { debugError } from "../debug-utils"; // XML 파싱용 타입 유틸리티: 스키마에서 XML 타입 생성 export type ToXMLFields<T> = { @@ -605,4 +606,50 @@ export function dateToSAPFormat(date: Date): { date: string; time: string } { date: formatDateForSAP(date.toISOString().slice(0, 10)), time: formatTimeForSAP(date.toTimeString().slice(0, 8)) }; -}
\ No newline at end of file +} + +/** + * SAP 날짜 문자열을 Date로 변환 (YYYYMMDD -> Date) + */ +export function parseSAPDate(dateStr: string | null | undefined): Date | null { + if (!dateStr || !dateStr.trim()) return null; + + const trimmed = dateStr.trim(); + if (trimmed.length !== 8) return null; + + try { + const year = parseInt(trimmed.substring(0, 4), 10); + const month = parseInt(trimmed.substring(4, 6), 10) - 1; // 월은 0부터 시작 + const day = parseInt(trimmed.substring(6, 8), 10); + + const date = new Date(year, month, day); + + // 유효한 날짜인지 확인 + if (isNaN(date.getTime())) return null; + + return date; + } catch (error) { + debugError('SAP 날짜 파싱 오류', { dateStr, error }); + return null; + } +} + + +/** + * 숫자 문자열을 string으로 변환 (numeric 타입용) + * drizzle-orm의 numeric 타입은 string으로 저장됨 + */ +export function parseNumericString(value: string | null | undefined): string | null { + if (!value || !value.trim()) return null; + + try { + const trimmed = value.trim(); + const parsed = parseFloat(trimmed); + // 유효한 숫자인지 확인 + if (isNaN(parsed)) return null; + // 원본 문자열 반환 (또는 정제된 숫자 문자열) + return trimmed; + } catch { + return null; + } +} |
