import { NextRequest } from "next/server"; import db from "@/db/db"; import { MODEL_MASTER_MATL, MODEL_MASTER_MATL_DESC, MODEL_MASTER_MATL_PLNT, MODEL_MASTER_MATL_UNIT, MODEL_MASTER_MATL_CLASSASGN, MODEL_MASTER_MATL_CHARASGN } from "@/db/schema/MDG/mdg"; import { ToXMLFields, serveWsdl, createXMLParser, extractRequestData, convertXMLToDBData, processNestedArray, createErrorResponse, createSuccessResponse, withSoapLogging } from "@/lib/soap/utils"; import { bulkUpsert, bulkReplaceSubTableData } from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 (Insert와 XML을 통합) type MatlData = typeof MODEL_MASTER_MATL.$inferInsert; type MatlDescData = typeof MODEL_MASTER_MATL_DESC.$inferInsert; type MatlPlntData = typeof MODEL_MASTER_MATL_PLNT.$inferInsert; type MatlUnitData = typeof MODEL_MASTER_MATL_UNIT.$inferInsert; type MatlClassAsgnData = typeof MODEL_MASTER_MATL_CLASSASGN.$inferInsert; type MatlCharAsgnData = typeof MODEL_MASTER_MATL_CHARASGN.$inferInsert; // XML에서 받는 데이터 구조 (스키마와 동일한 구조, string 타입) type MatlXML = ToXMLFields> & { DESC?: DescXML[]; PLNT?: PlntXML[]; UNIT?: UnitXML[]; CLASSASGN?: ClassAsgnXML[]; CHARASGN?: CharAsgnXML[]; }; type DescXML = ToXMLFields>; type PlntXML = ToXMLFields>; type UnitXML = ToXMLFields>; type ClassAsgnXML = ToXMLFields>; type CharAsgnXML = ToXMLFields>; // 처리된 데이터 구조 (Insert와 동일) interface ProcessedMaterialData { material: MatlData; descriptions: MatlDescData[]; plants: MatlPlntData[]; units: MatlUnitData[]; classAssignments: MatlClassAsgnData[]; characteristicAssignments: MatlCharAsgnData[]; } export async function GET(request: NextRequest) { const url = new URL(request.url); if (url.searchParams.has('wsdl')) { return serveWsdl('IF_MDZ_EVCP_MODEL_MASTER.wsdl'); } return new Response('Method Not Allowed', { status: 405 }); } export async function POST(request: NextRequest) { const url = new URL(request.url); if (url.searchParams.has('wsdl')) { return serveWsdl('IF_MDZ_EVCP_MODEL_MASTER.wsdl'); } const body = await request.text(); return withSoapLogging( 'INBOUND', 'MDG', 'IF_MDZ_EVCP_MODEL_MASTER', body, async () => { console.log('Request Body 일부:', body.substring(0, 200) + (body.length > 200 ? '...' : '')); const parser = createXMLParser(['MATL', 'DESC', 'PLNT', 'UNIT', 'CLASSASGN', 'CHARASGN']); const parsedData = parser.parse(body); console.log('XML root keys:', Object.keys(parsedData)); const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_MODEL_MASTERReq'); if (!requestData) { console.error('Could not find valid request data in the received payload'); console.error('Received XML structure:', JSON.stringify(parsedData, null, 2)); throw new Error('Missing request data - could not find IF_MDZ_EVCP_MODEL_MASTERReq or MATL data'); } console.log('Validating request data structure:', `MATL: ${requestData.MATL ? 'found' : 'not found'}` ); if (requestData.MATL && Array.isArray(requestData.MATL) && requestData.MATL.length > 0) { console.log('First MATL sample:', JSON.stringify(requestData.MATL[0], null, 2)); } // XML 데이터를 DB 삽입 가능한 형태로 변환 const processedMaterials = transformMatlData(requestData.MATL as MatlXML[] || []); // 필수 필드 검증 for (const materialData of processedMaterials) { if (!materialData.material.MATNR) { throw new Error('Missing required field: MATNR in material'); } } // 데이터베이스 저장 await saveToDatabase(processedMaterials); console.log(`Processed ${processedMaterials.length} materials`); return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_MODEL_MASTER/'); } ).catch(error => { return createErrorResponse(error); }); } // XML 데이터를 DB 삽입 가능한 형태로 변환 /** * MODEL 마스터 데이터 변환 함수 * * 데이터 처리 아키텍처: * 1. 최상위 테이블 (MODEL_MASTER_MATL) * - MATNR이 unique 필드로 충돌 시 upsert 처리 * * 2. 하위 테이블들 (DESC, PLNT, UNIT, CLASSASGN, CHARASGN) * - FK(MATNR)로 연결 * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) * - 전체 데이터셋 기반 삭제 후 재삽입 처리 * * XML 패턴: * - MODEL 인터페이스는 XML에 MATNR이 이미 포함된 패턴 * - 하위 테이블에도 MATNR 필드가 있어서 XML 값 우선 사용됨 * * @param matlData XML에서 파싱된 MODEL 데이터 * @returns 처리된 MODEL 데이터 구조 */ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { if (!matlData || !Array.isArray(matlData)) { return []; } return matlData.map(matl => { const matnrKey = matl.MATNR || ''; const fkData = { MATNR: matnrKey }; // 1단계: MATL (루트 - unique 필드: MATNR) const material = convertXMLToDBData( matl as Record, fkData ); // 2단계: 하위 테이블들 (FK: MATNR) const descriptions = processNestedArray( matl.DESC, (desc) => convertXMLToDBData(desc as Record, fkData), fkData ); const plants = processNestedArray( matl.PLNT, (plnt) => convertXMLToDBData(plnt as Record, fkData), fkData ); const units = processNestedArray( matl.UNIT, (unit) => convertXMLToDBData(unit as Record, fkData), fkData ); const classAssignments = processNestedArray( matl.CLASSASGN, (cls) => convertXMLToDBData(cls as Record, fkData), fkData ); const characteristicAssignments = processNestedArray( matl.CHARASGN, (char) => convertXMLToDBData(char as Record, fkData), fkData ); return { material, descriptions, plants, units, classAssignments, characteristicAssignments }; }); } // 데이터베이스 저장 함수 /** * 처리된 MODEL 데이터를 데이터베이스에 저장 * * 저장 전략: * 1. 최상위 테이블: MATNR 기준 upsert (충돌 시 업데이트) * 2. 하위 테이블들: FK(MATNR) 기준 전체 삭제 후 재삽입 * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 * - 데이터 일관성과 단순성 확보 * * @param processedMaterials 변환된 MODEL 데이터 배열 */ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { console.log(`데이터베이스(배치) 저장 시작: ${processedMaterials.length}개 모델 데이터`); try { await db.transaction(async (tx) => { // 1) 부모 테이블 데이터 준비 const materialRows = processedMaterials .map((m) => m.material) .filter((m): m is MatlData => !!m.MATNR); const matnrs = materialRows.map((m) => m.MATNR as string); // 2) 하위 테이블 데이터 평탄화 const descriptions = processedMaterials.flatMap((m) => m.descriptions); const plants = processedMaterials.flatMap((m) => m.plants); const units = processedMaterials.flatMap((m) => m.units); const classAssignments = processedMaterials.flatMap((m) => m.classAssignments); const characteristicAssignments = processedMaterials.flatMap((m) => m.characteristicAssignments); // 3) 부모 테이블 UPSERT (배치) await bulkUpsert(tx, MODEL_MASTER_MATL, materialRows, 'MATNR'); // 4) 하위 테이블 교체 (배치) await Promise.all([ bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_DESC, descriptions, MODEL_MASTER_MATL_DESC.MATNR, matnrs), bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_PLNT, plants, MODEL_MASTER_MATL_PLNT.MATNR, matnrs), bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_UNIT, units, MODEL_MASTER_MATL_UNIT.MATNR, matnrs), bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_CLASSASGN, classAssignments, MODEL_MASTER_MATL_CLASSASGN.MATNR, matnrs), bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_CHARASGN, characteristicAssignments, MODEL_MASTER_MATL_CHARASGN.MATNR, matnrs) ]); }); console.log(`데이터베이스 저장 완료: ${processedMaterials.length}개 모델`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); throw error; } }