import { NextRequest } from 'next/server'; import db from '@/db/db'; import { eq } from 'drizzle-orm'; import { PR_INFORMATION_T_BID_HEADER, PR_INFORMATION_T_BID_ITEM, } from '@/db/schema/ECC/ecc'; import { rfqsLast, rfqPrItems } from '@/db/schema/rfqLast'; import { biddings, prItemsForBidding, prDocuments } from '@/db/schema/bidding'; import { ToXMLFields, serveWsdl, createXMLParser, extractRequestData, convertXMLToDBData, processNestedArray, createSoapResponse, withSoapLogging, } from '@/lib/soap/utils'; import { bulkUpsert, bulkReplaceSubTableData } from "@/lib/soap/batch-utils"; import { mapAndSaveECCRfqData, deleteECCRfqData } from "@/lib/soap/ecc/mapper/rfq-and-pr-mapper"; import { mapAndSaveECCBiddingData, deleteECCBiddingData } from "@/lib/soap/ecc/mapper/bidding-and-pr-mapper"; // 스키마에서 타입 추론 type BidHeaderData = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert; type BidItemData = typeof PR_INFORMATION_T_BID_ITEM.$inferInsert; // XML 구조 타입 정의 type BidHeaderXML = ToXMLFields>; type BidItemXML = ToXMLFields>; // 처리된 데이터 구조 interface ProcessedPRData { bidHeader: BidHeaderData; bidItems: BidItemData[]; } // GET 요청 처리는 ?wsdl 달고 있으면 WSDL 서비스 제공 export async function GET(request: NextRequest) { const url = new URL(request.url); if (url.searchParams.has('wsdl')) { return serveWsdl('IF_ECC_EVCP_PR_INFORMATION.wsdl'); } return new Response('Method Not Allowed', { status: 405 }); } // POST 요청이 데이터 적재 요구 (SOAP) export async function POST(request: NextRequest) { const url = new URL(request.url); if (url.searchParams.has('wsdl')) { return serveWsdl('IF_ECC_EVCP_PR_INFORMATION.wsdl'); } const body = await request.text(); // SOAP 로깅 래퍼 함수 사용 return withSoapLogging( 'INBOUND', 'ECC', 'IF_ECC_EVCP_PR_INFORMATION', body, async () => { console.log('🚀 PR_INFORMATION 수신 시작, 데이터 길이:', body.length); // 1) XML 파싱 const parser = createXMLParser(['T_BID_HEADER', 'T_BID_ITEM']); const parsedData = parser.parse(body); // 2) SOAP Body 또는 루트에서 요청 데이터 추출 const requestData = extractRequestData(parsedData, 'IF_ECC_EVCP_PR_INFORMATIONReq'); if (!requestData) { console.error('유효한 요청 데이터를 찾을 수 없습니다'); throw new Error('Missing request data - IF_ECC_EVCP_PR_INFORMATIONReq not found'); } // 3) XML 데이터를 DB 삽입 가능한 형태로 변환 const processedData = transformPRData(requestData as PRRequestXML); // 4) 필수 필드 검증 for (const prData of processedData) { if (!prData.bidHeader.ANFNR) { throw new Error('Missing required field: ANFNR in Bid Header'); } for (const item of prData.bidItems) { if (!item.ANFNR || !item.ANFPS) { throw new Error('Missing required fields in Bid Item: ANFNR, ANFPS'); } } } // 5) CHG_GB에 따라 Create/Delete 분기 처리 // 전체 처리를 하나의 트랜잭션으로 묶어서 하나라도 실패하면 전부 롤백 const chgGb = requestData.CHG_GB || 'C'; // 기본값은 Create await db.transaction(async (tx) => { if (chgGb === 'D') { // Delete 처리 console.log('삭제 모드로 데이터 처리 시작 (트랜잭션)'); // ZBSART에 따라 삭제할 데이터 분류 const anHeaders: BidHeaderData[] = []; const abHeaders: BidHeaderData[] = []; for (const prData of processedData) { if (prData.bidHeader.ZBSART === 'AN') { anHeaders.push(prData.bidHeader); } else if (prData.bidHeader.ZBSART === 'AB') { abHeaders.push(prData.bidHeader); } } let deletedRfqCount = 0; let deletedBiddingCount = 0; // AN (RFQ) 데이터 삭제 - 트랜잭션 내에서 직접 처리 if (anHeaders.length > 0) { for (const eccHeader of anHeaders) { const anfnr = eccHeader.ANFNR; if (!anfnr) { console.error('삭제할 ANFNR이 없음', { eccHeader }); continue; } // 해당 ANFNR의 RFQ 찾기 const existingRfq = await tx .select({ id: rfqsLast.id, rfqCode: rfqsLast.rfqCode }) .from(rfqsLast) .where(eq(rfqsLast.ANFNR, anfnr)) .limit(1); if (existingRfq.length === 0) { console.log(`ANFNR ${anfnr}에 해당하는 RFQ가 존재하지 않음`); continue; } const rfqId = existingRfq[0].id; const rfqCode = existingRfq[0].rfqCode; if (!rfqCode) { console.error('RFQ 코드가 없는 데이터는 건너뜀', { rfqId }); continue; } // rfqPrItems 삭제 await tx .delete(rfqPrItems) .where(eq(rfqPrItems.rfqsLastId, rfqId)); // rfqsLast 삭제 await tx .delete(rfqsLast) .where(eq(rfqsLast.id, rfqId)); deletedRfqCount++; console.log(`RFQ ${rfqCode} 삭제 완료`); } } // AB (Bidding) 데이터 삭제 - 트랜잭션 내에서 직접 처리 if (abHeaders.length > 0) { for (const eccHeader of abHeaders) { const anfnr = eccHeader.ANFNR; if (!anfnr) { console.error('삭제할 ANFNR이 없음', { eccHeader }); continue; } // 해당 ANFNR의 Bidding 찾기 const existingBidding = await tx .select({ id: biddings.id, biddingNumber: biddings.biddingNumber }) .from(biddings) .where(eq(biddings.ANFNR, anfnr)) .limit(1); if (existingBidding.length === 0) { console.log(`ANFNR ${anfnr}에 해당하는 Bidding이 존재하지 않음`); continue; } const biddingId = existingBidding[0].id; const biddingNumber = existingBidding[0].biddingNumber; // prItemsForBidding 삭제 await tx .delete(prItemsForBidding) .where(eq(prItemsForBidding.biddingId, biddingId)); // prDocuments 삭제 await tx .delete(prDocuments) .where(eq(prDocuments.biddingId, biddingId)); // biddings 삭제 await tx .delete(biddings) .where(eq(biddings.id, biddingId)); deletedBiddingCount++; console.log(`Bidding ${biddingNumber} 삭제 완료`); } } console.log(`🗑️ 삭제 완료 (트랜잭션): ${processedData.length}개 PR 데이터, ${deletedRfqCount}개 RFQ 삭제, ${deletedBiddingCount}개 Bidding 삭제`); } else { // Create/Update 처리 (기존 로직) console.log('생성/업데이트 모드로 데이터 처리 시작 (트랜잭션)'); // 5) 원본 ECC 데이터 저장 (기존 로직 유지) await saveToDatabase(processedData); // 6) ZBSART에 따라 비즈니스 테이블 분기 처리 const anHeaders: BidHeaderData[] = []; const abHeaders: BidHeaderData[] = []; const anItems: BidItemData[] = []; const abItems: BidItemData[] = []; // ZBSART에 따라 데이터 분류 for (const prData of processedData) { if (prData.bidHeader.ZBSART === 'AN') { anHeaders.push(prData.bidHeader); anItems.push(...prData.bidItems); } else if (prData.bidHeader.ZBSART === 'AB') { abHeaders.push(prData.bidHeader); abItems.push(...prData.bidItems); } } // AN (RFQ) 데이터 처리 - rfqsLast 테이블 let rfqMappingResult: { success: boolean; message: string; processedCount: number } | null = null; if (anHeaders.length > 0) { rfqMappingResult = await mapAndSaveECCRfqData(anHeaders, anItems); if (!rfqMappingResult.success) { throw new Error(`RFQ 비즈니스 테이블 매핑 실패: ${rfqMappingResult.message}`); } } // AB (Bidding) 데이터 처리 let biddingMappingResult: { success: boolean; message: string; processedCount: number } | null = null; if (abHeaders.length > 0) { biddingMappingResult = await mapAndSaveECCBiddingData(abHeaders, abItems); if (!biddingMappingResult.success) { throw new Error(`Bidding 비즈니스 테이블 매핑 실패: ${biddingMappingResult.message}`); } } console.log(`🎉 처리 완료 (트랜잭션): ${processedData.length}개 PR 데이터, ${rfqMappingResult?.processedCount || 0}개 RFQ 매핑, ${biddingMappingResult?.processedCount || 0}개 Bidding 매핑`); } }); // 6) 성공 응답 반환 return createSoapResponse('http://60.101.108.100/', { 'tns:IF_ECC_EVCP_PR_INFORMATIONRes': { EV_TYPE: 'S', EV_MESSAGE: 'Success', }, }); } ).catch((error) => { // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성 return createSoapResponse('http://60.101.108.100/', { 'tns:IF_ECC_EVCP_PR_INFORMATIONRes': { EV_TYPE: 'E', EV_MESSAGE: error instanceof Error ? error.message.slice(0, 100) : 'Unknown error', }, }); }); } // ----------------------------------------------------------------------------- // 데이터 변환 및 저장 관련 유틸리티 // ----------------------------------------------------------------------------- // Root XML Request 타입 type PRRequestXML = { CHG_GB?: string; T_BID_HEADER?: BidHeaderXML[]; T_BID_ITEM?: BidItemXML[]; }; // XML -> DB 데이터 변환 함수 function transformPRData(requestData: PRRequestXML): ProcessedPRData[] { const headers = requestData.T_BID_HEADER || []; const items = requestData.T_BID_ITEM || []; return headers.map((header) => { const headerKey = header.ANFNR || ''; const fkData = { ANFNR: headerKey }; // Header 변환 const bidHeaderConverted = convertXMLToDBData( header as Record, undefined // Header는 자체 필드만 사용 ); // 해당 Header의 Item들 필터 후 변환 const relatedItems = items.filter((item) => item.ANFNR === headerKey); const bidItemsConverted = processNestedArray( relatedItems, (item) => convertXMLToDBData(item as Record, fkData), fkData ); return { bidHeader: bidHeaderConverted, bidItems: bidItemsConverted, }; }); } // 데이터베이스 저장 함수 async function saveToDatabase(processedPRs: ProcessedPRData[]) { console.log(`데이터베이스(배치) 저장 시작: ${processedPRs.length}개 PR 데이터`); try { await db.transaction(async (tx) => { // 1) 부모 테이블 데이터 준비 (키 없는 이상데이터 제거) const bidHeaderRows = processedPRs .map((c) => c.bidHeader) .filter((h): h is BidHeaderData => !!h.ANFNR); const bidHeaderKeys = bidHeaderRows.map((h) => h.ANFNR as string); // 2) 하위 테이블 데이터 평탄화 const bidItems = processedPRs.flatMap((c) => c.bidItems); // 3) 부모 테이블 UPSERT (배치) await bulkUpsert(tx, PR_INFORMATION_T_BID_HEADER, bidHeaderRows, 'ANFNR'); // 4) 하위 테이블 교체 (배치) await Promise.all([ bulkReplaceSubTableData(tx, PR_INFORMATION_T_BID_ITEM, bidItems, PR_INFORMATION_T_BID_ITEM.ANFNR, bidHeaderKeys), ]); }); console.log(`데이터베이스(배치) 저장 완료: ${processedPRs.length}개 PR`); return true; } catch (error) { console.error('데이터베이스(배치) 저장 중 오류 발생:', error); throw error; } }