From f440aee7ff899a6aa93b8e1b5d98ea78df210872 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 22 Sep 2025 19:48:40 +0900 Subject: (김준회) 수정: POS 문서는 ECC에서 들어올 때 PR 아이템 기준으로 가져오도록 변경 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/soap/ecc/mapper/bidding-and-pr-mapper.ts | 178 ++++++++++++++++++++++++++- lib/soap/ecc/mapper/rfq-and-pr-mapper.ts | 35 +++++- 2 files changed, 210 insertions(+), 3 deletions(-) (limited to 'lib/soap/ecc') diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts index 4db8d451..99373555 100644 --- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts @@ -9,6 +9,7 @@ import db from '@/db/db'; import { biddings, prItemsForBidding, + prDocuments, } from '@/db/schema/bidding'; import { PR_INFORMATION_T_BID_HEADER, @@ -22,6 +23,11 @@ import { parseSAPDateTime, parseSAPDateToString, } from './common-mapper-utils'; +import { + getDcmtmIdByMaterialCode, + getEncryptDocumentumFile, + downloadPosFile +} from '@/lib/pos'; // ECC 데이터 타입 정의 export type ECCBidHeader = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert; @@ -31,6 +37,117 @@ export type ECCBidItem = typeof PR_INFORMATION_T_BID_ITEM.$inferInsert; export type BiddingData = typeof biddings.$inferInsert; export type PrItemForBiddingData = typeof prItemsForBidding.$inferInsert; +/** + * Bidding용 POS 파일 동기화 함수 + * 자재코드 기준으로 POS 파일을 찾아서 prDocuments 테이블에 저장 + */ +async function syncBiddingPosFiles( + biddingId: number, + materialCodes: string[], + userId: string = '1' +): Promise<{ + success: boolean; + successCount: number; + failedCount: number; + errors: string[]; +}> { + debugLog('Bidding POS 파일 동기화 시작', { biddingId, materialCodes }); + + let successCount = 0; + let failedCount = 0; + const errors: string[] = []; + + // 중복 제거된 자재코드로 처리 + const uniqueMaterialCodes = [...new Set(materialCodes.filter(code => code && code.trim() !== ''))]; + + for (const materialCode of uniqueMaterialCodes) { + try { + debugLog(`자재코드 ${materialCode} POS 파일 조회 시작`); + + // 1. 자재코드로 DCMTM_ID 조회 + const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode }); + + if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) { + debugLog(`자재코드 ${materialCode}: POS 파일 없음`); + continue; // 에러로 카운트하지 않고 스킵 + } + + // 첫 번째 파일만 처리 + const posFile = dcmtmResult.files[0]; + + // 2. POS API로 파일 경로 가져오기 + const posResult = await getEncryptDocumentumFile({ + objectID: posFile.dcmtmId + }); + + if (!posResult.success || !posResult.result) { + errors.push(`${materialCode}: POS 파일 경로 조회 실패`); + failedCount++; + continue; + } + + // 3. 파일 다운로드 + const downloadResult = await downloadPosFile({ + relativePath: posResult.result + }); + + if (!downloadResult.success || !downloadResult.fileBuffer) { + errors.push(`${materialCode}: 파일 다운로드 실패`); + failedCount++; + continue; + } + + // 4. 서버에 파일 저장 (uploads/bidding-pos 디렉토리) + const path = await import('path'); + const fs = await import('fs/promises'); + + const uploadDir = path.join(process.cwd(), 'uploads', 'bidding-pos'); + try { + await fs.access(uploadDir); + } catch { + await fs.mkdir(uploadDir, { recursive: true }); + } + + const timestamp = Date.now(); + const sanitizedFileName = (downloadResult.fileName || `${materialCode}.pdf`).replace(/[^a-zA-Z0-9.-]/g, '_'); + const fileName = `${timestamp}_${sanitizedFileName}`; + const filePath = path.join(uploadDir, fileName); + + await fs.writeFile(filePath, downloadResult.fileBuffer); + + // 5. prDocuments 테이블에 저장 + await db.insert(prDocuments).values({ + biddingId, + documentName: `${materialCode} 설계문서`, + fileName, + originalFileName: posFile.fileName, + fileSize: downloadResult.fileBuffer.length, + mimeType: downloadResult.mimeType || 'application/pdf', + filePath: `uploads/bidding-pos/${fileName}`, + registeredBy: userId, + description: `POS 시스템에서 자동 동기화됨 (DCMTM_ID: ${posFile.dcmtmId}, 자재코드: ${materialCode})`, + version: 'Rev.0' + }); + + successCount++; + debugSuccess(`자재코드 ${materialCode} POS 파일 동기화 완료`); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류'; + errors.push(`${materialCode}: ${errorMessage}`); + failedCount++; + debugError(`자재코드 ${materialCode} POS 파일 동기화 실패`, error); + } + } + + return { + success: successCount > 0, + successCount, + failedCount, + errors + }; +} + /** * Bidding 코드 생성 함수 (배치 처리용) @@ -333,7 +450,12 @@ export async function mapAndSaveECCBiddingData( const inserted = await tx .insert(biddings) .values(biddingRecords) - .returning({ id: biddings.id, biddingNumber: biddings.biddingNumber }); + .returning({ + id: biddings.id, + biddingNumber: biddings.biddingNumber, + ANFNR: biddings.ANFNR, + createdBy: biddings.createdBy + }); const biddingNumberToId = new Map(); for (const row of inserted) { @@ -368,13 +490,65 @@ export async function mapAndSaveECCBiddingData( await tx.insert(prItemsForBidding).values(chunk); } - return { processedCount: biddingRecords.length }; + return { + processedCount: biddingRecords.length, + insertedBiddings: inserted as Array<{ id: number; biddingNumber: string; ANFNR: string | null; createdBy: string | null }>, + allEccItems: eccItems // POS 동기화를 위해 필요 + }; }); debugSuccess('ECC Bidding 데이터 일괄 처리 완료', { processedCount: result.processedCount, }); + // 7) 각 Bidding에 대해 POS 파일 자동 동기화 (비동기로 실행하여 메인 플로우 블록하지 않음) + debugLog('Bidding POS 파일 자동 동기화 시작', { biddingCount: result.insertedBiddings.length }); + + // 비동기로 각 Bidding의 POS 파일 동기화 실행 (결과를 기다리지 않음) + result.insertedBiddings.forEach(async (bidding) => { + try { + // 해당 Bidding과 관련된 모든 자재코드 추출 + const relatedMaterialCodes = result.allEccItems + .filter(item => item.ANFNR === bidding.ANFNR) + .map(item => item.MATNR) + .filter(Boolean) as string[]; + + if (relatedMaterialCodes.length === 0) { + debugLog(`Bidding ${bidding.biddingNumber}: 자재코드 없음`); + return; + } + + debugLog(`Bidding ${bidding.biddingNumber} POS 파일 동기화 시작`, { + biddingId: bidding.id, + materialCodes: relatedMaterialCodes + }); + + const syncResult = await syncBiddingPosFiles( + bidding.id, + relatedMaterialCodes, + bidding.createdBy || '1' + ); + + if (syncResult.success) { + debugSuccess(`Bidding ${bidding.biddingNumber} POS 파일 동기화 완료`, { + biddingId: bidding.id, + successCount: syncResult.successCount, + failedCount: syncResult.failedCount + }); + } else { + debugError(`Bidding ${bidding.biddingNumber} POS 파일 동기화 실패`, { + biddingId: bidding.id, + errors: syncResult.errors + }); + } + } catch (error) { + debugError(`Bidding ${bidding.biddingNumber} POS 파일 동기화 중 예외 발생`, { + biddingId: bidding.id, + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } + }); + return { success: true, message: `${result.processedCount}개의 Bidding 데이터가 성공적으로 처리되었습니다.`, diff --git a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts index 8748e244..a517d84c 100644 --- a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts @@ -21,6 +21,7 @@ import { findMaterialNameByMATNR, parseSAPDateTime, } from './common-mapper-utils'; +import { syncRfqPosFiles } from '@/lib/pos'; // ECC 데이터 타입 정의 export type ECCBidHeader = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert; @@ -346,13 +347,45 @@ export async function mapAndSaveECCRfqDataToRfqLast( await tx.insert(rfqPrItems).values(chunk); } - return { processedCount: rfqRecords.length }; + return { + processedCount: rfqRecords.length, + insertedRfqs: inserted // POS 동기화를 위해 inserted 데이터 반환 + }; }); debugSuccess('ECC 데이터 일괄 처리 완료 (rfqsLast)', { processedCount: result.processedCount, }); + // 6) 각 RFQ에 대해 POS 파일 자동 동기화 (비동기로 실행하여 메인 플로우 블록하지 않음) + debugLog('RFQ POS 파일 자동 동기화 시작', { rfqCount: result.insertedRfqs.length }); + + // 비동기로 각 RFQ의 POS 파일 동기화 실행 (결과를 기다리지 않음) + result.insertedRfqs.forEach(async (rfq) => { + try { + debugLog(`RFQ ${rfq.rfqCode} POS 파일 동기화 시작`, { rfqId: rfq.id }); + const syncResult = await syncRfqPosFiles(rfq.id, 1); // 시스템 사용자 ID = 1 + + if (syncResult.success) { + debugSuccess(`RFQ ${rfq.rfqCode} POS 파일 동기화 완료`, { + rfqId: rfq.id, + successCount: syncResult.successCount, + failedCount: syncResult.failedCount + }); + } else { + debugError(`RFQ ${rfq.rfqCode} POS 파일 동기화 실패`, { + rfqId: rfq.id, + errors: syncResult.errors + }); + } + } catch (error) { + debugError(`RFQ ${rfq.rfqCode} POS 파일 동기화 중 예외 발생`, { + rfqId: rfq.id, + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } + }); + return { success: true, message: `${result.processedCount}개의 RFQ 데이터가 성공적으로 처리되었습니다.`, -- cgit v1.2.3