import { NextRequest } from 'next/server'; import db from '@/db/db'; import { inArray } from 'drizzle-orm'; import { ZMM_HD, ZMM_DT, ZMM_PAY, ZMM_KN, ZMM_NOTE, ZMM_NOTE2, } from '@/db/schema/ECC/ecc'; import { ToXMLFields, serveWsdl, createXMLParser, extractRequestData, convertXMLToDBData, processNestedArray, createSoapResponse, withSoapLogging, } from '@/lib/soap/utils'; import { bulkUpsert, bulkReplaceSubTableData } from "@/lib/soap/batch-utils"; import { mapAndSaveECCPOData, ProcessedPOData as POMapperProcessedData } from "@/lib/soap/ecc/mapper/po-mapper"; // 스키마에서 타입 추론 type HeaderData = typeof ZMM_HD.$inferInsert; type DetailData = typeof ZMM_DT.$inferInsert; type PaymentData = typeof ZMM_PAY.$inferInsert; type AccountData = typeof ZMM_KN.$inferInsert; type NoteData = typeof ZMM_NOTE.$inferInsert; type Note2Data = typeof ZMM_NOTE2.$inferInsert; // XML 구조 타입 정의 type HeaderXML = ToXMLFields>; type DetailXML = ToXMLFields>; type PaymentXML = ToXMLFields>; type AccountXML = ToXMLFields>; type NoteXML = ToXMLFields>; type Note2XML = ToXMLFields>; // 처리된 데이터 구조 interface ProcessedPOData { header: HeaderData; details: DetailData[]; payments: PaymentData[]; accounts: AccountData[]; notes: NoteData[]; notes2: Note2Data[]; } // ZMM_DT와 연결된 데이터 구조 interface DetailWithAccounts { detail: DetailData; accounts: AccountData[]; } // 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_PO_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_PO_INFORMATION.wsdl'); } const body = await request.text(); // SOAP 로깅 래퍼 함수 사용 return withSoapLogging( 'INBOUND', 'ECC', 'IF_ECC_EVCP_PO_INFORMATION', body, async () => { console.log('🚀 PO_INFORMATION 수신 시작, 데이터 길이:', body.length); // 1) XML 파싱 - 실제 XML 구조에 맞게 수정 const parser = createXMLParser(['ZMM_HD', 'ZMM_DT', 'ZMM_PAY', 'ZMM_KN', 'ZMM_NOTE', 'ZMM_NOTE2']); const parsedData = parser.parse(body); console.log('🔍 파싱된 XML 데이터 구조:', JSON.stringify(parsedData, null, 2)); // 2) SOAP Body 또는 루트에서 요청 데이터 추출 const requestData = extractRequestData(parsedData, 'IF_ECC_EVCP_PO_INFORMATIONReq'); console.log('🔍 추출된 요청 데이터:', JSON.stringify(requestData, null, 2)); if (!requestData) { console.error('유효한 요청 데이터를 찾을 수 없습니다'); console.error('파싱된 데이터 키들:', Object.keys(parsedData)); throw new Error('Missing request data - IF_ECC_EVCP_PO_INFORMATIONReq not found'); } // 3) XML 데이터를 DB 삽입 가능한 형태로 변환 const processedData = transformPOData(requestData as PORequestXML); // 4) 필수 필드 검증 for (const poData of processedData) { if (!poData.header.EBELN) { throw new Error('Missing required field: EBELN in Header'); } } // 5) 원본 ECC 데이터 저장 (기존 로직 유지) await saveToDatabase(processedData); // 6) PO 데이터를 비즈니스 테이블(contracts, contract_items)로 매핑 // ProcessedPOData를 POMapperProcessedData 형식으로 변환 (notes 포함) const mappingData: POMapperProcessedData[] = processedData.map(poData => ({ header: { ...poData.header, notes: poData.notes.map(note => ({ ZNOTE_SER: note.ZNOTE_SER || '', ZNOTE_TXT: note.ZNOTE_TXT || '' })) }, details: poData.details })); const poMappingResult = await mapAndSaveECCPOData(mappingData); if (!poMappingResult.success) { throw new Error(`PO 비즈니스 테이블 매핑 실패: ${poMappingResult.message}`); } console.log(`🎉 처리 완료: ${processedData.length}개 PO 데이터, ${poMappingResult.processedCount}개 계약 매핑`); // 7) 성공 응답 반환 return createSoapResponse('http://60.101.108.100/', { 'tns:IF_ECC_EVCP_PO_INFORMATIONRes': { ZMM_RT: { EBELN: processedData[0]?.header.EBELN || '', RT_CODE: 'S', RT_TEXT: '', }, }, }); } ).catch((error) => { // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성 return createSoapResponse('http://60.101.108.100/', { 'tns:IF_ECC_EVCP_PO_INFORMATIONRes': { ZMM_RT: { EBELN: '', RT_CODE: 'E', RT_TEXT: error instanceof Error ? error.message.slice(0, 100) : 'Unknown error', }, }, }); }); } // ----------------------------------------------------------------------------- // 데이터 변환 및 저장 관련 유틸리티 // ----------------------------------------------------------------------------- // Root XML Request 타입 - 실제 XML 구조에 맞게 수정 type PORequestXML = { CHG_GB?: string; ZMM_HD?: HeaderXML[]; ZMM_DT?: DetailXML[]; ZMM_PAY?: PaymentXML[]; ZMM_KN?: AccountXML[]; ZMM_NOTE?: NoteXML[]; ZMM_NOTE2?: Note2XML[]; }; // XML -> DB 데이터 변환 함수 - 중첩 구조 처리 function transformPOData(requestData: PORequestXML): ProcessedPOData[] { console.log('🔍 변환할 요청 데이터:', JSON.stringify(requestData, null, 2)); const headers = requestData.ZMM_HD || []; console.log(`📊 처리할 Header 수: ${headers.length}`); return headers.map((header) => { const headerKey = header.EBELN || ''; const fkData = { EBELN: headerKey }; console.log(`📦 처리 중인 Header: ${headerKey}`); // Header 변환 (ZMM_DT, ZMM_NOTE, ZMM_NOTE2 필드 제외) const headerFields = { ...header }; delete (headerFields as any).ZMM_DT; delete (headerFields as any).ZMM_NOTE; delete (headerFields as any).ZMM_NOTE2; const headerConverted = convertXMLToDBData( headerFields as Record, undefined ); // Header 내부의 중첩된 ZMM_DT 처리 const detailsFromHeader = (header as any).ZMM_DT || []; console.log(`📋 ${headerKey}의 Detail 수: ${detailsFromHeader.length}`); const detailsWithAccounts: DetailWithAccounts[] = detailsFromHeader.map((detail: any) => { const detailKey = detail.EBELP || ''; const detailFkData = { EBELN: headerKey, EBELP: detailKey }; console.log(`📄 처리 중인 Detail: ${headerKey}-${detailKey}`); // Detail 변환 (ZMM_KN 필드 제외) const detailFields = { ...detail }; delete detailFields.ZMM_KN; const detailConverted = convertXMLToDBData( detailFields as Record, fkData ); // Detail 내부의 중첩된 ZMM_KN 처리 const accountsFromDetail = detail.ZMM_KN || []; console.log(`💳 ${headerKey}-${detailKey}의 Account 수: ${Array.isArray(accountsFromDetail) ? accountsFromDetail.length : (accountsFromDetail ? 1 : 0)}`); // ZMM_KN이 배열이 아닌 경우 배열로 변환 const accountsArray = Array.isArray(accountsFromDetail) ? accountsFromDetail : (accountsFromDetail ? [accountsFromDetail] : []); const accountsConverted = processNestedArray( accountsArray, (account: any) => convertXMLToDBData(account as Record, detailFkData), detailFkData ); return { detail: detailConverted, accounts: accountsConverted, }; }); // Detail들과 Account들을 분리 const detailsConverted = detailsWithAccounts.map(d => d.detail); const allAccountsConverted = detailsWithAccounts.flatMap(d => d.accounts); // Header 내부의 중첩된 ZMM_NOTE 처리 const notesFromHeader = (header as any).ZMM_NOTE || []; console.log(`📝 ${headerKey}의 Note 수: ${Array.isArray(notesFromHeader) ? notesFromHeader.length : (notesFromHeader ? 1 : 0)}`); const notesArray = Array.isArray(notesFromHeader) ? notesFromHeader : (notesFromHeader ? [notesFromHeader] : []); const notesConverted = processNestedArray( notesArray, (note: any) => convertXMLToDBData(note as Record, fkData), fkData ); // Header 내부의 중첩된 ZMM_NOTE2 처리 const notes2FromHeader = (header as any).ZMM_NOTE2 || []; console.log(`📝 ${headerKey}의 Note2 수: ${Array.isArray(notes2FromHeader) ? notes2FromHeader.length : (notes2FromHeader ? 1 : 0)}`); const notes2Array = Array.isArray(notes2FromHeader) ? notes2FromHeader : (notes2FromHeader ? [notes2FromHeader] : []); const notes2Converted = processNestedArray( notes2Array, (note2: any) => convertXMLToDBData(note2 as Record, fkData), fkData ); // ZMM_PAY는 별도 처리 (현재 XML에는 없지만 구조상 유지) const paymentsConverted: PaymentData[] = []; console.log(`✅ ${headerKey} 변환 완료 - Details: ${detailsConverted.length}, Accounts: ${allAccountsConverted.length}, Notes: ${notesConverted.length}, Notes2: ${notes2Converted.length}`); return { header: headerConverted, details: detailsConverted, payments: paymentsConverted, notes: notesConverted, notes2: notes2Converted, accounts: allAccountsConverted, }; }); } // 데이터베이스 저장 함수 async function saveToDatabase(processedPOs: ProcessedPOData[]) { console.log(`데이터베이스(배치) 저장 시작: ${processedPOs.length}개 PO 데이터`); try { await db.transaction(async (tx) => { // 1) 부모 테이블 데이터 준비 (키 없는 이상데이터 제거) const headerRows = processedPOs .map((po) => po.header) .filter((h): h is HeaderData => !!h.EBELN); const headerKeys = headerRows.map((h) => h.EBELN as string); // 2) 하위 테이블 데이터 평탄화 const detailRows = processedPOs.flatMap((po) => po.details); const paymentRows = processedPOs.flatMap((po) => po.payments); const accountRows = processedPOs.flatMap((po) => po.accounts); const noteRows = processedPOs.flatMap((po) => po.notes); const note2Rows = processedPOs.flatMap((po) => po.notes2); // 3) 부모 테이블 UPSERT (배치) await bulkUpsert(tx, ZMM_HD, headerRows, 'EBELN'); // 4) 하위 테이블 교체 (배치) await Promise.all([ bulkReplaceSubTableData(tx, ZMM_DT, detailRows, ZMM_DT.EBELN, headerKeys), bulkReplaceSubTableData(tx, ZMM_PAY, paymentRows, ZMM_PAY.EBELN, headerKeys), bulkReplaceSubTableData(tx, ZMM_NOTE, noteRows, ZMM_NOTE.EBELN, headerKeys), bulkReplaceSubTableData(tx, ZMM_NOTE2, note2Rows, ZMM_NOTE2.EBELN, headerKeys), ]); // 5) ZMM_KN은 ZMM_DT의 서브테이블이므로 별도 처리 (EBELN + EBELP 조합으로 관리) // 기존 ZMM_KN 데이터 삭제 (해당 EBELN의 모든 데이터) await tx.delete(ZMM_KN).where( inArray(ZMM_KN.EBELN, headerKeys) ); // 새로운 ZMM_KN 데이터 삽입 if (accountRows.length > 0) { await tx.insert(ZMM_KN).values(accountRows); } }); console.log(`데이터베이스(배치) 저장 완료: ${processedPOs.length}개 PO`); return true; } catch (error) { console.error('데이터베이스(배치) 저장 중 오류 발생:', error); throw error; } }