import { NextRequest } from "next/server"; import db from "@/db/db"; import { MATERIAL_MASTER_PART_MATL, MATERIAL_MASTER_PART_MATL_DESC, MATERIAL_MASTER_PART_MATL_PLNT, MATERIAL_MASTER_PART_MATL_UNIT, MATERIAL_MASTER_PART_MATL_CLASSASGN, MATERIAL_MASTER_PART_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 MATERIAL_MASTER_PART_MATL.$inferInsert; type MatlDescData = typeof MATERIAL_MASTER_PART_MATL_DESC.$inferInsert; type MatlPlntData = typeof MATERIAL_MASTER_PART_MATL_PLNT.$inferInsert; type MatlUnitData = typeof MATERIAL_MASTER_PART_MATL_UNIT.$inferInsert; type MatlClassAsgnData = typeof MATERIAL_MASTER_PART_MATL_CLASSASGN.$inferInsert; type MatlCharAsgnData = typeof MATERIAL_MASTER_PART_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_MATERIAL_MASTER_PART.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_MATERIAL_MASTER_PART.wsdl'); } const body = await request.text(); return withSoapLogging( 'INBOUND', 'MDG', 'IF_MDZ_EVCP_MATERIAL_MASTER_PART', 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_MATERIAL_MASTER_PARTReq'); 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_MATERIAL_MASTER_PARTReq 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_MATERIAL_MASTER_PART/'); } ).catch(error => { return createErrorResponse(error); }); } // XML 데이터를 DB 삽입 가능한 형태로 변환 /** * MATERIAL 마스터 데이터 변환 함수 * * 데이터 처리 아키텍처: * 1. 최상위 테이블 (MATERIAL_MASTER_PART_MATL) * - MATNR이 unique 필드로 충돌 시 upsert 처리 * * 2. 하위 테이블들 (DESC, PLNT, UNIT, CLASSASGN, CHARASGN) * - FK(MATNR)로 연결 * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) * - 전체 데이터셋 기반 삭제 후 재삽입 처리 * * XML 패턴: * - MATERIAL 인터페이스는 XML에 MATNR이 이미 포함된 패턴 * - 하위 테이블에도 MATNR 필드가 있어서 XML 값 우선 사용됨 * * @param matlData XML에서 파싱된 MATERIAL 데이터 * @returns 처리된 MATERIAL 데이터 구조 */ 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 }; }); } // 데이터베이스 저장 함수 /** * 처리된 MATERIAL 데이터를 데이터베이스에 저장 * * 저장 전략: * 1. 최상위 테이블: MATNR 기준 upsert (충돌 시 업데이트) * 2. 하위 테이블들: FK(MATNR) 기준 전체 삭제 후 재삽입 * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 * - 데이터 일관성과 단순성 확보 * * @param processedMaterials 변환된 MATERIAL 데이터 배열 */ 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, MATERIAL_MASTER_PART_MATL, materialRows, 'MATNR'); // 4) 하위 테이블 교체 (배치) await Promise.all([ bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_DESC, descriptions, MATERIAL_MASTER_PART_MATL_DESC.MATNR, matnrs), bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_PLNT, plants, MATERIAL_MASTER_PART_MATL_PLNT.MATNR, matnrs), bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_UNIT, units, MATERIAL_MASTER_PART_MATL_UNIT.MATNR, matnrs), bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_CLASSASGN, classAssignments, MATERIAL_MASTER_PART_MATL_CLASSASGN.MATNR, matnrs), bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_CHARASGN, characteristicAssignments, MATERIAL_MASTER_PART_MATL_CHARASGN.MATNR, matnrs), ]); }); console.log(`데이터베이스(배치) 저장 완료: ${processedMaterials.length}개 자재`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); throw error; } }