diff options
Diffstat (limited to 'app')
13 files changed, 3367 insertions, 900 deletions
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts new file mode 100644 index 00000000..2c9df21d --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts @@ -0,0 +1,344 @@ +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { + CUSTOMER_MASTER_BP_HEADER, + CUSTOMER_MASTER_BP_HEADER_ADDRESS, + CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, + CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_FAX, + CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, + CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_TEL, + CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_URL, + CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN, + CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZCOMPANY, + CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES, + CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES_ZCPFN, + CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZTAXIND, + CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZVATREG, + CUSTOMER_MASTER_BP_HEADER_BP_TAXNUM +} from "@/db/schema/MDG/mdg"; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createErrorResponse, + createSuccessResponse, + replaceSubTableData, + withSoapLogging +} from "../utils"; + +// 스키마에서 직접 타입 추론 +type BpHeaderData = typeof CUSTOMER_MASTER_BP_HEADER.$inferInsert; +type AddressData = typeof CUSTOMER_MASTER_BP_HEADER_ADDRESS.$inferInsert; +type AdEmailData = typeof CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_EMAIL.$inferInsert; +type AdFaxData = typeof CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_FAX.$inferInsert; +type AdPostalData = typeof CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_POSTAL.$inferInsert; +type AdTelData = typeof CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_TEL.$inferInsert; +type AdUrlData = typeof CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_URL.$inferInsert; +type BpCusgenData = typeof CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN.$inferInsert; +type ZcompanyData = typeof CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZCOMPANY.$inferInsert; +type ZsalesData = typeof CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES.$inferInsert; +type ZcpfnData = typeof CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES_ZCPFN.$inferInsert; +type ZtaxindData = typeof CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZTAXIND.$inferInsert; +type ZvatregData = typeof CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZVATREG.$inferInsert; +type BpTaxnumData = typeof CUSTOMER_MASTER_BP_HEADER_BP_TAXNUM.$inferInsert; + +// XML 구조 타입 +type BpHeaderXML = ToXMLFields<Omit<BpHeaderData, 'id' | 'createdAt' | 'updatedAt'>> & { + ADDRESS?: AddressXML[]; + BP_TAXNUM?: BpTaxnumXML[]; + BP_CUSGEN?: BpCusgenXML[]; +}; + +type AddressXML = ToXMLFields<Omit<AddressData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>> & { + AD_EMAIL?: AdEmailXML[]; + AD_FAX?: AdFaxXML[]; + AD_POSTAL?: AdPostalXML[]; + AD_TEL?: AdTelXML[]; + AD_URL?: AdUrlXML[]; +}; + +type AdEmailXML = ToXMLFields<Omit<AdEmailData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; +type AdFaxXML = ToXMLFields<Omit<AdFaxData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; +type AdPostalXML = ToXMLFields<Omit<AdPostalData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; +type AdTelXML = ToXMLFields<Omit<AdTelData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; +type AdUrlXML = ToXMLFields<Omit<AdUrlData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; + +type BpCusgenXML = ToXMLFields<Omit<BpCusgenData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>> & { + ZVATREG?: ZvatregXML[]; + ZTAXIND?: ZtaxindXML[]; + ZCOMPANY?: ZcompanyXML[]; + ZSALES?: ZsalesXML[]; +}; + +type ZvatregXML = ToXMLFields<Omit<ZvatregData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; +type ZtaxindXML = ToXMLFields<Omit<ZtaxindData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; +type ZcompanyXML = ToXMLFields<Omit<ZcompanyData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; + +type ZsalesXML = ToXMLFields<Omit<ZsalesData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>> & { + ZCPFN?: ZcpfnXML[]; +}; + +type ZcpfnXML = ToXMLFields<Omit<ZcpfnData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; +type BpTaxnumXML = ToXMLFields<Omit<BpTaxnumData, 'id' | 'createdAt' | 'updatedAt' | 'BP_HEADER'>>; + +// 처리된 데이터 구조 +interface ProcessedCustomerData { + bpHeader: BpHeaderData; + addresses: AddressData[]; + adEmails: AdEmailData[]; + adFaxes: AdFaxData[]; + adPostals: AdPostalData[]; + adTels: AdTelData[]; + adUrls: AdUrlData[]; + bpCusgens: BpCusgenData[]; + zvatregs: ZvatregData[]; + ztaxinds: ZtaxindData[]; + zcompanies: ZcompanyData[]; + zsales: ZsalesData[]; + zcpfns: ZcpfnData[]; + bpTaxnums: BpTaxnumData[]; +} + +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_MDZ_EVCP_CUSTOMER_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_CUSTOMER_MASTER.wsdl'); + } + + const body = await request.text(); + + // SOAP 로깅 래퍼 함수 사용 + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_CUSTOMER_MASTER', + body, + async () => { + console.log('🚀 CUSTOMER_MASTER 수신 시작, 데이터 길이:', body.length); + + const parser = createXMLParser([ + 'BP_HEADER', 'ADDRESS', 'AD_EMAIL', 'AD_FAX', 'AD_POSTAL', 'AD_TEL', 'AD_URL', + 'BP_CUSGEN', 'ZVATREG', 'ZTAXIND', 'ZCOMPANY', 'ZSALES', 'ZCPFN', 'BP_TAXNUM' + ]); + + const parsedData = parser.parse(body); + console.log('XML root keys:', Object.keys(parsedData)); + + const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_CUSTOMER_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_CUSTOMER_MASTERReq or BP_HEADER data'); + } + + console.log('Validating request data structure:', + `BP_HEADER: ${requestData.BP_HEADER ? 'found' : 'not found'}` + ); + + if (requestData.BP_HEADER && Array.isArray(requestData.BP_HEADER) && requestData.BP_HEADER.length > 0) { + console.log('First BP_HEADER sample:', JSON.stringify(requestData.BP_HEADER[0], null, 2)); + } + + // XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedCustomers = transformCustomerData(requestData.BP_HEADER as BpHeaderXML[] || []); + + // 필수 필드 검증 + for (const customerData of processedCustomers) { + if (!customerData.bpHeader.BP_HEADER) { + throw new Error('Missing required field: BP_HEADER in customer'); + } + } + + // 데이터베이스 저장 + await saveToDatabase(processedCustomers); + + console.log(`🎉 처리 완료: ${processedCustomers.length}개 고객 데이터`); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_CUSTOMER_MASTER/'); + } + ).catch(error => { + // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성 + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformCustomerData(bpHeaderData: BpHeaderXML[]): ProcessedCustomerData[] { + if (!bpHeaderData || !Array.isArray(bpHeaderData)) { + return []; + } + + return bpHeaderData.map(bpHeader => { + const bpHeaderKey = bpHeader.BP_HEADER || ''; + const fkData = { BP_HEADER: bpHeaderKey }; + + // 1단계: BP_HEADER (루트) + const bpHeaderConverted = convertXMLToDBData<BpHeaderData>( + bpHeader as Record<string, string | undefined>, + ['BP_HEADER'], + fkData + ); + + // 2단계: ADDRESS와 직속 하위들 + const addresses = processNestedArray( + bpHeader.ADDRESS, + (addr) => convertXMLToDBData<AddressData>(addr as Record<string, string | undefined>, ['ADDRNO'], fkData), + fkData + ); + + // ADDRESS의 하위 테이블들 (3단계) + const adEmails = bpHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_EMAIL, (item) => + convertXMLToDBData<AdEmailData>(item as Record<string, string | undefined>, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData) + ) || []; + + const adFaxes = bpHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_FAX, (item) => + convertXMLToDBData<AdFaxData>(item as Record<string, string | undefined>, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData) + ) || []; + + const adPostals = bpHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_POSTAL, (item) => + convertXMLToDBData<AdPostalData>(item as Record<string, string | undefined>, ['NATION'], fkData), fkData) + ) || []; + + const adTels = bpHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_TEL, (item) => + convertXMLToDBData<AdTelData>(item as Record<string, string | undefined>, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData) + ) || []; + + const adUrls = bpHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_URL, (item) => + convertXMLToDBData<AdUrlData>(item as Record<string, string | undefined>, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData) + ) || []; + + // 2단계: BP_TAXNUM + const bpTaxnums = processNestedArray( + bpHeader.BP_TAXNUM, + (item) => convertXMLToDBData<BpTaxnumData>(item as Record<string, string | undefined>, ['TAXTYPE'], fkData), + fkData + ); + + // 2단계: BP_CUSGEN과 하위들 + const bpCusgens = processNestedArray( + bpHeader.BP_CUSGEN, + (cusgen) => convertXMLToDBData<BpCusgenData>(cusgen as Record<string, string | undefined>, ['KUNNR'], fkData), + fkData + ); + + // BP_CUSGEN의 하위 테이블들 (3단계) + const zvatregs = bpHeader.BP_CUSGEN?.flatMap(cusgen => + processNestedArray(cusgen.ZVATREG, (item) => + convertXMLToDBData<ZvatregData>(item as Record<string, string | undefined>, ['LAND1'], fkData), fkData) + ) || []; + + const ztaxinds = bpHeader.BP_CUSGEN?.flatMap(cusgen => + processNestedArray(cusgen.ZTAXIND, (item) => + convertXMLToDBData<ZtaxindData>(item as Record<string, string | undefined>, ['ALAND', 'TATYP'], fkData), fkData) + ) || []; + + const zcompanies = bpHeader.BP_CUSGEN?.flatMap(cusgen => + processNestedArray(cusgen.ZCOMPANY, (item) => + convertXMLToDBData<ZcompanyData>(item as Record<string, string | undefined>, ['BUKRS'], fkData), fkData) + ) || []; + + const zsales = bpHeader.BP_CUSGEN?.flatMap(cusgen => + processNestedArray(cusgen.ZSALES, (item) => + convertXMLToDBData<ZsalesData>(item as Record<string, string | undefined>, ['VKORG', 'VTWEG', 'SPART'], fkData), fkData) + ) || []; + + // ZSALES의 하위 테이블 (4단계) + const zcpfns = bpHeader.BP_CUSGEN?.flatMap(cusgen => + cusgen.ZSALES?.flatMap(sales => + processNestedArray(sales.ZCPFN, (item) => + convertXMLToDBData<ZcpfnData>(item as Record<string, string | undefined>, ['PARVW', 'PARZA'], fkData), fkData) + ) || [] + ) || []; + + return { + bpHeader: bpHeaderConverted, + addresses, + adEmails, + adFaxes, + adPostals, + adTels, + adUrls, + bpCusgens, + zvatregs, + ztaxinds, + zcompanies, + zsales, + zcpfns, + bpTaxnums + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedCustomers: ProcessedCustomerData[]) { + console.log(`데이터베이스 저장 시작: ${processedCustomers.length}개 고객 데이터`); + + try { + await db.transaction(async (tx) => { + for (const customerData of processedCustomers) { + const { bpHeader, addresses, adEmails, adFaxes, adPostals, adTels, adUrls, + bpCusgens, zvatregs, ztaxinds, zcompanies, zsales, zcpfns, bpTaxnums } = customerData; + + if (!bpHeader.BP_HEADER) { + console.warn('BP_HEADER가 없는 항목 발견, 건너뜁니다.'); + continue; + } + + // 1. BP_HEADER 테이블 Upsert (최상위 테이블) + await tx.insert(CUSTOMER_MASTER_BP_HEADER) + .values(bpHeader) + .onConflictDoUpdate({ + target: CUSTOMER_MASTER_BP_HEADER.BP_HEADER, + set: { + ...bpHeader, + updatedAt: new Date(), + } + }); + + // 2. 하위 테이블들 처리 - FK 기준으로 전체 삭제 후 재삽입 + await Promise.all([ + // 2단계 테이블들 + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS, addresses, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN, bpCusgens, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_TAXNUM, bpTaxnums, 'BP_HEADER', bpHeader.BP_HEADER), + + // 3-4단계 테이블들 + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, adEmails, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_FAX, adFaxes, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, adPostals, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_TEL, adTels, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_URL, adUrls, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZVATREG, zvatregs, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZTAXIND, ztaxinds, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZCOMPANY, zcompanies, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES, zsales, 'BP_HEADER', bpHeader.BP_HEADER), + replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES_ZCPFN, zcpfns, 'BP_HEADER', bpHeader.BP_HEADER), + ]); + } + }); + + console.log(`✅ 데이터베이스 저장 완료: ${processedCustomers.length}개 고객`); + return true; + } catch (error) { + console.error('❌ 데이터베이스 저장 중 오류 발생:', error); + throw error; + } +} diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts new file mode 100644 index 00000000..5d407e1f --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts @@ -0,0 +1,235 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { + DEPARTMENT_CODE_CMCTB_DEPT_MDG, + DEPARTMENT_CODE_CMCTB_DEPT_MDG_COMPNM, + DEPARTMENT_CODE_CMCTB_DEPT_MDG_CORPNM, + DEPARTMENT_CODE_CMCTB_DEPT_MDG_DEPTNM +} from "@/db/schema/MDG/mdg"; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createErrorResponse, + createSuccessResponse, + replaceSubTableData, + withSoapLogging +} from "../utils"; + +// 스키마에서 직접 타입 추론 +type DeptData = typeof DEPARTMENT_CODE_CMCTB_DEPT_MDG.$inferInsert; +type CompnmData = typeof DEPARTMENT_CODE_CMCTB_DEPT_MDG_COMPNM.$inferInsert; +type CorpnmData = typeof DEPARTMENT_CODE_CMCTB_DEPT_MDG_CORPNM.$inferInsert; +type DeptnmData = typeof DEPARTMENT_CODE_CMCTB_DEPT_MDG_DEPTNM.$inferInsert; + +// XML에서 받는 데이터 구조 +type DeptXML = ToXMLFields<Omit<DeptData, 'id' | 'createdAt' | 'updatedAt'>> & { + DEPTNM?: DeptnmXML[]; + COMPNM?: CompnmXML[]; + CORPNM?: CorpnmXML[]; +}; + +type DeptnmXML = ToXMLFields<Omit<DeptnmData, 'id' | 'createdAt' | 'updatedAt'>>; +type CompnmXML = ToXMLFields<Omit<CompnmData, 'id' | 'createdAt' | 'updatedAt'>>; +type CorpnmXML = ToXMLFields<Omit<CorpnmData, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 +interface ProcessedDepartmentData { + dept: DeptData; + deptnms: DeptnmData[]; + compnms: CompnmData[]; + corpnms: CorpnmData[]; +} + +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_MDZ_EVCP_DEPARTMENT_CODE.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_DEPARTMENT_CODE.wsdl'); + } + + const body = await request.text(); + + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_DEPARTMENT_CODE', + body, + async () => { + console.log('Request Body 일부:', body.substring(0, 200) + (body.length > 200 ? '...' : '')); + + const parser = createXMLParser(['CMCTB_DEPT_MDG', 'DEPTNM', 'COMPNM', 'CORPNM']); + const parsedData = parser.parse(body); + console.log('XML root keys:', Object.keys(parsedData)); + + const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_DEPARTMENT_CODEReq'); + + 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_DEPARTMENT_CODEReq or CMCTB_DEPT_MDG data'); + } + + console.log('Validating request data structure:', + `CMCTB_DEPT_MDG: ${requestData.CMCTB_DEPT_MDG ? 'found' : 'not found'}` + ); + + if (requestData.CMCTB_DEPT_MDG && Array.isArray(requestData.CMCTB_DEPT_MDG) && requestData.CMCTB_DEPT_MDG.length > 0) { + console.log('First CMCTB_DEPT_MDG sample:', JSON.stringify(requestData.CMCTB_DEPT_MDG[0], null, 2)); + } + + // XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedDepts = transformDepartmentData(requestData.CMCTB_DEPT_MDG as DeptXML[] || []); + + // 필수 필드 검증 + for (const deptData of processedDepts) { + if (!deptData.dept.DEPTCD) { + throw new Error('Missing required field: DEPTCD in department'); + } + if (!deptData.dept.CORPCD) { + throw new Error('Missing required field: CORPCD in department'); + } + } + + // 데이터베이스 저장 + await saveToDatabase(processedDepts); + + console.log(`Processed ${processedDepts.length} departments`); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_DEPARTMENT_CODE/'); + } + ).catch(error => { + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformDepartmentData(deptData: DeptXML[]): ProcessedDepartmentData[] { + if (!deptData || !Array.isArray(deptData)) { + return []; + } + + return deptData.map(dept => { + // 메인 Department 데이터 변환 + const deptRecord = convertXMLToDBData<DeptData>( + dept as Record<string, string | undefined>, + ['DEPTCD', 'CORPCD'] + ); + + // 필수 필드 보정 + if (!deptRecord.DEPTCD) { + deptRecord.DEPTCD = ''; + } + if (!deptRecord.CORPCD) { + deptRecord.CORPCD = ''; + } + + // FK 데이터 준비 + const fkData = { DEPTCD: dept.DEPTCD || '' }; + + // DEPTNM 데이터 변환 + const deptnms = processNestedArray( + dept.DEPTNM, + (deptnm) => convertXMLToDBData<DeptnmData>(deptnm, ['SPRAS'], fkData), + fkData + ); + + // COMPNM 데이터 변환 + const compnms = processNestedArray( + dept.COMPNM, + (compnm) => convertXMLToDBData<CompnmData>(compnm, ['SPRAS'], fkData), + fkData + ); + + // CORPNM 데이터 변환 + const corpnms = processNestedArray( + dept.CORPNM, + (corpnm) => convertXMLToDBData<CorpnmData>(corpnm, ['SPRAS'], fkData), + fkData + ); + + return { + dept: deptRecord, + deptnms, + compnms, + corpnms + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedDepts: ProcessedDepartmentData[]) { + console.log(`데이터베이스 저장 함수가 호출됨. ${processedDepts.length}개의 부서 데이터 수신.`); + + try { + await db.transaction(async (tx) => { + for (const deptData of processedDepts) { + const { dept, deptnms, compnms, corpnms } = deptData; + + if (!dept.DEPTCD) { + console.warn('부서코드(DEPTCD)가 없는 항목 발견, 건너뜁니다.'); + continue; + } + + // 1. Department 테이블 Upsert (최상위 테이블) + await tx.insert(DEPARTMENT_CODE_CMCTB_DEPT_MDG) + .values(dept) + .onConflictDoUpdate({ + target: DEPARTMENT_CODE_CMCTB_DEPT_MDG.DEPTCD, + set: { + ...dept, + updatedAt: new Date(), + } + }); + + // 2. 하위 테이블 데이터 처리 - FK 기준으로 전체 삭제 후 재삽입 + await Promise.all([ + // DEPTNM 테이블 처리 + replaceSubTableData( + tx, + DEPARTMENT_CODE_CMCTB_DEPT_MDG_DEPTNM, + deptnms, + 'DEPTCD', + dept.DEPTCD + ), + + // COMPNM 테이블 처리 + replaceSubTableData( + tx, + DEPARTMENT_CODE_CMCTB_DEPT_MDG_COMPNM, + compnms, + 'DEPTCD', + dept.DEPTCD + ), + + // CORPNM 테이블 처리 + replaceSubTableData( + tx, + DEPARTMENT_CODE_CMCTB_DEPT_MDG_CORPNM, + corpnms, + 'DEPTCD', + dept.DEPTCD + ) + ]); + } + }); + + console.log(`${processedDepts.length}개의 부서 데이터 처리 완료.`); + return true; + } catch (error) { + console.error('데이터베이스 저장 중 오류 발생:', error); + throw error; + } +} diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts new file mode 100644 index 00000000..39e9aa2f --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts @@ -0,0 +1,346 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// EMPLOYEE_MASTER +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { + EMPLOYEE_MASTER_CMCTB_EMP_MDG, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_BANM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_BINM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_COMPNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_CORPNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_COUNTRYNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE_PCCDNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_DHJOBGDNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBDUTYNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDTYPE, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_GNNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBDUTYNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBGRDNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_KTLNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_OKTLNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGBICDNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCOMPNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCORPNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGDEPTNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGPDEPNM, + EMPLOYEE_MASTER_CMCTB_EMP_MDG_PDEPTNM +} from "@/db/schema/MDG/mdg"; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createErrorResponse, + createSuccessResponse, + replaceSubTableData, + withSoapLogging +} from "../utils"; + +// 스키마에서 직접 타입 추론 +type EmpMdgData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG.$inferInsert; +type EmpBanmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_BANM.$inferInsert; +type EmpBinmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_BINM.$inferInsert; +type EmpCompnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_COMPNM.$inferInsert; +type EmpCorpnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_CORPNM.$inferInsert; +type EmpCountrynmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_COUNTRYNM.$inferInsert; +type EmpDeptcodeData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE.$inferInsert; +type EmpDeptcodePccdnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE_PCCDNM.$inferInsert; +type EmpDeptnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTNM.$inferInsert; +type EmpDhjobgdnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_DHJOBGDNM.$inferInsert; +type EmpGjobdutynmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBDUTYNM.$inferInsert; +type EmpGjobgrdnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDNM.$inferInsert; +type EmpGjobgrdtypeData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDTYPE.$inferInsert; +type EmpGjobnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBNM.$inferInsert; +type EmpGnnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_GNNM.$inferInsert; +type EmpJobdutynmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBDUTYNM.$inferInsert; +type EmpJobgrdnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBGRDNM.$inferInsert; +type EmpJobnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBNM.$inferInsert; +type EmpKtlnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_KTLNM.$inferInsert; +type EmpOktlnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_OKTLNM.$inferInsert; +type EmpOrgbicdnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGBICDNM.$inferInsert; +type EmpOrgcompnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCOMPNM.$inferInsert; +type EmpOrgcorpnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCORPNM.$inferInsert; +type EmpOrgdeptnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGDEPTNM.$inferInsert; +type EmpOrgpdepnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGPDEPNM.$inferInsert; +type EmpPdeptnmData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG_PDEPTNM.$inferInsert; + +// XML 구조 타입 +type EmpMdgXML = ToXMLFields<Omit<EmpMdgData, 'id' | 'createdAt' | 'updatedAt'>> & { + BANM?: ToXMLFields<Omit<EmpBanmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + BINM?: ToXMLFields<Omit<EmpBinmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + COMPNM?: ToXMLFields<Omit<EmpCompnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + CORPNM?: ToXMLFields<Omit<EmpCorpnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + COUNTRYNM?: ToXMLFields<Omit<EmpCountrynmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + DEPTCODE?: ToXMLFields<Omit<EmpDeptcodeData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + DEPTCODE_PCCDNM?: ToXMLFields<Omit<EmpDeptcodePccdnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + DEPTNM?: ToXMLFields<Omit<EmpDeptnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + DHJOBGDNM?: ToXMLFields<Omit<EmpDhjobgdnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + GJOBDUTYNM?: ToXMLFields<Omit<EmpGjobdutynmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + GJOBGRDNM?: ToXMLFields<Omit<EmpGjobgrdnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + GJOBGRDTYPE?: ToXMLFields<Omit<EmpGjobgrdtypeData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + GJOBNM?: ToXMLFields<Omit<EmpGjobnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + GNNM?: ToXMLFields<Omit<EmpGnnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + JOBDUTYNM?: ToXMLFields<Omit<EmpJobdutynmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + JOBGRDNM?: ToXMLFields<Omit<EmpJobgrdnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + JOBNM?: ToXMLFields<Omit<EmpJobnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + KTLNM?: ToXMLFields<Omit<EmpKtlnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + OKTLNM?: ToXMLFields<Omit<EmpOktlnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + ORGBICDNM?: ToXMLFields<Omit<EmpOrgbicdnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + ORGCOMPNM?: ToXMLFields<Omit<EmpOrgcompnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + ORGCORPNM?: ToXMLFields<Omit<EmpOrgcorpnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + ORGDEPTNM?: ToXMLFields<Omit<EmpOrgdeptnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + ORGPDEPNM?: ToXMLFields<Omit<EmpOrgpdepnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; + PDEPTNM?: ToXMLFields<Omit<EmpPdeptnmData, 'id' | 'createdAt' | 'updatedAt' | 'EMPID'>>[]; +}; + +// 처리된 데이터 구조 +interface ProcessedEmployeeData { + employee: EmpMdgData; + banm: EmpBanmData[]; + binm: EmpBinmData[]; + compnm: EmpCompnmData[]; + corpnm: EmpCorpnmData[]; + countrynm: EmpCountrynmData[]; + deptcode: EmpDeptcodeData[]; + deptcodePccdnm: EmpDeptcodePccdnmData[]; + deptnm: EmpDeptnmData[]; + dhjobgdnm: EmpDhjobgdnmData[]; + gjobdutynm: EmpGjobdutynmData[]; + gjobgrdnm: EmpGjobgrdnmData[]; + gjobgrdtype: EmpGjobgrdtypeData[]; + gjobnm: EmpGjobnmData[]; + gnnm: EmpGnnmData[]; + jobdutynm: EmpJobdutynmData[]; + jobgrdnm: EmpJobgrdnmData[]; + jobnm: EmpJobnmData[]; + ktlnm: EmpKtlnmData[]; + oktlnm: EmpOktlnmData[]; + orgbicdnm: EmpOrgbicdnmData[]; + orgcompnm: EmpOrgcompnmData[]; + orgcorpnm: EmpOrgcorpnmData[]; + orgdeptnm: EmpOrgdeptnmData[]; + orgpdepnm: EmpOrgpdepnmData[]; + pdeptnm: EmpPdeptnmData[]; +} + +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_MDZ_EVCP_EMPLOYEE_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_EMPLOYEE_MASTER.wsdl'); + } + + const body = await request.text(); + + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_EMPLOYEE_MASTER', + body, + async () => { + console.log('🚀 EMPLOYEE_MASTER 수신 시작, 데이터 길이:', body.length); + + const parser = createXMLParser([ + 'CMCTB_EMP_MDG', 'BANM', 'BINM', 'COMPNM', 'CORPNM', 'COUNTRYNM', 'DEPTCODE', + 'DEPTCODE_PCCDNM', 'DEPTNM', 'DHJOBGDNM', 'GJOBDUTYNM', 'GJOBGRDNM', 'GJOBGRDTYPE', + 'GJOBNM', 'GNNM', 'JOBDUTYNM', 'JOBGRDNM', 'JOBNM', 'KTLNM', 'OKTLNM', 'ORGBICDNM', + 'ORGCOMPNM', 'ORGCORPNM', 'ORGDEPTNM', 'ORGPDEPNM', 'PDEPTNM' + ]); + + const parsedData = parser.parse(body); + console.log('XML root keys:', Object.keys(parsedData)); + + const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_EMPLOYEE_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_EMPLOYEE_MASTERReq or CMCTB_EMP_MDG data'); + } + + console.log('Validating request data structure:', + `CMCTB_EMP_MDG: ${requestData.CMCTB_EMP_MDG ? 'found' : 'not found'}` + ); + + if (requestData.CMCTB_EMP_MDG && Array.isArray(requestData.CMCTB_EMP_MDG) && requestData.CMCTB_EMP_MDG.length > 0) { + console.log('First CMCTB_EMP_MDG sample:', JSON.stringify(requestData.CMCTB_EMP_MDG[0], null, 2)); + } + + // XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedEmployees = transformEmployeeData(requestData.CMCTB_EMP_MDG as EmpMdgXML[] || []); + + // 필수 필드 검증 + for (const employeeData of processedEmployees) { + if (!employeeData.employee.EMPID) { + throw new Error('Missing required field: EMPID in employee'); + } + } + + // 데이터베이스 저장 + await saveToDatabase(processedEmployees); + + console.log(`🎉 처리 완료: ${processedEmployees.length}개 사원 데이터`); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_EMPLOYEE_MASTER/'); + } + ).catch(error => { + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformEmployeeData(empData: EmpMdgXML[]): ProcessedEmployeeData[] { + if (!empData || !Array.isArray(empData)) { + return []; + } + + return empData.map(emp => { + const empId = emp.EMPID || ''; + const fkData = { EMPID: empId }; + + // 메인 Employee 데이터 변환 + const employee = convertXMLToDBData<EmpMdgData>( + emp as Record<string, string | undefined>, + ['EMPID'], + fkData + ); + + // 하위 테이블 데이터 변환 + const banm = processNestedArray(emp.BANM, (item) => convertXMLToDBData<EmpBanmData>(item, ['SPRAS'], fkData), fkData); + const binm = processNestedArray(emp.BINM, (item) => convertXMLToDBData<EmpBinmData>(item, ['SPRAS'], fkData), fkData); + const compnm = processNestedArray(emp.COMPNM, (item) => convertXMLToDBData<EmpCompnmData>(item, ['SPRAS'], fkData), fkData); + const corpnm = processNestedArray(emp.CORPNM, (item) => convertXMLToDBData<EmpCorpnmData>(item, ['SPRAS'], fkData), fkData); + const countrynm = processNestedArray(emp.COUNTRYNM, (item) => convertXMLToDBData<EmpCountrynmData>(item, ['SPRAS'], fkData), fkData); + const deptcode = processNestedArray(emp.DEPTCODE, (item) => convertXMLToDBData<EmpDeptcodeData>(item, [], fkData), fkData); + const deptcodePccdnm = processNestedArray(emp.DEPTCODE_PCCDNM, (item) => convertXMLToDBData<EmpDeptcodePccdnmData>(item, [], fkData), fkData); + const deptnm = processNestedArray(emp.DEPTNM, (item) => convertXMLToDBData<EmpDeptnmData>(item, ['SPRAS'], fkData), fkData); + const dhjobgdnm = processNestedArray(emp.DHJOBGDNM, (item) => convertXMLToDBData<EmpDhjobgdnmData>(item, ['SPRAS'], fkData), fkData); + const gjobdutynm = processNestedArray(emp.GJOBDUTYNM, (item) => convertXMLToDBData<EmpGjobdutynmData>(item, ['SPRAS'], fkData), fkData); + const gjobgrdnm = processNestedArray(emp.GJOBGRDNM, (item) => convertXMLToDBData<EmpGjobgrdnmData>(item, ['SPRAS'], fkData), fkData); + const gjobgrdtype = processNestedArray(emp.GJOBGRDTYPE, (item) => convertXMLToDBData<EmpGjobgrdtypeData>(item, [], fkData), fkData); + const gjobnm = processNestedArray(emp.GJOBNM, (item) => convertXMLToDBData<EmpGjobnmData>(item, ['SPRAS'], fkData), fkData); + const gnnm = processNestedArray(emp.GNNM, (item) => convertXMLToDBData<EmpGnnmData>(item, ['SPRAS'], fkData), fkData); + const jobdutynm = processNestedArray(emp.JOBDUTYNM, (item) => convertXMLToDBData<EmpJobdutynmData>(item, ['SPRAS'], fkData), fkData); + const jobgrdnm = processNestedArray(emp.JOBGRDNM, (item) => convertXMLToDBData<EmpJobgrdnmData>(item, ['SPRAS'], fkData), fkData); + const jobnm = processNestedArray(emp.JOBNM, (item) => convertXMLToDBData<EmpJobnmData>(item, ['SPRAS'], fkData), fkData); + const ktlnm = processNestedArray(emp.KTLNM, (item) => convertXMLToDBData<EmpKtlnmData>(item, ['SPRAS'], fkData), fkData); + const oktlnm = processNestedArray(emp.OKTLNM, (item) => convertXMLToDBData<EmpOktlnmData>(item, ['SPRAS'], fkData), fkData); + const orgbicdnm = processNestedArray(emp.ORGBICDNM, (item) => convertXMLToDBData<EmpOrgbicdnmData>(item, ['SPRAS'], fkData), fkData); + const orgcompnm = processNestedArray(emp.ORGCOMPNM, (item) => convertXMLToDBData<EmpOrgcompnmData>(item, ['SPRAS'], fkData), fkData); + const orgcorpnm = processNestedArray(emp.ORGCORPNM, (item) => convertXMLToDBData<EmpOrgcorpnmData>(item, ['SPRAS'], fkData), fkData); + const orgdeptnm = processNestedArray(emp.ORGDEPTNM, (item) => convertXMLToDBData<EmpOrgdeptnmData>(item, ['SPRAS'], fkData), fkData); + const orgpdepnm = processNestedArray(emp.ORGPDEPNM, (item) => convertXMLToDBData<EmpOrgpdepnmData>(item, ['SPRAS'], fkData), fkData); + const pdeptnm = processNestedArray(emp.PDEPTNM, (item) => convertXMLToDBData<EmpPdeptnmData>(item, [], fkData), fkData); + + return { + employee, + banm, + binm, + compnm, + corpnm, + countrynm, + deptcode, + deptcodePccdnm, + deptnm, + dhjobgdnm, + gjobdutynm, + gjobgrdnm, + gjobgrdtype, + gjobnm, + gnnm, + jobdutynm, + jobgrdnm, + jobnm, + ktlnm, + oktlnm, + orgbicdnm, + orgcompnm, + orgcorpnm, + orgdeptnm, + orgpdepnm, + pdeptnm + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedEmployees: ProcessedEmployeeData[]) { + console.log(`데이터베이스 저장 시작: ${processedEmployees.length}개 사원 데이터`); + + try { + await db.transaction(async (tx) => { + for (const employeeData of processedEmployees) { + const { employee, banm, binm, compnm, corpnm, countrynm, deptcode, deptcodePccdnm, + deptnm, dhjobgdnm, gjobdutynm, gjobgrdnm, gjobgrdtype, gjobnm, gnnm, + jobdutynm, jobgrdnm, jobnm, ktlnm, oktlnm, orgbicdnm, orgcompnm, + orgcorpnm, orgdeptnm, orgpdepnm, pdeptnm } = employeeData; + + if (!employee.EMPID) { + console.warn('사원번호(EMPID)가 없는 항목 발견, 건너뜁니다.'); + continue; + } + + // 1. CMCTB_EMP_MDG 테이블 Upsert (최상위 테이블) + await tx.insert(EMPLOYEE_MASTER_CMCTB_EMP_MDG) + .values(employee) + .onConflictDoUpdate({ + target: EMPLOYEE_MASTER_CMCTB_EMP_MDG.EMPID, + set: { + ...employee, + updatedAt: new Date(), + } + }); + + // 2. 하위 테이블들 처리 - FK 기준으로 전체 삭제 후 재삽입 + await Promise.all([ + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_BANM, banm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_BINM, binm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_COMPNM, compnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_CORPNM, corpnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_COUNTRYNM, countrynm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE, deptcode, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE_PCCDNM, deptcodePccdnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTNM, deptnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DHJOBGDNM, dhjobgdnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBDUTYNM, gjobdutynm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDNM, gjobgrdnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDTYPE, gjobgrdtype, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBNM, gjobnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GNNM, gnnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBDUTYNM, jobdutynm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBGRDNM, jobgrdnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBNM, jobnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_KTLNM, ktlnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_OKTLNM, oktlnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGBICDNM, orgbicdnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCOMPNM, orgcompnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCORPNM, orgcorpnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGDEPTNM, orgdeptnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGPDEPNM, orgpdepnm, 'EMPID', employee.EMPID), + replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_PDEPTNM, pdeptnm, 'EMPID', employee.EMPID) + ]); + } + }); + + console.log(`✅ 데이터베이스 저장 완료: ${processedEmployees.length}개 사원`); + return true; + } catch (error) { + console.error('❌ 데이터베이스 저장 중 오류 발생:', error); + throw error; + } +}
\ No newline at end of file diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts new file mode 100644 index 00000000..a265fea2 --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts @@ -0,0 +1,182 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { + EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF, + EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF_NAME +} from "@/db/schema/MDG/mdg"; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createErrorResponse, + createSuccessResponse, + replaceSubTableData, + withSoapLogging +} from "../utils"; + +// 스키마에서 직접 타입 추론 +type EmpRefData = typeof EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF.$inferInsert; +type EmpRefNameData = typeof EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF_NAME.$inferInsert; + +// XML에서 받는 데이터 구조 +type EmpRefXML = ToXMLFields<Omit<EmpRefData, 'id' | 'createdAt' | 'updatedAt'>> & { + NAME?: NameXML[]; +}; + +type NameXML = ToXMLFields<Omit<EmpRefNameData, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 +interface ProcessedEmployeeReferenceData { + empRef: EmpRefData; + names: EmpRefNameData[]; +} + +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_MDZ_EVCP_EMPLOYEE_REFERENCE_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_EMPLOYEE_REFERENCE_MASTER.wsdl'); + } + + const body = await request.text(); + + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER', + body, + async () => { + console.log('Request Body 일부:', body.substring(0, 200) + (body.length > 200 ? '...' : '')); + + const parser = createXMLParser(['CMCTB_EMP_REF_MDG_IF', 'NAME']); + const parsedData = parser.parse(body); + console.log('XML root keys:', Object.keys(parsedData)); + + const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_EMPLOYEE_REFERENCE_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_EMPLOYEE_REFERENCE_MASTERReq or CMCTB_EMP_REF_MDG_IF data'); + } + + console.log('Validating request data structure:', + `CMCTB_EMP_REF_MDG_IF: ${requestData.CMCTB_EMP_REF_MDG_IF ? 'found' : 'not found'}` + ); + + if (requestData.CMCTB_EMP_REF_MDG_IF && Array.isArray(requestData.CMCTB_EMP_REF_MDG_IF) && requestData.CMCTB_EMP_REF_MDG_IF.length > 0) { + console.log('First CMCTB_EMP_REF_MDG_IF sample:', JSON.stringify(requestData.CMCTB_EMP_REF_MDG_IF[0], null, 2)); + } + + // XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedEmpRefs = transformEmpRefData(requestData.CMCTB_EMP_REF_MDG_IF as EmpRefXML[] || []); + + // 필수 필드 검증 + for (const empRefData of processedEmpRefs) { + if (!empRefData.empRef.GRPCD) { + throw new Error('Missing required field: GRPCD in employee reference'); + } + } + + // 데이터베이스 저장 + await saveToDatabase(processedEmpRefs); + + console.log(`Processed ${processedEmpRefs.length} employee references`); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/'); + } + ).catch(error => { + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformEmpRefData(empRefData: EmpRefXML[]): ProcessedEmployeeReferenceData[] { + if (!empRefData || !Array.isArray(empRefData)) { + return []; + } + + return empRefData.map(empRef => { + // 메인 Employee Reference 데이터 변환 + const empRefRecord = convertXMLToDBData<EmpRefData>( + empRef as Record<string, string | undefined>, + ['GRPCD', 'CORPCD', 'MAINCD'] + ); + + // 필수 필드 보정 + if (!empRefRecord.GRPCD) { + empRefRecord.GRPCD = ''; + } + + // FK 데이터 준비 + const fkData = { GRPCD: empRef.GRPCD || '' }; + + // Name 데이터 변환 + const names = processNestedArray( + empRef.NAME, + (name) => convertXMLToDBData<EmpRefNameData>(name, ['SPRAS'], fkData), + fkData + ); + + return { + empRef: empRefRecord, + names + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedEmpRefs: ProcessedEmployeeReferenceData[]) { + console.log(`데이터베이스 저장 함수가 호출됨. ${processedEmpRefs.length}개의 직원 참조 데이터 수신.`); + + try { + await db.transaction(async (tx) => { + for (const empRefData of processedEmpRefs) { + const { empRef, names } = empRefData; + + if (!empRef.GRPCD) { + console.warn('그룹코드(GRPCD)가 없는 항목 발견, 건너뜁니다.'); + continue; + } + + // 1. Employee Reference 테이블 Upsert (최상위 테이블) + await tx.insert(EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF) + .values(empRef) + .onConflictDoUpdate({ + target: EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF.GRPCD, + set: { + ...empRef, + updatedAt: new Date(), + } + }); + + // 2. NAME 테이블 처리 - FK 기준으로 전체 삭제 후 재삽입 + await replaceSubTableData( + tx, + EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF_NAME, + names, + 'GRPCD', + empRef.GRPCD + ); + } + }); + + console.log(`${processedEmpRefs.length}개의 직원 참조 데이터 처리 완료.`); + return true; + } catch (error) { + console.error('데이터베이스 저장 중 오류 발생:', error); + throw error; + } +}
\ No newline at end of file diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts new file mode 100644 index 00000000..358e9c62 --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts @@ -0,0 +1,271 @@ +//equp_master +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { + EQUP_MASTER_MATL, + EQUP_MASTER_MATL_DESC, + EQUP_MASTER_MATL_PLNT, + EQUP_MASTER_MATL_UNIT, + EQUP_MASTER_MATL_CLASSASGN, + EQUP_MASTER_MATL_CHARASGN +} from "@/db/schema/MDG/mdg"; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createErrorResponse, + createSuccessResponse, + replaceSubTableData, + withSoapLogging +} from "../utils"; + +// 스키마에서 직접 타입 추론 (Insert와 XML을 통합) +type MatlData = typeof EQUP_MASTER_MATL.$inferInsert; +type MatlDescData = typeof EQUP_MASTER_MATL_DESC.$inferInsert; +type MatlPlntData = typeof EQUP_MASTER_MATL_PLNT.$inferInsert; +type MatlUnitData = typeof EQUP_MASTER_MATL_UNIT.$inferInsert; +type MatlClassAsgnData = typeof EQUP_MASTER_MATL_CLASSASGN.$inferInsert; +type MatlCharAsgnData = typeof EQUP_MASTER_MATL_CHARASGN.$inferInsert; + +// XML에서 받는 데이터 구조 (스키마와 동일한 구조, string 타입) +type MatlXML = ToXMLFields<Omit<MatlData, 'id' | 'createdAt' | 'updatedAt'>> & { + DESC?: DescXML[]; + PLNT?: PlntXML[]; + UNIT?: UnitXML[]; + CLASSASGN?: ClassAsgnXML[]; + CHARASGN?: CharAsgnXML[]; +}; + +type DescXML = ToXMLFields<Omit<MatlDescData, 'id' | 'createdAt' | 'updatedAt'>>; +type PlntXML = ToXMLFields<Omit<MatlPlntData, 'id' | 'createdAt' | 'updatedAt'>>; +type UnitXML = ToXMLFields<Omit<MatlUnitData, 'id' | 'createdAt' | 'updatedAt'>>; +type ClassAsgnXML = ToXMLFields<Omit<MatlClassAsgnData, 'id' | 'createdAt' | 'updatedAt'>>; +type CharAsgnXML = ToXMLFields<Omit<MatlCharAsgnData, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 (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_EQUP_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_EQUP_MASTER.wsdl'); + } + + const body = await request.text(); + + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_EQUP_MASTER', + body, + async () => { + console.log('🚀 EQUP_MASTER 수신 시작, 데이터 길이:', body.length); + + 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_EQUP_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_EQUP_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(`🎉 처리 완료: ${processedMaterials.length}개 장비 자재 데이터`); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_EQUP_MASTER/'); + } + ).catch(error => { + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { + if (!matlData || !Array.isArray(matlData)) { + return []; + } + + return matlData.map(matl => { + // 메인 Material 데이터 변환 (자동) + const material = convertXMLToDBData<MatlData>( + matl as Record<string, string | undefined>, + ['MATNR'] + ); + + // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨) + if (!material.MATNR) { + material.MATNR = ''; + } + + // FK 데이터 준비 + const fkData = { MATNR: matl.MATNR || '' }; + + // Description 데이터 변환 (자동) + const descriptions = processNestedArray( + matl.DESC, + (desc) => convertXMLToDBData<MatlDescData>(desc, ['MATNR'], fkData), + fkData + ); + + // Plant 데이터 변환 (자동) + const plants = processNestedArray( + matl.PLNT, + (plnt) => convertXMLToDBData<MatlPlntData>(plnt, ['MATNR'], fkData), + fkData + ); + + // Unit 데이터 변환 (자동) + const units = processNestedArray( + matl.UNIT, + (unit) => convertXMLToDBData<MatlUnitData>(unit, ['MATNR'], fkData), + fkData + ); + + // Class Assignment 데이터 변환 (자동) + const classAssignments = processNestedArray( + matl.CLASSASGN, + (cls) => convertXMLToDBData<MatlClassAsgnData>(cls, ['MATNR'], fkData), + fkData + ); + + // Characteristic Assignment 데이터 변환 (자동) + const characteristicAssignments = processNestedArray( + matl.CHARASGN, + (char) => convertXMLToDBData<MatlCharAsgnData>(char, ['MATNR'], fkData), + fkData + ); + + return { + material, + descriptions, + plants, + units, + classAssignments, + characteristicAssignments + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { + console.log(`Starting database save for ${processedMaterials.length} equipment materials`); + + await db.transaction(async (tx) => { + for (const materialData of processedMaterials) { + const { material, descriptions, plants, units, classAssignments, characteristicAssignments } = materialData; + + if (!material.MATNR) { + console.warn('Skipping material without MATNR'); + continue; + } + + console.log(`Processing MATNR: ${material.MATNR}`); + + // 1. MATL 테이블 Upsert (최상위 테이블) + await tx.insert(EQUP_MASTER_MATL) + .values(material) + .onConflictDoUpdate({ + target: EQUP_MASTER_MATL.MATNR, + set: { + ...material, + updatedAt: new Date(), + } + }); + + // 2. 하위 테이블 데이터 처리 - FK 기준으로 전체 삭제 후 재삽입 (병렬 처리) + await Promise.all([ + // DESC 테이블 처리 + replaceSubTableData( + tx, + EQUP_MASTER_MATL_DESC, + descriptions, + 'MATNR', + material.MATNR + ), + + // PLNT 테이블 처리 + replaceSubTableData( + tx, + EQUP_MASTER_MATL_PLNT, + plants, + 'MATNR', + material.MATNR + ), + + // UNIT 테이블 처리 + replaceSubTableData( + tx, + EQUP_MASTER_MATL_UNIT, + units, + 'MATNR', + material.MATNR + ), + + // CLASSASGN 테이블 처리 + replaceSubTableData( + tx, + EQUP_MASTER_MATL_CLASSASGN, + classAssignments, + 'MATNR', + material.MATNR + ), + + // CHARASGN 테이블 처리 + replaceSubTableData( + tx, + EQUP_MASTER_MATL_CHARASGN, + characteristicAssignments, + 'MATNR', + material.MATNR + ) + ]); + + console.log(`Successfully processed MATNR: ${material.MATNR}`); + } + }); + + console.log(`Database save completed for ${processedMaterials.length} equipment materials`); +}
\ No newline at end of file diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts new file mode 100644 index 00000000..3992d788 --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts @@ -0,0 +1,274 @@ +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, + SoapBodyData, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createErrorResponse, + createSuccessResponse, + replaceSubTableData, + withSoapLogging +} from "../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<Omit<MatlData, 'id' | 'createdAt' | 'updatedAt'>> & { + DESC?: DescXML[]; + PLNT?: PlntXML[]; + UNIT?: UnitXML[]; + CLASSASGN?: ClassAsgnXML[]; + CHARASGN?: CharAsgnXML[]; +}; + +type DescXML = ToXMLFields<Omit<MatlDescData, 'id' | 'createdAt' | 'updatedAt'>>; +type PlntXML = ToXMLFields<Omit<MatlPlntData, 'id' | 'createdAt' | 'updatedAt'>>; +type UnitXML = ToXMLFields<Omit<MatlUnitData, 'id' | 'createdAt' | 'updatedAt'>>; +type ClassAsgnXML = ToXMLFields<Omit<MatlClassAsgnData, 'id' | 'createdAt' | 'updatedAt'>>; +type CharAsgnXML = ToXMLFields<Omit<MatlCharAsgnData, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 (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', + 'S-ERP', + '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 삽입 가능한 형태로 변환 +function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { + if (!matlData || !Array.isArray(matlData)) { + return []; + } + + return matlData.map(matl => { + // 메인 Material 데이터 변환 (자동) + const material = convertXMLToDBData<MatlData>( + matl as Record<string, string | undefined>, + ['MATNR'] + ); + + // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨) + if (!material.MATNR) { + material.MATNR = ''; + } + + // FK 데이터 준비 + const fkData = { MATNR: matl.MATNR || '' }; + + // Description 데이터 변환 (자동) + const descriptions = processNestedArray( + matl.DESC, + (desc) => convertXMLToDBData<MatlDescData>(desc, ['MATNR'], fkData), + fkData + ); + + // Plant 데이터 변환 (자동) + const plants = processNestedArray( + matl.PLNT, + (plnt) => convertXMLToDBData<MatlPlntData>(plnt, ['MATNR'], fkData), + fkData + ); + + // Unit 데이터 변환 (자동) + const units = processNestedArray( + matl.UNIT, + (unit) => convertXMLToDBData<MatlUnitData>(unit, ['MATNR'], fkData), + fkData + ); + + // Class Assignment 데이터 변환 (자동) + const classAssignments = processNestedArray( + matl.CLASSASGN, + (cls) => convertXMLToDBData<MatlClassAsgnData>(cls, ['MATNR'], fkData), + fkData + ); + + // Characteristic Assignment 데이터 변환 (자동) + const characteristicAssignments = processNestedArray( + matl.CHARASGN, + (char) => convertXMLToDBData<MatlCharAsgnData>(char, ['MATNR'], fkData), + fkData + ); + + return { + material, + descriptions, + plants, + units, + classAssignments, + characteristicAssignments + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { + console.log(`데이터베이스 저장 함수가 호출됨. ${processedMaterials.length}개의 자재 데이터 수신.`); + + try { + await db.transaction(async (tx) => { + for (const materialData of processedMaterials) { + const { material, descriptions, plants, units, classAssignments, characteristicAssignments } = materialData; + + if (!material.MATNR) { + console.warn('자재번호(MATNR)가 없는 항목 발견, 건너뜁니다.'); + continue; + } + + // 1. MATL 테이블 Upsert (최상위 테이블) + await tx.insert(MATERIAL_MASTER_PART_MATL) + .values(material) + .onConflictDoUpdate({ + target: MATERIAL_MASTER_PART_MATL.MATNR, + set: { + ...material, + updatedAt: new Date(), + } + }); + + // 2. 하위 테이블 데이터 처리 - FK 기준으로 전체 삭제 후 재삽입 + await Promise.all([ + // DESC 테이블 처리 + replaceSubTableData( + tx, + MATERIAL_MASTER_PART_MATL_DESC, + descriptions, + 'MATNR', + material.MATNR + ), + + // PLNT 테이블 처리 + replaceSubTableData( + tx, + MATERIAL_MASTER_PART_MATL_PLNT, + plants, + 'MATNR', + material.MATNR + ), + + // UNIT 테이블 처리 + replaceSubTableData( + tx, + MATERIAL_MASTER_PART_MATL_UNIT, + units, + 'MATNR', + material.MATNR + ), + + // CLASSASGN 테이블 처리 + replaceSubTableData( + tx, + MATERIAL_MASTER_PART_MATL_CLASSASGN, + classAssignments, + 'MATNR', + material.MATNR + ), + + // CHARASGN 테이블 처리 + replaceSubTableData( + tx, + MATERIAL_MASTER_PART_MATL_CHARASGN, + characteristicAssignments, + 'MATNR', + material.MATNR + ) + ]); + } + }); + + console.log(`${processedMaterials.length}개의 자재 데이터 처리 완료.`); + return true; + } catch (error) { + console.error('데이터베이스 저장 중 오류 발생:', error); + throw error; + } +} diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts new file mode 100644 index 00000000..ecbc23bc --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts @@ -0,0 +1,150 @@ +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE } from "@/db/schema/MDG/mdg"; +import { + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + createErrorResponse, + createSuccessResponse, + ToXMLFields, + withSoapLogging, +} from "../utils"; + +// 스키마에서 직접 타입 추론 +type CMCTBMatBseData = typeof MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE.$inferInsert; + +// XML에서 받는 데이터 구조 +type CMCTBMatBseXML = ToXMLFields<Omit<CMCTBMatBseData, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 +interface ProcessedMaterialData { + materialData: CMCTBMatBseData; +} + +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_RETURN.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_RETURN.wsdl'); + } + + const body = await request.text(); + + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN', + body, + async () => { + console.log('🚀 MATERIAL_MASTER_PART_RETURN 수신 시작, 데이터 길이:', body.length); + + const parser = createXMLParser(['CMCTB_MAT_BSE']); + const parsedData = parser.parse(body); + console.log('XML root keys:', Object.keys(parsedData)); + + const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURNReq'); + + 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_PART_RETURNReq or CMCTB_MAT_BSE data'); + } + + console.log('Validating request data structure:', + `CMCTB_MAT_BSE: ${requestData.CMCTB_MAT_BSE ? 'found' : 'not found'}` + ); + + if (requestData.CMCTB_MAT_BSE && Array.isArray(requestData.CMCTB_MAT_BSE) && requestData.CMCTB_MAT_BSE.length > 0) { + console.log('First CMCTB_MAT_BSE sample:', JSON.stringify(requestData.CMCTB_MAT_BSE[0], null, 2)); + } + + // XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedMaterials = transformMaterialData(requestData.CMCTB_MAT_BSE as CMCTBMatBseXML[] || []); + + // 필수 필드 검증 + for (const materialData of processedMaterials) { + if (!materialData.materialData.MAT_CD) { + throw new Error('Missing required field: MAT_CD in material'); + } + } + + // 데이터베이스 저장 + await saveToDatabase(processedMaterials); + + console.log(`🎉 처리 완료: ${processedMaterials.length}개 자재 데이터`); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/'); + } + ).catch(error => { + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformMaterialData(materialData: CMCTBMatBseXML[]): ProcessedMaterialData[] { + if (!materialData || !Array.isArray(materialData)) { + return []; + } + + return materialData.map(material => { + // 메인 Material 데이터 변환 + const materialConverted = convertXMLToDBData<CMCTBMatBseData>( + material as Record<string, string | undefined>, + ['MAT_CD'] + ); + + // 필수 필드 보정 + if (!materialConverted.MAT_CD) { + materialConverted.MAT_CD = ''; + } + + return { + materialData: materialConverted + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { + console.log(`데이터베이스 저장 시작: ${processedMaterials.length}개 자재 데이터`); + + try { + await db.transaction(async (tx) => { + for (const materialData of processedMaterials) { + const { materialData: material } = materialData; + + if (!material.MAT_CD) { + console.warn('자재코드(MAT_CD)가 없는 항목 발견, 건너뜁니다.'); + continue; + } + + // MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE 테이블 Upsert + await tx.insert(MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE) + .values(material) + .onConflictDoUpdate({ + target: MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE.MAT_CD, + set: { + ...material, + updatedAt: new Date(), + } + }); + } + }); + + console.log(`✅ 데이터베이스 저장 완료: ${processedMaterials.length}개 자료`); + return true; + } catch (error) { + console.error('❌ 데이터베이스 저장 중 오류 발생:', error); + throw error; + } +} diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts index 6c73cf08..cb8de491 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts @@ -1,927 +1,272 @@ -import { XMLParser } from "fast-xml-parser"; -import { readFileSync } from "fs"; -import { NextRequest, NextResponse } from "next/server"; -import { join } from "path"; +import { NextRequest } from "next/server"; import db from "@/db/db"; -import { MATL, DESC, PLNT, UNIT, CLASSASGN, CHARASGN } from "@/db/schema/MDG/modelMaster"; -import { eq } from "drizzle-orm"; - -// 요청 데이터 인터페이스 정의 -interface RequestData { - materials: Material[]; -} - -// 애플리케이션 내부 데이터 모델 (XML 필드와 1:1 매핑) -interface Material { - matnr?: string; // Material Number - mbrsh?: string; // Industry Sector - mtart?: string; // Material Type - lvorm?: string; // Deletion flag - meins?: string; // Base Unit of Measure - matkl?: string; // Material Group - bismt?: string; // Old Material Number - spart?: string; // Division - prdha?: string; // Product Hierarchy - mstae?: string; // Cross-plant Material Status - mstde?: string; // Cross-distribution-chain Material Status - brgew?: string; // Gross Weight - gewei?: string; // Weight Unit - ntgew?: string; // Net Weight - volum?: string; // Volume - voleh?: string; // Volume Unit - groes?: string; // Size/dimensions - laeng?: string; // Length - breit?: string; // Width - hoehe?: string; // Height - meabm?: string; // Unit of Dimension - magrv?: string; // Material Group: Packaging Materials - vhart?: string; // Packaging Material Type - zzname?: string; // Material Name (Custom) - zzspec?: string; // Material Specification (Custom) - zzdesc?: string; // Material Description (Custom) - zzmmtyp?: string; // Material Type (Custom) - zzregdt?: string; // Registration Date (Custom) - zzregtm?: string; // Registration Time (Custom) - zzregus?: string; // Registration User (Custom) - zzappdt?: string; // Approval Date (Custom) - zzapptm?: string; // Approval Time (Custom) - zzappus?: string; // Approval User (Custom) - zzlamdt?: string; // Last Modified Date (Custom) - zzlamtm?: string; // Last Modified Time (Custom) - zzlamus?: string; // Last Modified User (Custom) - zzprflg?: string; // Process Flag (Custom) - zzdokar?: string; // Document Type (Custom) - zzdoknr?: string; // Document Number (Custom) - zzdoktl?: string; // Document Part (Custom) - zzdokvr?: string; // Document Version (Custom) - descriptions?: Description[]; - plants?: Plant[]; - units?: Unit[]; - classAssignments?: ClassAssignment[]; - characteristicAssignments?: CharacteristicAssignment[]; -} - -interface Description { - matnr?: string; // Material Number - spras?: string; // Language Key - maktx?: string; // Material Description -} - -interface Plant { - matnr?: string; // Material Number - werks?: string; // Plant - lvorm?: string; // Deletion Flag - mmsta?: string; // Plant-specific Material Status - mmstd?: string; // Plant-specific Material Status Valid From - zzmtarp?: string; // Custom Field - zzregdt?: string; // Registration Date (Custom) - zzregtm?: string; // Registration Time (Custom) - zzregus?: string; // Registration User (Custom) - zzlamdt?: string; // Last Modified Date (Custom) - zzlamtm?: string; // Last Modified Time (Custom) - zzlamus?: string; // Last Modified User (Custom) - zzprflg?: string; // Process Flag (Custom) -} - -interface Unit { - matnr?: string; // Material Number - meinh?: string; // Unit of Measure - umrez?: string; // Numerator for Conversion to Base UoM - umren?: string; // Denominator for Conversion to Base UoM - laeng?: string; // Length - breit?: string; // Width - hoehe?: string; // Height - meabm?: string; // Unit of Dimension - volum?: string; // Volume - voleh?: string; // Volume Unit - brgew?: string; // Gross Weight - gewei?: string; // Weight Unit -} - -interface ClassAssignment { - matnr?: string; // Material Number - class?: string; // Class - klart?: string; // Class Type -} - -interface CharacteristicAssignment { - matnr?: string; // Material Number - class?: string; // Class - klart?: string; // Class Type - atnam?: string; // Characteristic Name - atwrt?: string; // Characteristic Value - atflv?: string; // Value From - atawe?: string; // Value To - atflb?: string; // Description - ataw1?: string; // Additional Value - atbez?: string; // Characteristic Description - atwtb?: string; // Characteristic Value Description -} - -// SOAP XML 데이터 구조 인터페이스 -// XML 기준 대문자 필드명 사용 -interface MatlXML { - MATNR?: string; - MBRSH?: string; - MTART?: string; - LVORM?: string; - MEINS?: string; - MATKL?: string; - BISMT?: string; - SPART?: string; - PRDHA?: string; - MSTAE?: string; - MSTDE?: string; - BRGEW?: string; - GEWEI?: string; - NTGEW?: string; - VOLUM?: string; - VOLEH?: string; - GROES?: string; - LAENG?: string; - BREIT?: string; - HOEHE?: string; - MEABM?: string; - MAGRV?: string; - VHART?: string; - ZZNAME?: string; - ZZSPEC?: string; - ZZDESC?: string; - ZZMMTYP?: string; - ZZREGDT?: string; - ZZREGTM?: string; - ZZREGUS?: string; - ZZAPPDT?: string; - ZZAPPTM?: string; - ZZAPPUS?: string; - ZZLAMDT?: string; - ZZLAMTM?: string; - ZZLAMUS?: string; - ZZPRFLG?: string; - ZZDOKAR?: string; - ZZDOKNR?: string; - ZZDOKTL?: string; - ZZDOKVR?: string; +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, + replaceSubTableData, + withSoapLogging +} from "../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<Omit<MatlData, 'id' | 'createdAt' | 'updatedAt'>> & { DESC?: DescXML[]; PLNT?: PlntXML[]; UNIT?: UnitXML[]; CLASSASGN?: ClassAsgnXML[]; CHARASGN?: CharAsgnXML[]; -} - -interface DescXML { - MATNR?: string; - SPRAS?: string; - MAKTX?: string; -} - -interface PlntXML { - MATNR?: string; - WERKS?: string; - LVORM?: string; - MMSTA?: string; - MMSTD?: string; - ZZMTARP?: string; - ZZREGDT?: string; - ZZREGTM?: string; - ZZREGUS?: string; - ZZLAMDT?: string; - ZZLAMTM?: string; - ZZLAMUS?: string; - ZZPRFLG?: string; -} - -interface UnitXML { - MATNR?: string; - MEINH?: string; - UMREZ?: string; - UMREN?: string; - LAENG?: string; - BREIT?: string; - HOEHE?: string; - MEABM?: string; - VOLUM?: string; - VOLEH?: string; - BRGEW?: string; - GEWEI?: string; -} - -interface ClassAsgnXML { - MATNR?: string; - CLASS?: string; - KLART?: string; -} - -interface CharAsgnXML { - MATNR?: string; - CLASS?: string; - KLART?: string; - ATNAM?: string; - ATWRT?: string; - ATFLV?: string; - ATAWE?: string; - ATFLB?: string; - ATAW1?: string; - ATBEZ?: string; - ATWTB?: string; -} - -// SOAP Body에 대한 데이터 타입 정의 -interface SoapBodyData { - [key: string]: unknown; - IF_MDZ_EVCP_MODEL_MASTERReq?: Record<string, unknown>; - 'tns:IF_MDZ_EVCP_MODEL_MASTERReq'?: Record<string, unknown>; - 'ns1:IF_MDZ_EVCP_MODEL_MASTERReq'?: Record<string, unknown>; - 'p0:IF_MDZ_EVCP_MODEL_MASTERReq'?: Record<string, unknown>; - MATL?: MatlXML[]; -} - -function serveWsdl() { - try { - const wsdlPath = join(process.cwd(), 'public', 'wsdl', 'IF_MDZ_EVCP_MODEL_MASTER.wsdl'); - const wsdlContent = readFileSync(wsdlPath, 'utf-8'); - - return new NextResponse(wsdlContent, { - headers: { - 'Content-Type': 'text/xml; charset=utf-8', - }, - }); - } catch (error) { - console.error('Failed to read WSDL file:', error); - return new NextResponse('WSDL file not found', { status: 404 }); - } +}; + +type DescXML = ToXMLFields<Omit<MatlDescData, 'id' | 'createdAt' | 'updatedAt'>>; +type PlntXML = ToXMLFields<Omit<MatlPlntData, 'id' | 'createdAt' | 'updatedAt'>>; +type UnitXML = ToXMLFields<Omit<MatlUnitData, 'id' | 'createdAt' | 'updatedAt'>>; +type ClassAsgnXML = ToXMLFields<Omit<MatlClassAsgnData, 'id' | 'createdAt' | 'updatedAt'>>; +type CharAsgnXML = ToXMLFields<Omit<MatlCharAsgnData, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 (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(); - } - - return new NextResponse('Method Not Allowed', { status: 405 }); + 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 }); } -// WSDL 기반의 SOAP 요청 (데이터 전송건) 처리하기 (HTTP) export async function POST(request: NextRequest) { - const url = new URL(request.url); - if (url.searchParams.has('wsdl')) { - return serveWsdl(); - } - - try { - // 요청 본문 (MDZ 데이터)를 가져오기 - const body = await request.text(); - - // 요청 로깅 - console.log('Request Body 일부:', body.substring(0, 200) + (body.length > 200 ? '...' : '')); - - // XML 파서 설정하기 - const parser = new XMLParser({ - ignoreAttributes: false, - attributeNamePrefix: '@_', - parseAttributeValue: false, // 값 조작 방지 - trimValues: true, - isArray: (name: string) => { - return ['MATL', 'DESC', 'PLNT', 'UNIT', 'CLASSASGN', 'CHARASGN'].includes(name); - }, - parseTagValue: false, // 값 조작 방지 - allowBooleanAttributes: true, - }); - - // XML 파싱하기 - const parsedData = parser.parse(body); - - // 디버깅용 - 최상위 구조 확인 - console.log('XML root keys:', Object.keys(parsedData)); - - // 재할당 가능한 변수 선언 - let requestData = null; - - // 가능한 경로 확인 - if (parsedData?.['soap:Envelope']?.['soap:Body']) { - const soapBody = parsedData['soap:Envelope']['soap:Body']; - requestData = extractRequestData(soapBody); - } else if (parsedData?.['SOAP:Envelope']?.['SOAP:Body']) { - const soapBody = parsedData['SOAP:Envelope']['SOAP:Body']; - requestData = extractRequestData(soapBody); - } else if (parsedData?.['Envelope']?.['Body']) { - const soapBody = parsedData['Envelope']['Body']; - requestData = extractRequestData(soapBody); - } else if (parsedData?.['soapenv:Envelope']?.['soapenv:Body']) { - const soapBody = parsedData['soapenv:Envelope']['soapenv:Body']; - requestData = extractRequestData(soapBody); - } else if (parsedData?.['IF_MDZ_EVCP_MODEL_MASTERReq']) { - requestData = parsedData['IF_MDZ_EVCP_MODEL_MASTERReq']; - console.log('Found direct IF_MDZ_EVCP_MODEL_MASTERReq data'); - } else if (parsedData?.['ns1:IF_MDZ_EVCP_MODEL_MASTERReq']) { - requestData = parsedData['ns1:IF_MDZ_EVCP_MODEL_MASTERReq']; - console.log('Found direct ns1:IF_MDZ_EVCP_MODEL_MASTERReq data'); - } else if (parsedData?.['p0:IF_MDZ_EVCP_MODEL_MASTERReq']) { - requestData = parsedData['p0:IF_MDZ_EVCP_MODEL_MASTERReq']; - console.log('Found direct p0:IF_MDZ_EVCP_MODEL_MASTERReq data'); - } else { - // 루트 레벨에서 MATL을 직접 찾기 - if (parsedData?.MATL) { - requestData = parsedData; - console.log('Found MATL data at root level'); - } else { - // 다른 모든 키에 대해 확인 - for (const key of Object.keys(parsedData)) { - const value = parsedData[key]; - // 데이터 구조가 맞는지 확인 (MATL이 있는지) - if (value && value.MATL) { - requestData = value; - console.log(`Found data in root key: ${key}`); - break; - } - - // 키 이름에 IF_MDZ_EVCP_MODEL_MASTERReq가 포함되어 있는지 확인 - if (key.includes('IF_MDZ_EVCP_MODEL_MASTERReq')) { - requestData = value; - console.log(`Found data in root key with matching name: ${key}`); - break; - } - } - } - } - - 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'); + 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', + 'S-ERP', + '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'); } - - // 데이터 유효성 검증 - 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)); - } - - // 데이터 구조 정규화 - MDZ 데이터를 우리 애플리케이션 모델로 변환 - const normalizedData: RequestData = { - materials: transformMatlData(requestData.MATL) - }; - - // 기본 유효성 검사 - 필수 필드 확인 - for (const material of normalizedData.materials) { - if (!material.matnr) { - throw new Error('Missing required field: matnr in material'); - } - } - - // 데이터베이스 저장 - await saveToDatabase(normalizedData); - - console.log(`Processed ${normalizedData.materials.length} materials`); - - // XML 응답 생성 - const xmlResponse = `<?xml version="1.0" encoding="UTF-8"?> -<soap:Envelope - xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" - xmlns:tns="http://60.101.108.100/api/IF_MDZ_EVCP_MODEL_MASTER/"> - <soap:Body> - </soap:Body> -</soap:Envelope>`; - - return new NextResponse(xmlResponse, { - headers: { - 'Content-Type': 'text/xml; charset=utf-8', - }, - }); - } catch (error: unknown) { - console.error('API Error:', error); - - // XML 에러 응답 - const errorResponse = `<?xml version="1.0" encoding="UTF-8"?> -<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> - <soap:Body> - <soap:Fault> - <faultcode>soap:Server</faultcode> - <faultstring>${error instanceof Error ? ('[from eVCP]: ' + error.message) : 'Unknown error'}</faultstring> - </soap:Fault> - </soap:Body> -</soap:Envelope>`; - - return new NextResponse(errorResponse, { - status: 500, - headers: { - 'Content-Type': 'text/xml; charset=utf-8', - }, - }); + } + + // 데이터베이스 저장 + 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); + }); } -// SOAP Body나 루트에서 요청 데이터 추출하는 헬퍼 함수 -function extractRequestData(data: SoapBodyData): Record<string, unknown> | null { - if (!data) return null; +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { + if (!matlData || !Array.isArray(matlData)) { + return []; + } + + return matlData.map(matl => { + // 메인 Material 데이터 변환 (자동) + const material = convertXMLToDBData<MatlData>( + matl as Record<string, string | undefined>, + ['MATNR'] + ); - if (data['IF_MDZ_EVCP_MODEL_MASTERReq']) { - return data['IF_MDZ_EVCP_MODEL_MASTERReq'] as Record<string, unknown>; - } else if (data['tns:IF_MDZ_EVCP_MODEL_MASTERReq']) { - return data['tns:IF_MDZ_EVCP_MODEL_MASTERReq'] as Record<string, unknown>; - } else if (data['ns1:IF_MDZ_EVCP_MODEL_MASTERReq']) { - return data['ns1:IF_MDZ_EVCP_MODEL_MASTERReq'] as Record<string, unknown>; - } else if (data['p0:IF_MDZ_EVCP_MODEL_MASTERReq']) { - return data['p0:IF_MDZ_EVCP_MODEL_MASTERReq'] as Record<string, unknown>; + // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨) + if (!material.MATNR) { + material.MATNR = ''; } - // 다른 키 검색 - for (const key of Object.keys(data)) { - if (key.includes('IF_MDZ_EVCP_MODEL_MASTERReq')) { - return data[key] as Record<string, unknown>; - } - } + // FK 데이터 준비 + const fkData = { MATNR: matl.MATNR || '' }; - // MATL이 직접 있는 경우 - if (data.MATL && Array.isArray(data.MATL)) { - return data; - } + // Description 데이터 변환 (자동) + const descriptions = processNestedArray( + matl.DESC, + (desc) => convertXMLToDBData<MatlDescData>(desc, ['MATNR'], fkData), + fkData + ); - return null; -} - -// XML MATL 데이터를 내부 Material 형식으로 변환하는 함수 -function transformMatlData(matlData: MatlXML[]): Material[] { - if (!matlData || !Array.isArray(matlData)) { - return []; - } + // Plant 데이터 변환 (자동) + const plants = processNestedArray( + matl.PLNT, + (plnt) => convertXMLToDBData<MatlPlntData>(plnt, ['MATNR'], fkData), + fkData + ); - return matlData.map(matl => { - const material: Material = { - matnr: matl.MATNR, - mbrsh: matl.MBRSH, - mtart: matl.MTART, - lvorm: matl.LVORM, - meins: matl.MEINS, - matkl: matl.MATKL, - bismt: matl.BISMT, - spart: matl.SPART, - prdha: matl.PRDHA, - mstae: matl.MSTAE, - mstde: matl.MSTDE, - brgew: matl.BRGEW, - gewei: matl.GEWEI, - ntgew: matl.NTGEW, - volum: matl.VOLUM, - voleh: matl.VOLEH, - groes: matl.GROES, - laeng: matl.LAENG, - breit: matl.BREIT, - hoehe: matl.HOEHE, - meabm: matl.MEABM, - magrv: matl.MAGRV, - vhart: matl.VHART, - zzname: matl.ZZNAME, - zzspec: matl.ZZSPEC, - zzdesc: matl.ZZDESC, - zzmmtyp: matl.ZZMMTYP, - zzregdt: matl.ZZREGDT, - zzregtm: matl.ZZREGTM, - zzregus: matl.ZZREGUS, - zzappdt: matl.ZZAPPDT, - zzapptm: matl.ZZAPPTM, - zzappus: matl.ZZAPPUS, - zzlamdt: matl.ZZLAMDT, - zzlamtm: matl.ZZLAMTM, - zzlamus: matl.ZZLAMUS, - zzprflg: matl.ZZPRFLG, - zzdokar: matl.ZZDOKAR, - zzdoknr: matl.ZZDOKNR, - zzdoktl: matl.ZZDOKTL, - zzdokvr: matl.ZZDOKVR, - }; - - // DESC 항목 처리 - if (matl.DESC && Array.isArray(matl.DESC)) { - material.descriptions = matl.DESC.map((desc: DescXML) => ({ - matnr: desc.MATNR, - spras: desc.SPRAS, - maktx: desc.MAKTX - })); - } - - // PLNT 항목 처리 - if (matl.PLNT && Array.isArray(matl.PLNT)) { - - material.plants = matl.PLNT.map((plnt: PlntXML) => ({ - matnr: plnt.MATNR, - werks: plnt.WERKS, - lvorm: plnt.LVORM, - mmsta: plnt.MMSTA, - mmstd: plnt.MMSTD, - zzmtarp: plnt.ZZMTARP, - zzregdt: plnt.ZZREGDT, - zzregtm: plnt.ZZREGTM, - zzregus: plnt.ZZREGUS, - zzlamdt: plnt.ZZLAMDT, - zzlamtm: plnt.ZZLAMTM, - zzlamus: plnt.ZZLAMUS, - zzprflg: plnt.ZZPRFLG - })); - } - - // UNIT 항목 처리 - if (matl.UNIT && Array.isArray(matl.UNIT)) { - material.units = matl.UNIT.map((unit: UnitXML) => ({ - matnr: unit.MATNR, - meinh: unit.MEINH, - umrez: unit.UMREZ, - umren: unit.UMREN, - laeng: unit.LAENG, - breit: unit.BREIT, - hoehe: unit.HOEHE, - meabm: unit.MEABM, - volum: unit.VOLUM, - voleh: unit.VOLEH, - brgew: unit.BRGEW, - gewei: unit.GEWEI - })); - } - - // CLASSASGN 항목 처리 - if (matl.CLASSASGN && Array.isArray(matl.CLASSASGN)) { - material.classAssignments = matl.CLASSASGN.map((cls: ClassAsgnXML) => ({ - matnr: cls.MATNR, - class: cls.CLASS, - klart: cls.KLART - })); - } - - // CHARASGN 항목 처리 - if (matl.CHARASGN && Array.isArray(matl.CHARASGN)) { - material.characteristicAssignments = matl.CHARASGN.map((char: CharAsgnXML) => ({ - matnr: char.MATNR, - class: char.CLASS, - klart: char.KLART, - atnam: char.ATNAM, - atwrt: char.ATWRT, - atflv: char.ATFLV, - atawe: char.ATAWE, - atflb: char.ATFLB, - ataw1: char.ATAW1, - atbez: char.ATBEZ, - atwtb: char.ATWTB - })); - } - - return material; - }); + // Unit 데이터 변환 (자동) + const units = processNestedArray( + matl.UNIT, + (unit) => convertXMLToDBData<MatlUnitData>(unit, ['MATNR'], fkData), + fkData + ); + + // Class Assignment 데이터 변환 (자동) + const classAssignments = processNestedArray( + matl.CLASSASGN, + (cls) => convertXMLToDBData<MatlClassAsgnData>(cls, ['MATNR'], fkData), + fkData + ); + + // Characteristic Assignment 데이터 변환 (자동) + const characteristicAssignments = processNestedArray( + matl.CHARASGN, + (char) => convertXMLToDBData<MatlCharAsgnData>(char, ['MATNR'], fkData), + fkData + ); + + return { + material, + descriptions, + plants, + units, + classAssignments, + characteristicAssignments + }; + }); } // 데이터베이스 저장 함수 -async function saveToDatabase(data: RequestData) { - console.log(`데이터베이스 저장 함수가 호출됨. ${data.materials.length}개의 자재 데이터 수신.`); - - try { - // 트랜잭션으로 모든 데이터 처리 - await db.transaction(async (tx) => { - for (const material of data.materials) { - if (!material.matnr) { - console.warn('자재번호(MATNR)가 없는 항목 발견, 건너뜁니다.'); - continue; - } - - // 1. MATL 테이블 Upsert - await tx.insert(MATL) - .values({ - MATNR: material.matnr, - MBRSH: material.mbrsh || null, - MTART: material.mtart || null, - LVORM: material.lvorm || null, - MEINS: material.meins || null, - MATKL: material.matkl || null, - BISMT: material.bismt || null, - SPART: material.spart || null, - PRDHA: material.prdha || null, - MSTAE: material.mstae || null, - MSTDE: material.mstde || null, - BRGEW: material.brgew || null, - GEWEI: material.gewei || null, - NTGEW: material.ntgew || null, - VOLUM: material.volum || null, - VOLEH: material.voleh || null, - GROES: material.groes || null, - LAENG: material.laeng || null, - BREIT: material.breit || null, - HOEHE: material.hoehe || null, - MEABM: material.meabm || null, - MAGRV: material.magrv || null, - VHART: material.vhart || null, - ZZNAME: material.zzname || null, - ZZSPEC: material.zzspec || null, - ZZDESC: material.zzdesc || null, - ZZMMTYP: material.zzmmtyp || null, - ZZREGDT: material.zzregdt || null, - ZZREGTM: material.zzregtm || null, - ZZREGUS: material.zzregus || null, - ZZAPPDT: material.zzappdt || null, - ZZAPPTM: material.zzapptm || null, - ZZAPPUS: material.zzappus || null, - ZZLAMDT: material.zzlamdt || null, - ZZLAMTM: material.zzlamtm || null, - ZZLAMUS: material.zzlamus || null, - ZZPRFLG: material.zzprflg || null, - ZZDOKAR: material.zzdokar || null, - ZZDOKNR: material.zzdoknr || null, - ZZDOKTL: material.zzdoktl || null, - ZZDOKVR: material.zzdokvr || null, - }) - .onConflictDoUpdate({ - target: MATL.MATNR, - set: { - MBRSH: material.mbrsh || null, - MTART: material.mtart || null, - LVORM: material.lvorm || null, - MEINS: material.meins || null, - MATKL: material.matkl || null, - BISMT: material.bismt || null, - SPART: material.spart || null, - PRDHA: material.prdha || null, - MSTAE: material.mstae || null, - MSTDE: material.mstde || null, - BRGEW: material.brgew || null, - GEWEI: material.gewei || null, - NTGEW: material.ntgew || null, - VOLUM: material.volum || null, - VOLEH: material.voleh || null, - GROES: material.groes || null, - LAENG: material.laeng || null, - BREIT: material.breit || null, - HOEHE: material.hoehe || null, - MEABM: material.meabm || null, - MAGRV: material.magrv || null, - VHART: material.vhart || null, - ZZNAME: material.zzname || null, - ZZSPEC: material.zzspec || null, - ZZDESC: material.zzdesc || null, - ZZMMTYP: material.zzmmtyp || null, - ZZREGDT: material.zzregdt || null, - ZZREGTM: material.zzregtm || null, - ZZREGUS: material.zzregus || null, - ZZAPPDT: material.zzappdt || null, - ZZAPPTM: material.zzapptm || null, - ZZAPPUS: material.zzappus || null, - ZZLAMDT: material.zzlamdt || null, - ZZLAMTM: material.zzlamtm || null, - ZZLAMUS: material.zzlamus || null, - ZZPRFLG: material.zzprflg || null, - ZZDOKAR: material.zzdokar || null, - ZZDOKNR: material.zzdoknr || null, - ZZDOKTL: material.zzdoktl || null, - ZZDOKVR: material.zzdokvr || null, - updatedAt: new Date(), - } - }); - - // 2. 하위 테이블 데이터 처리 (Upsert) - // DESC 테이블 데이터 처리 - if (material.descriptions && material.descriptions.length > 0) { - // 기존 데이터 조회 (해당 자재의 모든 설명) - const existingDescs = await tx.select().from(DESC) - .where(eq(DESC.MATNR, material.matnr)); - - // 설명 데이터 매핑 - const existingDescsMap = new Map( - existingDescs.map(desc => [`${desc.MATNR}-${desc.SPRAS}`, desc]) - ); - - for (const desc of material.descriptions) { - if (!desc.matnr && !material.matnr) continue; // 자재번호 필수 - - const matnr = desc.matnr || material.matnr; - const spras = desc.spras || ''; - const key = `${matnr}-${spras}`; - - if (existingDescsMap.has(key)) { - // 기존 데이터 업데이트 - await tx.update(DESC) - .set({ - MAKTX: desc.maktx || null, - updatedAt: new Date() - }) - .where(eq(DESC.id, existingDescsMap.get(key)!.id)); - } else { - // 신규 데이터 삽입 - await tx.insert(DESC).values({ - MATNR: matnr, - SPRAS: desc.spras || null, - MAKTX: desc.maktx || null, - }); - } - } - } - - // PLNT 테이블 데이터 처리 - if (material.plants && material.plants.length > 0) { - // 기존 데이터 조회 - const existingPlants = await tx.select().from(PLNT) - .where(eq(PLNT.MATNR, material.matnr)); - - // 플랜트 데이터 매핑 - const existingPlantsMap = new Map( - existingPlants.map(plant => [`${plant.MATNR}-${plant.WERKS}`, plant]) - ); - - for (const plant of material.plants) { - if (!plant.matnr && !material.matnr) continue; // 자재번호 필수 - if (!plant.werks) continue; // 플랜트 코드 필수 - - const matnr = plant.matnr || material.matnr; - const werks = plant.werks; - const key = `${matnr}-${werks}`; - - if (existingPlantsMap.has(key)) { - // 기존 데이터 업데이트 - await tx.update(PLNT) - .set({ - LVORM: plant.lvorm || null, - MMSTA: plant.mmsta || null, - MMSTD: plant.mmstd || null, - ZZMTARP: plant.zzmtarp || null, - ZZREGDT: plant.zzregdt || null, - ZZREGTM: plant.zzregtm || null, - ZZREGUS: plant.zzregus || null, - ZZLAMDT: plant.zzlamdt || null, - ZZLAMTM: plant.zzlamtm || null, - ZZLAMUS: plant.zzlamus || null, - ZZPRFLG: plant.zzprflg || null, - updatedAt: new Date() - }) - .where(eq(PLNT.id, existingPlantsMap.get(key)!.id)); - } else { - // 신규 데이터 삽입 - await tx.insert(PLNT).values({ - MATNR: matnr, - WERKS: werks, - LVORM: plant.lvorm || null, - MMSTA: plant.mmsta || null, - MMSTD: plant.mmstd || null, - ZZMTARP: plant.zzmtarp || null, - ZZREGDT: plant.zzregdt || null, - ZZREGTM: plant.zzregtm || null, - ZZREGUS: plant.zzregus || null, - ZZLAMDT: plant.zzlamdt || null, - ZZLAMTM: plant.zzlamtm || null, - ZZLAMUS: plant.zzlamus || null, - ZZPRFLG: plant.zzprflg || null, - }); - } - } - } - - // UNIT 테이블 데이터 처리 - if (material.units && material.units.length > 0) { - // 기존 데이터 조회 - const existingUnits = await tx.select().from(UNIT) - .where(eq(UNIT.MATNR, material.matnr)); - - // 단위 데이터 매핑 - const existingUnitsMap = new Map( - existingUnits.map(unit => [`${unit.MATNR}-${unit.MEINH}`, unit]) - ); - - for (const unit of material.units) { - if (!unit.matnr && !material.matnr) continue; // 자재번호 필수 - if (!unit.meinh) continue; // 단위 코드 필수 - - const matnr = unit.matnr || material.matnr; - const meinh = unit.meinh; - const key = `${matnr}-${meinh}`; - - if (existingUnitsMap.has(key)) { - // 기존 데이터 업데이트 - await tx.update(UNIT) - .set({ - UMREZ: unit.umrez || null, - UMREN: unit.umren || null, - LAENG: unit.laeng || null, - BREIT: unit.breit || null, - HOEHE: unit.hoehe || null, - MEABM: unit.meabm || null, - VOLUM: unit.volum || null, - VOLEH: unit.voleh || null, - BRGEW: unit.brgew || null, - GEWEI: unit.gewei || null, - updatedAt: new Date() - }) - .where(eq(UNIT.id, existingUnitsMap.get(key)!.id)); - } else { - // 신규 데이터 삽입 - await tx.insert(UNIT).values({ - MATNR: matnr, - MEINH: meinh, - UMREZ: unit.umrez || null, - UMREN: unit.umren || null, - LAENG: unit.laeng || null, - BREIT: unit.breit || null, - HOEHE: unit.hoehe || null, - MEABM: unit.meabm || null, - VOLUM: unit.volum || null, - VOLEH: unit.voleh || null, - BRGEW: unit.brgew || null, - GEWEI: unit.gewei || null, - }); - } - } - } - - // CLASSASGN 테이블 데이터 처리 - if (material.classAssignments && material.classAssignments.length > 0) { - // 기존 데이터 조회 - const existingClassAsgns = await tx.select().from(CLASSASGN) - .where(eq(CLASSASGN.MATNR, material.matnr)); - - // 클래스 할당 데이터 매핑 - const existingClassAsgnsMap = new Map( - existingClassAsgns.map(cls => [`${cls.MATNR}-${cls.CLASS}-${cls.KLART}`, cls]) - ); - - for (const cls of material.classAssignments) { - if (!cls.matnr && !material.matnr) continue; // 자재번호 필수 - if (!cls.class || !cls.klart) continue; // 클래스 및 유형 필수 - - const matnr = cls.matnr || material.matnr; - const clsVal = cls.class; - const klart = cls.klart; - const key = `${matnr}-${clsVal}-${klart}`; - - if (!existingClassAsgnsMap.has(key)) { - // 클래스 할당은 기본키 자체가 변경되는 경우가 드물어 신규 삽입만 처리 - await tx.insert(CLASSASGN).values({ - MATNR: matnr, - CLASS: clsVal, - KLART: klart, - }); - } - } - } +async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { + console.log(`데이터베이스 저장 함수가 호출됨. ${processedMaterials.length}개의 자재 데이터 수신.`); + + try { + await db.transaction(async (tx) => { + for (const materialData of processedMaterials) { + const { material, descriptions, plants, units, classAssignments, characteristicAssignments } = materialData; + + if (!material.MATNR) { + console.warn('자재번호(MATNR)가 없는 항목 발견, 건너뜁니다.'); + continue; + } - // CHARASGN 테이블 데이터 처리 - if (material.characteristicAssignments && material.characteristicAssignments.length > 0) { - // 기존 데이터 조회 - const existingCharAsgns = await tx.select().from(CHARASGN) - .where(eq(CHARASGN.MATNR, material.matnr)); - - // 특성 할당 데이터 매핑 - const existingCharAsgnsMap = new Map( - existingCharAsgns.map(char => - [`${char.MATNR}-${char.CLASS}-${char.KLART}-${char.ATNAM}`, char] - ) - ); - - for (const char of material.characteristicAssignments) { - if (!char.matnr && !material.matnr) continue; // 자재번호 필수 - if (!char.class || !char.klart || !char.atnam) continue; // 클래스, 유형, 특성명 필수 - - const matnr = char.matnr || material.matnr; - const clsVal = char.class; - const klart = char.klart; - const atnam = char.atnam; - const key = `${matnr}-${clsVal}-${klart}-${atnam}`; - - if (existingCharAsgnsMap.has(key)) { - // 기존 데이터 업데이트 - await tx.update(CHARASGN) - .set({ - ATWRT: char.atwrt || null, - ATFLV: char.atflv || null, - ATAWE: char.atawe || null, - ATFLB: char.atflb || null, - ATAW1: char.ataw1 || null, - ATBEZ: char.atbez || null, - ATWTB: char.atwtb || null, - updatedAt: new Date() - }) - .where(eq(CHARASGN.id, existingCharAsgnsMap.get(key)!.id)); - } else { - // 신규 데이터 삽입 - await tx.insert(CHARASGN).values({ - MATNR: matnr, - CLASS: clsVal, - KLART: klart, - ATNAM: atnam, - ATWRT: char.atwrt || null, - ATFLV: char.atflv || null, - ATAWE: char.atawe || null, - ATFLB: char.atflb || null, - ATAW1: char.ataw1 || null, - ATBEZ: char.atbez || null, - ATWTB: char.atwtb || null, - }); - } - } - } + // 1. MATL 테이블 Upsert (최상위 테이블) + await tx.insert(MODEL_MASTER_MATL) + .values(material) + .onConflictDoUpdate({ + target: MODEL_MASTER_MATL.MATNR, + set: { + ...material, + updatedAt: new Date(), } - }); + }); + + // 2. 하위 테이블 데이터 처리 - FK 기준으로 전체 삭제 후 재삽입 + await Promise.all([ + // DESC 테이블 처리 + replaceSubTableData( + tx, + MODEL_MASTER_MATL_DESC, + descriptions, + 'MATNR', + material.MATNR + ), + + // PLNT 테이블 처리 + replaceSubTableData( + tx, + MODEL_MASTER_MATL_PLNT, + plants, + 'MATNR', + material.MATNR + ), + + // UNIT 테이블 처리 + replaceSubTableData( + tx, + MODEL_MASTER_MATL_UNIT, + units, + 'MATNR', + material.MATNR + ), + + // CLASSASGN 테이블 처리 + replaceSubTableData( + tx, + MODEL_MASTER_MATL_CLASSASGN, + classAssignments, + 'MATNR', + material.MATNR + ), + + // CHARASGN 테이블 처리 + replaceSubTableData( + tx, + MODEL_MASTER_MATL_CHARASGN, + characteristicAssignments, + 'MATNR', + material.MATNR + ) + ]); + } + }); - console.log(`${data.materials.length}개의 자재 데이터 처리 완료.`); - return true; - } catch (error) { - console.error('데이터베이스 저장 중 오류 발생:', error); - throw error; - } + console.log(`${processedMaterials.length}개의 자재 데이터 처리 완료.`); + return true; + } catch (error) { + console.error('데이터베이스 저장 중 오류 발생:', error); + throw error; + } } diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts new file mode 100644 index 00000000..c3f214e6 --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts @@ -0,0 +1,435 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { + ORGANIZATION_MASTER_HRHMTB_CCTR, + ORGANIZATION_MASTER_HRHMTB_CCTR_TEXT, + ORGANIZATION_MASTER_HRHMTB_PCTR, + ORGANIZATION_MASTER_HRHMTB_ZBUKRS, + ORGANIZATION_MASTER_HRHMTB_ZEKGRP, + ORGANIZATION_MASTER_HRHMTB_ZEKORG, + ORGANIZATION_MASTER_HRHMTB_ZGSBER, + ORGANIZATION_MASTER_HRHMTB_ZGSBER_TEXT, + ORGANIZATION_MASTER_HRHMTB_ZLGORT, + ORGANIZATION_MASTER_HRHMTB_ZSPART, + ORGANIZATION_MASTER_HRHMTB_ZVKBUR, + ORGANIZATION_MASTER_HRHMTB_ZVKGRP, + ORGANIZATION_MASTER_HRHMTB_ZVKORG, + ORGANIZATION_MASTER_HRHMTB_ZVSTEL, + ORGANIZATION_MASTER_HRHMTB_ZVTWEG, + ORGANIZATION_MASTER_HRHMTB_ZWERKS +} from "@/db/schema/MDG/mdg"; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createErrorResponse, + createSuccessResponse, + replaceSubTableData, + withSoapLogging +} from "../utils"; + +// 스키마에서 직접 타입 추론 +type CctrData = typeof ORGANIZATION_MASTER_HRHMTB_CCTR.$inferInsert; +type CctrTextData = typeof ORGANIZATION_MASTER_HRHMTB_CCTR_TEXT.$inferInsert; +type PctrData = typeof ORGANIZATION_MASTER_HRHMTB_PCTR.$inferInsert; +type ZbukrsData = typeof ORGANIZATION_MASTER_HRHMTB_ZBUKRS.$inferInsert; +type ZekgrpData = typeof ORGANIZATION_MASTER_HRHMTB_ZEKGRP.$inferInsert; +type ZekorgData = typeof ORGANIZATION_MASTER_HRHMTB_ZEKORG.$inferInsert; +type ZgsberData = typeof ORGANIZATION_MASTER_HRHMTB_ZGSBER.$inferInsert; +type ZgsberTextData = typeof ORGANIZATION_MASTER_HRHMTB_ZGSBER_TEXT.$inferInsert; +type ZlgortData = typeof ORGANIZATION_MASTER_HRHMTB_ZLGORT.$inferInsert; +type ZspartData = typeof ORGANIZATION_MASTER_HRHMTB_ZSPART.$inferInsert; +type ZvkburData = typeof ORGANIZATION_MASTER_HRHMTB_ZVKBUR.$inferInsert; +type ZvkgrpData = typeof ORGANIZATION_MASTER_HRHMTB_ZVKGRP.$inferInsert; +type ZvkorgData = typeof ORGANIZATION_MASTER_HRHMTB_ZVKORG.$inferInsert; +type ZvstelData = typeof ORGANIZATION_MASTER_HRHMTB_ZVSTEL.$inferInsert; +type ZvtwegData = typeof ORGANIZATION_MASTER_HRHMTB_ZVTWEG.$inferInsert; +type ZwerksData = typeof ORGANIZATION_MASTER_HRHMTB_ZWERKS.$inferInsert; + +// XML에서 받는 데이터 구조 +type CctrXML = ToXMLFields<Omit<CctrData, 'id' | 'createdAt' | 'updatedAt'>> & { + TEXT?: TextXML[]; +}; + +type TextXML = ToXMLFields<Omit<CctrTextData, 'id' | 'createdAt' | 'updatedAt'>>; + +type PctrXML = ToXMLFields<Omit<PctrData, 'id' | 'createdAt' | 'updatedAt'>> & { + TEXT?: TextXML[]; +}; + +type ZbukrsXML = ToXMLFields<Omit<ZbukrsData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZekgrpXML = ToXMLFields<Omit<ZekgrpData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZekorgXML = ToXMLFields<Omit<ZekorgData, 'id' | 'createdAt' | 'updatedAt'>>; + +type ZgsberXML = ToXMLFields<Omit<ZgsberData, 'id' | 'createdAt' | 'updatedAt'>> & { + TEXT?: ZgsberTextXML[]; +}; + +type ZgsberTextXML = ToXMLFields<Omit<ZgsberTextData, 'id' | 'createdAt' | 'updatedAt'>>; + +type ZlgortXML = ToXMLFields<Omit<ZlgortData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZspartXML = ToXMLFields<Omit<ZspartData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZvkburXML = ToXMLFields<Omit<ZvkburData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZvkgrpXML = ToXMLFields<Omit<ZvkgrpData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZvkorgXML = ToXMLFields<Omit<ZvkorgData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZvstelXML = ToXMLFields<Omit<ZvstelData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZvtwegXML = ToXMLFields<Omit<ZvtwegData, 'id' | 'createdAt' | 'updatedAt'>>; +type ZwerksXML = ToXMLFields<Omit<ZwerksData, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 +interface ProcessedOrganizationData { + cctrItems: Array<{ cctr: CctrData; texts: CctrTextData[] }>; + pctrItems: Array<{ pctr: PctrData; texts: CctrTextData[] }>; + zbukrsItems: ZbukrsData[]; + zekgrpItems: ZekgrpData[]; + zekorgItems: ZekorgData[]; + zgsberItems: Array<{ zgsber: ZgsberData; texts: ZgsberTextData[] }>; + zlgortItems: ZlgortData[]; + zspartItems: ZspartData[]; + zvkburItems: ZvkburData[]; + zvkgrpItems: ZvkgrpData[]; + zvkorgItems: ZvkorgData[]; + zvstelItems: ZvstelData[]; + zvtwegItems: ZvtwegData[]; + zwerksItems: ZwerksData[]; +} + +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_MDZ_EVCP_ORGANIZATION_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_ORGANIZATION_MASTER.wsdl'); + } + + const body = await request.text(); + + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_ORGANIZATION_MASTER', + body, + async () => { + console.log('🚀 ORGANIZATION_MASTER 수신 시작, 데이터 길이:', body.length); + + const parser = createXMLParser([ + 'HRHMTB_CCTR', 'HRHMTB_PCTR', 'HRHMTB_ZBUKRS', 'HRHMTB_ZEKGRP', + 'HRHMTB_ZEKORG', 'HRHMTB_ZGSBER', 'HRHMTB_ZLGORT', 'HRHMTB_ZSPART', + 'HRHMTB_ZVKBUR', 'HRHMTB_ZVKGRP', 'HRHMTB_ZVKORG', 'HRHMTB_ZVSTEL', + 'HRHMTB_ZVTWEG', 'HRHMTB_ZWERKS', 'TEXT' + ]); + + const parsedData = parser.parse(body); + console.log('XML root keys:', Object.keys(parsedData)); + + const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_ORGANIZATION_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_ORGANIZATION_MASTERReq data'); + } + + console.log('Validating request data structure:', + Object.keys(requestData).map(key => `${key}: ${requestData[key] ? 'found' : 'not found'}`).join(', ') + ); + + // XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedOrganizations = transformOrganizationData(requestData); + + // 데이터베이스 저장 + await saveToDatabase(processedOrganizations); + + console.log('🎉 처리 완료: 조직 마스터 데이터'); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_ORGANIZATION_MASTER/'); + } + ).catch(error => { + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformOrganizationData(requestData: any): ProcessedOrganizationData { + const result: ProcessedOrganizationData = { + cctrItems: [], + pctrItems: [], + zbukrsItems: [], + zekgrpItems: [], + zekorgItems: [], + zgsberItems: [], + zlgortItems: [], + zspartItems: [], + zvkburItems: [], + zvkgrpItems: [], + zvkorgItems: [], + zvstelItems: [], + zvtwegItems: [], + zwerksItems: [] + }; + + // HRHMTB_CCTR 처리 + if (requestData.items1 && Array.isArray(requestData.items1)) { + result.cctrItems = requestData.items1.map((item: CctrXML) => { + const cctr = convertXMLToDBData<CctrData>( + item as Record<string, string | undefined>, + ['CCTR', 'KOKRS', 'DATBI'] + ); + + const fkData = { CCTR: item.CCTR || '' }; + const texts = processNestedArray( + item.TEXT, + (text) => convertXMLToDBData<CctrTextData>(text, [], fkData), + fkData + ); + + return { cctr, texts }; + }); + } + + // HRHMTB_PCTR 처리 + if (requestData.items2 && Array.isArray(requestData.items2)) { + result.pctrItems = requestData.items2.map((item: PctrXML) => { + const pctr = convertXMLToDBData<PctrData>( + item as Record<string, string | undefined>, + ['PCTR', 'KOKRS', 'DATBI'] + ); + + const fkData = { CCTR: item.PCTR || '' }; // TEXT 테이블은 CCTR 필드를 사용 + const texts = processNestedArray( + item.TEXT, + (text) => convertXMLToDBData<CctrTextData>(text, [], fkData), + fkData + ); + + return { pctr, texts }; + }); + } + + // HRHMTB_ZBUKRS 처리 + if (requestData.items3 && Array.isArray(requestData.items3)) { + result.zbukrsItems = requestData.items3.map((item: ZbukrsXML) => + convertXMLToDBData<ZbukrsData>( + item as Record<string, string | undefined>, + ['ZBUKRS'] + ) + ); + } + + // HRHMTB_ZEKGRP 처리 + if (requestData.items4 && Array.isArray(requestData.items4)) { + result.zekgrpItems = requestData.items4.map((item: ZekgrpXML) => + convertXMLToDBData<ZekgrpData>( + item as Record<string, string | undefined>, + ['ZEKGRP'] + ) + ); + } + + // HRHMTB_ZEKORG 처리 + if (requestData.items5 && Array.isArray(requestData.items5)) { + result.zekorgItems = requestData.items5.map((item: ZekorgXML) => + convertXMLToDBData<ZekorgData>( + item as Record<string, string | undefined>, + ['ZEKORG'] + ) + ); + } + + // HRHMTB_ZGSBER 처리 + if (requestData.items6 && Array.isArray(requestData.items6)) { + result.zgsberItems = requestData.items6.map((item: ZgsberXML) => { + const zgsber = convertXMLToDBData<ZgsberData>( + item as Record<string, string | undefined>, + ['ZGSBER'] + ); + + const fkData = { ZGSBER: item.ZGSBER || '' }; + const texts = processNestedArray( + item.TEXT, + (text) => convertXMLToDBData<ZgsberTextData>(text, ['LANGU'], fkData), + fkData + ); + + return { zgsber, texts }; + }); + } + + // HRHMTB_ZLGORT 처리 + if (requestData.items7 && Array.isArray(requestData.items7)) { + result.zlgortItems = requestData.items7.map((item: ZlgortXML) => + convertXMLToDBData<ZlgortData>( + item as Record<string, string | undefined>, + ['ZLGORT', 'ZWERKS'] + ) + ); + } + + // HRHMTB_ZSPART 처리 + if (requestData.items8 && Array.isArray(requestData.items8)) { + result.zspartItems = requestData.items8.map((item: ZspartXML) => + convertXMLToDBData<ZspartData>( + item as Record<string, string | undefined>, + ['ZSPART'] + ) + ); + } + + // HRHMTB_ZVKBUR 처리 + if (requestData.items9 && Array.isArray(requestData.items9)) { + result.zvkburItems = requestData.items9.map((item: ZvkburXML) => + convertXMLToDBData<ZvkburData>( + item as Record<string, string | undefined>, + ['ZVKBUR'] + ) + ); + } + + // HRHMTB_ZVKGRP 처리 + if (requestData.items10 && Array.isArray(requestData.items10)) { + result.zvkgrpItems = requestData.items10.map((item: ZvkgrpXML) => + convertXMLToDBData<ZvkgrpData>( + item as Record<string, string | undefined>, + ['ZVKGRP'] + ) + ); + } + + // HRHMTB_ZVKORG 처리 + if (requestData.items11 && Array.isArray(requestData.items11)) { + result.zvkorgItems = requestData.items11.map((item: ZvkorgXML) => + convertXMLToDBData<ZvkorgData>( + item as Record<string, string | undefined>, + ['ZVKORG'] + ) + ); + } + + // HRHMTB_ZVSTEL 처리 + if (requestData.items12 && Array.isArray(requestData.items12)) { + result.zvstelItems = requestData.items12.map((item: ZvstelXML) => + convertXMLToDBData<ZvstelData>( + item as Record<string, string | undefined>, + ['ZVSTEL'] + ) + ); + } + + // HRHMTB_ZVTWEG 처리 + if (requestData.items13 && Array.isArray(requestData.items13)) { + result.zvtwegItems = requestData.items13.map((item: ZvtwegXML) => + convertXMLToDBData<ZvtwegData>( + item as Record<string, string | undefined>, + ['ZVTWEG'] + ) + ); + } + + // HRHMTB_ZWERKS 처리 + if (requestData.items14 && Array.isArray(requestData.items14)) { + result.zwerksItems = requestData.items14.map((item: ZwerksXML) => + convertXMLToDBData<ZwerksData>( + item as Record<string, string | undefined>, + ['ZWERKS'] + ) + ); + } + + return result; +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedOrganizations: ProcessedOrganizationData) { + console.log('데이터베이스 저장 함수가 호출됨. 조직 마스터 데이터 수신.'); + + try { + await db.transaction(async (tx) => { + // CCTR 테이블 처리 + for (const { cctr, texts } of processedOrganizations.cctrItems) { + if (!cctr.CCTR) continue; + + await tx.insert(ORGANIZATION_MASTER_HRHMTB_CCTR) + .values(cctr) + .onConflictDoUpdate({ + target: ORGANIZATION_MASTER_HRHMTB_CCTR.CCTR, + set: { ...cctr, updatedAt: new Date() } + }); + + await replaceSubTableData(tx, ORGANIZATION_MASTER_HRHMTB_CCTR_TEXT, texts, 'CCTR', cctr.CCTR); + } + + // PCTR 테이블 처리 + for (const { pctr, texts } of processedOrganizations.pctrItems) { + if (!pctr.PCTR) continue; + + await tx.insert(ORGANIZATION_MASTER_HRHMTB_PCTR) + .values(pctr) + .onConflictDoUpdate({ + target: ORGANIZATION_MASTER_HRHMTB_PCTR.PCTR, + set: { ...pctr, updatedAt: new Date() } + }); + + // PCTR의 TEXT는 CCTR_TEXT 테이블을 사용하므로 처리하지 않음 + } + + // 나머지 단일 테이블들 처리 + const tableProcessors = [ + { items: processedOrganizations.zbukrsItems, table: ORGANIZATION_MASTER_HRHMTB_ZBUKRS, key: 'ZBUKRS' }, + { items: processedOrganizations.zekgrpItems, table: ORGANIZATION_MASTER_HRHMTB_ZEKGRP, key: 'ZEKGRP' }, + { items: processedOrganizations.zekorgItems, table: ORGANIZATION_MASTER_HRHMTB_ZEKORG, key: 'ZEKORG' }, + { items: processedOrganizations.zlgortItems, table: ORGANIZATION_MASTER_HRHMTB_ZLGORT, key: 'ZLGORT' }, + { items: processedOrganizations.zspartItems, table: ORGANIZATION_MASTER_HRHMTB_ZSPART, key: 'ZSPART' }, + { items: processedOrganizations.zvkburItems, table: ORGANIZATION_MASTER_HRHMTB_ZVKBUR, key: 'ZVKBUR' }, + { items: processedOrganizations.zvkgrpItems, table: ORGANIZATION_MASTER_HRHMTB_ZVKGRP, key: 'ZVKGRP' }, + { items: processedOrganizations.zvkorgItems, table: ORGANIZATION_MASTER_HRHMTB_ZVKORG, key: 'ZVKORG' }, + { items: processedOrganizations.zvstelItems, table: ORGANIZATION_MASTER_HRHMTB_ZVSTEL, key: 'ZVSTEL' }, + { items: processedOrganizations.zvtwegItems, table: ORGANIZATION_MASTER_HRHMTB_ZVTWEG, key: 'ZVTWEG' }, + { items: processedOrganizations.zwerksItems, table: ORGANIZATION_MASTER_HRHMTB_ZWERKS, key: 'ZWERKS' } + ]; + + for (const { items, table, key } of tableProcessors) { + for (const item of items) { + if (!(item as any)[key]) continue; + + await tx.insert(table) + .values(item) + .onConflictDoUpdate({ + target: (table as any)[key], + set: { ...item, updatedAt: new Date() } + }); + } + } + + // ZGSBER 테이블 처리 (TEXT 포함) + for (const { zgsber, texts } of processedOrganizations.zgsberItems) { + if (!zgsber.ZGSBER) continue; + + await tx.insert(ORGANIZATION_MASTER_HRHMTB_ZGSBER) + .values(zgsber) + .onConflictDoUpdate({ + target: ORGANIZATION_MASTER_HRHMTB_ZGSBER.ZGSBER, + set: { ...zgsber, updatedAt: new Date() } + }); + + await replaceSubTableData(tx, ORGANIZATION_MASTER_HRHMTB_ZGSBER_TEXT, texts, 'ZGSBER', zgsber.ZGSBER); + } + }); + + console.log('조직 마스터 데이터 처리 완료.'); + return true; + } catch (error) { + console.error('데이터베이스 저장 중 오류 발생:', error); + throw error; + } +} diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts new file mode 100644 index 00000000..167c5c5d --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { + PROJECT_MASTER_CMCTB_PROJ_MAST +} from "@/db/schema/MDG/mdg"; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + createErrorResponse, + createSuccessResponse, + withSoapLogging +} from "../utils"; + +// 스키마에서 직접 타입 추론 +type ProjectData = typeof PROJECT_MASTER_CMCTB_PROJ_MAST.$inferInsert; + +// XML에서 받는 데이터 구조 +type ProjectXML = ToXMLFields<Omit<ProjectData, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 +interface ProcessedProjectData { + project: ProjectData; +} + +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_MDZ_EVCP_PROJECT_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_PROJECT_MASTER.wsdl'); + } + + const body = await request.text(); + + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_PROJECT_MASTER', + body, + async () => { + console.log('Request Body 일부:', body.substring(0, 200) + (body.length > 200 ? '...' : '')); + + const parser = createXMLParser(['CMCTB_PROJ_MAST']); + const parsedData = parser.parse(body); + console.log('XML root keys:', Object.keys(parsedData)); + + const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_PROJECT_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_PROJECT_MASTERReq or CMCTB_PROJ_MAST data'); + } + + console.log('Validating request data structure:', + `CMCTB_PROJ_MAST: ${requestData.CMCTB_PROJ_MAST ? 'found' : 'not found'}` + ); + + if (requestData.CMCTB_PROJ_MAST && Array.isArray(requestData.CMCTB_PROJ_MAST) && requestData.CMCTB_PROJ_MAST.length > 0) { + console.log('First CMCTB_PROJ_MAST sample:', JSON.stringify(requestData.CMCTB_PROJ_MAST[0], null, 2)); + } + + // XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedProjects = transformProjectData(requestData.CMCTB_PROJ_MAST as ProjectXML[] || []); + + // 필수 필드 검증 + for (const projectData of processedProjects) { + if (!projectData.project.PROJ_NO) { + throw new Error('Missing required field: PROJ_NO in project'); + } + } + + // 데이터베이스 저장 + await saveToDatabase(processedProjects); + + console.log(`Processed ${processedProjects.length} projects`); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_PROJECT_MASTER/'); + } + ).catch(error => { + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformProjectData(projectData: ProjectXML[]): ProcessedProjectData[] { + if (!projectData || !Array.isArray(projectData)) { + return []; + } + + return projectData.map(proj => { + // Project 데이터 변환 + const project = convertXMLToDBData<ProjectData>( + proj as Record<string, string | undefined>, + ['PROJ_NO'] + ); + + // 필수 필드 보정 + if (!project.PROJ_NO) { + project.PROJ_NO = ''; + } + + return { + project + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedProjects: ProcessedProjectData[]) { + console.log(`데이터베이스 저장 함수가 호출됨. ${processedProjects.length}개의 프로젝트 데이터 수신.`); + + try { + await db.transaction(async (tx) => { + for (const projectData of processedProjects) { + const { project } = projectData; + + if (!project.PROJ_NO) { + console.warn('프로젝트번호(PROJ_NO)가 없는 항목 발견, 건너뜁니다.'); + continue; + } + + // Project 테이블 Upsert + await tx.insert(PROJECT_MASTER_CMCTB_PROJ_MAST) + .values(project) + .onConflictDoUpdate({ + target: PROJECT_MASTER_CMCTB_PROJ_MAST.PROJ_NO, + set: { + ...project, + updatedAt: new Date(), + } + }); + } + }); + + console.log(`${processedProjects.length}개의 프로젝트 데이터 처리 완료.`); + return true; + } catch (error) { + console.error('데이터베이스 저장 중 오류 발생:', error); + throw error; + } +} diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts new file mode 100644 index 00000000..e257a28a --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts @@ -0,0 +1,336 @@ +import { NextRequest } from "next/server"; +import db from "@/db/db"; +import { + VENDOR_MASTER_BP_HEADER, + VENDOR_MASTER_BP_HEADER_ADDRESS, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_FAX, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL, + VENDOR_MASTER_BP_HEADER_BP_TAXNUM, + VENDOR_MASTER_BP_HEADER_BP_VENGEN, + VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY, + VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY_BP_WHTAX, + VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG, + VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG_ZVPFN +} from "@/db/schema/MDG/mdg"; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createErrorResponse, + createSuccessResponse, + replaceSubTableData, + withSoapLogging +} from "../utils"; + +// 스키마에서 직접 타입 추론 +type VendorHeaderData = typeof VENDOR_MASTER_BP_HEADER.$inferInsert; +type AddressData = typeof VENDOR_MASTER_BP_HEADER_ADDRESS.$inferInsert; +type AdEmailData = typeof VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL.$inferInsert; +type AdFaxData = typeof VENDOR_MASTER_BP_HEADER_ADDRESS_AD_FAX.$inferInsert; +type AdPostalData = typeof VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL.$inferInsert; +type AdTelData = typeof VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL.$inferInsert; +type AdUrlData = typeof VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL.$inferInsert; +type BpTaxnumData = typeof VENDOR_MASTER_BP_HEADER_BP_TAXNUM.$inferInsert; +type BpVengenData = typeof VENDOR_MASTER_BP_HEADER_BP_VENGEN.$inferInsert; +type BpCompnyData = typeof VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY.$inferInsert; +type BpWhtaxData = typeof VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY_BP_WHTAX.$inferInsert; +type BpPorgData = typeof VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG.$inferInsert; +type ZvpfnData = typeof VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG_ZVPFN.$inferInsert; + +// XML 구조 타입 +type VendorHeaderXML = ToXMLFields<Omit<VendorHeaderData, 'id' | 'createdAt' | 'updatedAt'>> & { + ADDRESS?: AddressXML[]; + BP_TAXNUM?: BpTaxnumXML[]; + BP_VENGEN?: BpVengenXML[]; +}; + +type AddressXML = ToXMLFields<Omit<AddressData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>> & { + AD_EMAIL?: AdEmailXML[]; + AD_FAX?: AdFaxXML[]; + AD_POSTAL?: AdPostalXML[]; + AD_TEL?: AdTelXML[]; + AD_URL?: AdUrlXML[]; +}; + +type AdEmailXML = ToXMLFields<Omit<AdEmailData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>>; +type AdFaxXML = ToXMLFields<Omit<AdFaxData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>>; +type AdPostalXML = ToXMLFields<Omit<AdPostalData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>>; +type AdTelXML = ToXMLFields<Omit<AdTelData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>>; +type AdUrlXML = ToXMLFields<Omit<AdUrlData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>>; + +type BpTaxnumXML = ToXMLFields<Omit<BpTaxnumData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>>; + +type BpVengenXML = ToXMLFields<Omit<BpVengenData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>> & { + BP_COMPNY?: BpCompnyXML[]; + BP_PORG?: BpPorgXML[]; +}; + +type BpCompnyXML = ToXMLFields<Omit<BpCompnyData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>> & { + BP_WHTAX?: BpWhtaxXML[]; +}; + +type BpWhtaxXML = ToXMLFields<Omit<BpWhtaxData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>>; + +type BpPorgXML = ToXMLFields<Omit<BpPorgData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>> & { + ZVPFN?: ZvpfnXML[]; +}; + +type ZvpfnXML = ToXMLFields<Omit<ZvpfnData, 'id' | 'createdAt' | 'updatedAt' | 'VNDRCD'>>; + +// 처리된 데이터 구조 +interface ProcessedVendorData { + vendorHeader: VendorHeaderData; + addresses: AddressData[]; + adEmails: AdEmailData[]; + adFaxes: AdFaxData[]; + adPostals: AdPostalData[]; + adTels: AdTelData[]; + adUrls: AdUrlData[]; + bpTaxnums: BpTaxnumData[]; + bpVengens: BpVengenData[]; + bpCompnies: BpCompnyData[]; + bpWhtaxes: BpWhtaxData[]; + bpPorgs: BpPorgData[]; + zvpfns: ZvpfnData[]; +} + +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_MDZ_EVCP_VENDOR_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_VENDOR_MASTER.wsdl'); + } + + const body = await request.text(); + + return withSoapLogging( + 'INBOUND', + 'S-ERP', + 'IF_MDZ_EVCP_VENDOR_MASTER', + body, + async () => { + console.log('🚀 VENDOR_MASTER 수신 시작, 데이터 길이:', body.length); + + const parser = createXMLParser([ + 'BP_HEADER', 'ADDRESS', 'AD_EMAIL', 'AD_FAX', 'AD_POSTAL', 'AD_TEL', 'AD_URL', + 'BP_TAXNUM', 'BP_VENGEN', 'BP_COMPNY', 'BP_WHTAX', 'BP_PORG', 'ZVPFN' + ]); + + const parsedData = parser.parse(body); + console.log('XML root keys:', Object.keys(parsedData)); + + const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_VENDOR_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_VENDOR_MASTERReq or BP_HEADER data'); + } + + console.log('Validating request data structure:', + `BP_HEADER: ${requestData.BP_HEADER ? 'found' : 'not found'}` + ); + + if (requestData.BP_HEADER && Array.isArray(requestData.BP_HEADER) && requestData.BP_HEADER.length > 0) { + console.log('First BP_HEADER sample:', JSON.stringify(requestData.BP_HEADER[0], null, 2)); + } + + // XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedVendors = transformVendorData(requestData.BP_HEADER as VendorHeaderXML[] || []); + + // 필수 필드 검증 + for (const vendorData of processedVendors) { + if (!vendorData.vendorHeader.VNDRCD) { + throw new Error('Missing required field: VNDRCD in vendor'); + } + } + + // 데이터베이스 저장 + await saveToDatabase(processedVendors); + + console.log(`🎉 처리 완료: ${processedVendors.length}개 벤더 데이터`); + + return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_VENDOR_MASTER/'); + } + ).catch(error => { + return createErrorResponse(error); + }); +} + +// XML 데이터를 DB 삽입 가능한 형태로 변환 +function transformVendorData(vendorHeaderData: VendorHeaderXML[]): ProcessedVendorData[] { + if (!vendorHeaderData || !Array.isArray(vendorHeaderData)) { + return []; + } + + return vendorHeaderData.map(vendorHeader => { + const vndrcdKey = vendorHeader.VNDRCD || ''; + const fkData = { VNDRCD: vndrcdKey }; + + // 1단계: VENDOR_HEADER (루트) + const vendorHeaderConverted = convertXMLToDBData<VendorHeaderData>( + vendorHeader as Record<string, string | undefined>, + ['VNDRCD'], + fkData + ); + + // 2단계: ADDRESS와 직속 하위들 + const addresses = processNestedArray( + vendorHeader.ADDRESS, + (addr) => convertXMLToDBData<AddressData>(addr as Record<string, string | undefined>, ['ADR_NO'], fkData), + fkData + ); + + // ADDRESS의 하위 테이블들 (3단계) + const adEmails = vendorHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_EMAIL, (item) => + convertXMLToDBData<AdEmailData>(item as Record<string, string | undefined>, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData) + ) || []; + + const adFaxes = vendorHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_FAX, (item) => + convertXMLToDBData<AdFaxData>(item as Record<string, string | undefined>, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData) + ) || []; + + const adPostals = vendorHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_POSTAL, (item) => + convertXMLToDBData<AdPostalData>(item as Record<string, string | undefined>, ['INTL_ADR_VER_ID'], fkData), fkData) + ) || []; + + const adTels = vendorHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_TEL, (item) => + convertXMLToDBData<AdTelData>(item as Record<string, string | undefined>, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData) + ) || []; + + const adUrls = vendorHeader.ADDRESS?.flatMap(addr => + processNestedArray(addr.AD_URL, (item) => + convertXMLToDBData<AdUrlData>(item as Record<string, string | undefined>, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData) + ) || []; + + // 2단계: BP_TAXNUM + const bpTaxnums = processNestedArray( + vendorHeader.BP_TAXNUM, + (item) => convertXMLToDBData<BpTaxnumData>(item as Record<string, string | undefined>, ['TX_NO_CTG'], fkData), + fkData + ); + + // 2단계: BP_VENGEN과 하위들 + const bpVengens = processNestedArray( + vendorHeader.BP_VENGEN, + (vengen) => convertXMLToDBData<BpVengenData>(vengen as Record<string, string | undefined>, ['VNDRNO'], fkData), + fkData + ); + + // BP_VENGEN의 하위 테이블들 (3단계) + const bpCompnies = vendorHeader.BP_VENGEN?.flatMap(vengen => + processNestedArray(vengen.BP_COMPNY, (item) => + convertXMLToDBData<BpCompnyData>(item as Record<string, string | undefined>, ['CO_CD'], fkData), fkData) + ) || []; + + const bpPorgs = vendorHeader.BP_VENGEN?.flatMap(vengen => + processNestedArray(vengen.BP_PORG, (item) => + convertXMLToDBData<BpPorgData>(item as Record<string, string | undefined>, ['PUR_ORG_CD'], fkData), fkData) + ) || []; + + // BP_COMPNY의 하위 테이블 (4단계) + const bpWhtaxes = vendorHeader.BP_VENGEN?.flatMap(vengen => + vengen.BP_COMPNY?.flatMap(compny => + processNestedArray(compny.BP_WHTAX, (item) => + convertXMLToDBData<BpWhtaxData>(item as Record<string, string | undefined>, ['SRCE_TX_TP'], fkData), fkData) + ) || [] + ) || []; + + // BP_PORG의 하위 테이블 (4단계) + const zvpfns = vendorHeader.BP_VENGEN?.flatMap(vengen => + vengen.BP_PORG?.flatMap(porg => + processNestedArray(porg.ZVPFN, (item) => + convertXMLToDBData<ZvpfnData>(item as Record<string, string | undefined>, ['PTNR_SKL', 'PTNR_CNT'], fkData), fkData) + ) || [] + ) || []; + + return { + vendorHeader: vendorHeaderConverted, + addresses, + adEmails, + adFaxes, + adPostals, + adTels, + adUrls, + bpTaxnums, + bpVengens, + bpCompnies, + bpWhtaxes, + bpPorgs, + zvpfns + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedVendors: ProcessedVendorData[]) { + console.log(`데이터베이스 저장 시작: ${processedVendors.length}개 벤더 데이터`); + + try { + await db.transaction(async (tx) => { + for (const vendorData of processedVendors) { + const { vendorHeader, addresses, adEmails, adFaxes, adPostals, adTels, adUrls, + bpTaxnums, bpVengens, bpCompnies, bpWhtaxes, bpPorgs, zvpfns } = vendorData; + + if (!vendorHeader.VNDRCD) { + console.warn('벤더코드(VNDRCD)가 없는 항목 발견, 건너뜁니다.'); + continue; + } + + // 1. BP_HEADER 테이블 Upsert (최상위 테이블) + await tx.insert(VENDOR_MASTER_BP_HEADER) + .values(vendorHeader) + .onConflictDoUpdate({ + target: VENDOR_MASTER_BP_HEADER.VNDRCD, + set: { + ...vendorHeader, + updatedAt: new Date(), + } + }); + + // 2. 하위 테이블들 처리 - FK 기준으로 전체 삭제 후 재삽입 + await Promise.all([ + // 2단계 테이블들 + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS, addresses, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_TAXNUM, bpTaxnums, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN, bpVengens, 'VNDRCD', vendorHeader.VNDRCD), + + // 3-4단계 테이블들 + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, adEmails, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_FAX, adFaxes, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, adPostals, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL, adTels, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL, adUrls, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY, bpCompnies, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY_BP_WHTAX, bpWhtaxes, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG, bpPorgs, 'VNDRCD', vendorHeader.VNDRCD), + replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG_ZVPFN, zvpfns, 'VNDRCD', vendorHeader.VNDRCD), + ]); + } + }); + + console.log(`✅ 데이터베이스 저장 완료: ${processedVendors.length}개 벤더`); + return true; + } catch (error) { + console.error('❌ 데이터베이스 저장 중 오류 발생:', error); + throw error; + } +} diff --git a/app/api/(S-ERP)/(MDG)/utils.ts b/app/api/(S-ERP)/(MDG)/utils.ts new file mode 100644 index 00000000..bcb1dd45 --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/utils.ts @@ -0,0 +1,396 @@ +import { XMLParser } from "fast-xml-parser"; +import { readFileSync } from "fs"; +import { NextRequest, NextResponse } from "next/server"; +import { join } from "path"; +import { eq, desc } from "drizzle-orm"; +import db from "@/db/db"; +import { soapLogs, type LogDirection, type SoapLogInsert } from "@/db/schema/SOAP/soap"; + +// XML 파싱용 타입 유틸리티: 스키마에서 XML 타입 생성 +export type ToXMLFields<T> = { + [K in keyof T]?: T[K] extends string | null | undefined ? string : never; +}; + +// SOAP Body 데이터 타입 (범용) +export interface SoapBodyData { + [key: string]: unknown; +} + +// WSDL 파일 제공 함수 +export function serveWsdl(wsdlFileName: string) { + try { + const wsdlPath = join(process.cwd(), 'public', 'wsdl', wsdlFileName); + const wsdlContent = readFileSync(wsdlPath, 'utf-8'); + + return new NextResponse(wsdlContent, { + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + }, + }); + } catch (error) { + console.error('Failed to read WSDL file:', error); + return new NextResponse('WSDL file not found', { status: 404 }); + } +} + +// XML 파서 생성 (기본 설정) +export function createXMLParser(arrayTags: string[] = []) { + return new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + parseAttributeValue: false, + trimValues: true, + isArray: (name: string) => arrayTags.includes(name), + parseTagValue: false, + allowBooleanAttributes: true, + }); +} + +// SOAP Body나 루트에서 요청 데이터 추출 (범용) +export function extractRequestData( + parsedData: Record<string, unknown>, + requestKeyPattern: string +): SoapBodyData | null { + // SOAP 구조 체크 + const soapPaths = [ + ['soap:Envelope', 'soap:Body'], + ['SOAP:Envelope', 'SOAP:Body'], + ['Envelope', 'Body'], + ['soapenv:Envelope', 'soapenv:Body'] + ]; + + for (const [envelope, body] of soapPaths) { + if (parsedData?.[envelope]?.[body]) { + const result = extractFromSoapBody(parsedData[envelope][body] as SoapBodyData, requestKeyPattern); + if (result) return result; + } + } + + // 직접 요청 데이터 체크 + const requestKeys = [ + requestKeyPattern, + `tns:${requestKeyPattern}`, + `ns1:${requestKeyPattern}`, + `p0:${requestKeyPattern}` + ]; + + for (const key of requestKeys) { + if (parsedData?.[key]) { + return parsedData[key] as SoapBodyData; + } + } + + // 키 이름 패턴 검색 + for (const key of Object.keys(parsedData)) { + if (key.includes(requestKeyPattern)) { + return parsedData[key] as SoapBodyData; + } + } + + // 메인 데이터가 직접 있는 경우 (MATL 등) + if (parsedData?.MATL && Array.isArray(parsedData.MATL)) { + return parsedData as SoapBodyData; + } + + return null; +} + +function extractFromSoapBody(soapBody: SoapBodyData, requestKeyPattern: string): SoapBodyData | null { + const requestKeys = [ + requestKeyPattern.replace('Req', ''), + requestKeyPattern, + `tns:${requestKeyPattern}`, + `ns1:${requestKeyPattern}`, + `p0:${requestKeyPattern}` + ]; + + for (const key of requestKeys) { + if (soapBody?.[key]) { + return soapBody[key] as SoapBodyData; + } + } + + // 패턴 검색 + for (const key of Object.keys(soapBody)) { + if (key.includes(requestKeyPattern)) { + return soapBody[key] as SoapBodyData; + } + } + + // 메인 데이터가 직접 있는 경우 + if (soapBody.MATL && Array.isArray(soapBody.MATL)) { + return soapBody; + } + + return null; +} + +// 범용 XML → DB 변환 함수 +export function convertXMLToDBData<T extends Record<string, unknown>>( + xmlData: Record<string, string | undefined>, + requiredFields: (keyof T)[] = [], + fkData?: Record<string, string> +): T { + const result = {} as T; + + // XML 필드를 DB 필드로 변환 (string → string|null) + for (const key in xmlData) { + if (xmlData.hasOwnProperty(key)) { + const value = xmlData[key]; + (result as Record<string, unknown>)[key] = value || null; + } + } + + // 필수 필드 처리 (FK 등) + for (const field of requiredFields) { + if (!result[field] && fkData) { + const fieldStr = String(field); + if (fkData[fieldStr]) { + (result as Record<string, unknown>)[field] = fkData[fieldStr]; + } + } + } + + return result; +} + +// 중첩 배열 처리 함수 +export function processNestedArray<T, U>( + items: T[] | undefined, + converter: (item: T, fkData?: Record<string, string>) => U, + fkData?: Record<string, string> +): U[] { + if (!items || !Array.isArray(items)) { + return []; + } + + return items.map(item => converter(item, fkData)); +} + +// 에러 응답 생성 +export function createErrorResponse(error: unknown): NextResponse { + console.error('API Error:', error); + + const errorResponse = `<?xml version="1.0" encoding="UTF-8"?> +<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> + <soap:Body> + <soap:Fault> + <faultcode>soap:Server</faultcode> + <faultstring>${error instanceof Error ? ('[from eVCP]: ' + error.message) : 'Unknown error'}</faultstring> + </soap:Fault> + </soap:Body> +</soap:Envelope>`; + + return new NextResponse(errorResponse, { + status: 500, + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + }, + }); +} + +// 성공 응답 생성 +export function createSuccessResponse(namespace: string): NextResponse { + const xmlResponse = `<?xml version="1.0" encoding="UTF-8"?> +<soap:Envelope + xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:tns="${namespace}"> + <soap:Body> + </soap:Body> +</soap:Envelope>`; + + return new NextResponse(xmlResponse, { + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + }, + }); +} + +// 하위 테이블 처리: FK 기준으로 전체 삭제 후 재삽입 +export async function replaceSubTableData<T>( + tx: any, + table: any, + data: T[], + parentField: string, + parentValue: string +) { + // 1. 기존 데이터 전체 삭제 (FK 기준) - eq() 함수 사용 + await tx.delete(table).where(eq(table[parentField], parentValue)); + + // 2. 새 데이터 삽입 + if (data.length > 0) { + await tx.insert(table).values(data); + } +} + +// ======================================== +// SOAP 로그 관련 공통 함수들 +// ======================================== + +/** + * SOAP 요청 로그를 시작하고 로그 ID를 반환 + * @param direction 수신/송신 구분 ('INBOUND' | 'OUTBOUND') + * @param system 시스템명 (예: 'S-ERP', 'MDG') + * @param interfaceName 인터페이스명 (예: 'IF_MDZ_EVCP_CUSTOMER_MASTER') + * @param requestData 요청 XML 데이터 + * @returns 생성된 로그 ID + */ +export async function startSoapLog( + direction: LogDirection, + system: string, + interfaceName: string, + requestData: string +): Promise<number> { + try { + const logData: SoapLogInsert = { + direction, + system, + interface: interfaceName, + startedAt: new Date(), + endedAt: null, + isSuccess: false, + requestData, + responseData: null, + errorMessage: null, + }; + + const [result] = await db.insert(soapLogs).values(logData).returning({ id: soapLogs.id }); + + console.log(`📝 SOAP 로그 시작 [${direction}] ${system}/${interfaceName} - ID: ${result.id}`); + return result.id; + } catch (error) { + console.error('SOAP 로그 시작 실패:', error); + throw error; + } +} + +/** + * SOAP 요청 로그를 완료 처리 + * @param logId 로그 ID + * @param isSuccess 성공 여부 + * @param responseData 응답 XML 데이터 (선택사항) + * @param errorMessage 에러 메시지 (실패시) + */ +export async function completeSoapLog( + logId: number, + isSuccess: boolean, + responseData?: string, + errorMessage?: string +): Promise<void> { + try { + await db.update(soapLogs) + .set({ + endedAt: new Date(), + isSuccess, + responseData: responseData || null, + errorMessage: errorMessage || null, + }) + .where(eq(soapLogs.id, logId)); + + console.log(`✅ SOAP 로그 완료 - ID: ${logId}, 성공: ${isSuccess}`); + } catch (error) { + console.error('SOAP 로그 완료 처리 실패:', error); + throw error; + } +} + +/** + * 환경변수 기반으로 오래된 SOAP 로그 정리 + * SOAP_LOG_MAX_RECORDS 환경변수를 확인하여 최대 개수 초과시 오래된 로그 삭제 + */ +export async function cleanupOldSoapLogs(): Promise<void> { + try { + const maxRecords = parseInt(process.env.SOAP_LOG_MAX_RECORDS || '0'); + + if (maxRecords <= 0) { + console.log('🔄 SOAP 로그 정리: 무제한 저장 설정 (SOAP_LOG_MAX_RECORDS = 0)'); + return; + } + + // 현재 총 로그 개수 확인 + const totalLogs = await db.select({ count: soapLogs.id }).from(soapLogs); + const currentCount = totalLogs.length; + + if (currentCount <= maxRecords) { + console.log(`🔄 SOAP 로그 정리: 현재 ${currentCount}개, 최대 ${maxRecords}개 - 정리 불필요`); + return; + } + + // 삭제할 개수 계산 + const deleteCount = currentCount - maxRecords; + + // 가장 오래된 로그들 조회 (ID 기준) + const oldestLogs = await db.select({ id: soapLogs.id }) + .from(soapLogs) + .orderBy(soapLogs.id) + .limit(deleteCount); + + if (oldestLogs.length === 0) { + console.log('🔄 SOAP 로그 정리: 삭제할 로그 없음'); + return; + } + + // 오래된 로그들 삭제 + const oldestIds = oldestLogs.map(log => log.id); + + // 배치 삭제 (IN 절 사용) + for (const logId of oldestIds) { + await db.delete(soapLogs).where(eq(soapLogs.id, logId)); + } + + console.log(`🗑️ SOAP 로그 정리 완료: ${deleteCount}개 삭제 (${currentCount} → ${maxRecords})`); + } catch (error) { + console.error('SOAP 로그 정리 실패:', error); + throw error; + } +} + +/** + * SOAP 로그 관련 래퍼 함수: 로그 시작부터 완료까지 자동 처리 + * @param direction 수신/송신 구분 + * @param system 시스템명 + * @param interfaceName 인터페이스명 + * @param requestData 요청 데이터 + * @param processor 실제 비즈니스 로직 함수 + * @returns 처리 결과 + */ +export async function withSoapLogging<T>( + direction: LogDirection, + system: string, + interfaceName: string, + requestData: string, + processor: () => Promise<T> +): Promise<T> { + let logId: number | null = null; + + try { + // 1. 로그 시작 + logId = await startSoapLog(direction, system, interfaceName, requestData); + + // 2. 실제 처리 실행 + const result = await processor(); + + // 3. 성공 로그 완료 + await completeSoapLog(logId, true); + + // 4. 로그 정리 (백그라운드) + cleanupOldSoapLogs().catch(error => + console.error('백그라운드 로그 정리 실패:', error) + ); + + return result; + + } catch (error) { + // 5. 실패 로그 완료 + if (logId !== null) { + await completeSoapLog( + logId, + false, + undefined, + error instanceof Error ? error.message : 'Unknown error' + ); + } + + throw error; + } +}
\ No newline at end of file diff --git a/app/api/auth/[...nextauth]/saml/provider.ts b/app/api/auth/[...nextauth]/saml/provider.ts index 8486a690..dfe3d830 100644 --- a/app/api/auth/[...nextauth]/saml/provider.ts +++ b/app/api/auth/[...nextauth]/saml/provider.ts @@ -228,6 +228,7 @@ export async function authenticateSAMLUser(userData: SAMLUser) { } } +// TODO: SecuritySetting 함수에서 가져올 것 // NextAuth JWT 토큰 생성 헬퍼 export async function createNextAuthToken(user: User): Promise<string> { const token = { @@ -239,7 +240,7 @@ export async function createNextAuthToken(user: User): Promise<string> { domain: user.domain, imageUrl: user.imageUrl, iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30일 + exp: Math.floor(Date.now() / 1000) + (480 * 60) // 480분 }; const secret = process.env.NEXTAUTH_SECRET!; @@ -256,4 +257,3 @@ export function getSessionCookieName(): string { ? '__Secure-next-auth.session-token' : 'next-auth.session-token'; } -
\ No newline at end of file |
