summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts344
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts235
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts346
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts182
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts271
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts274
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts150
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts1141
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts435
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts153
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts336
-rw-r--r--app/api/(S-ERP)/(MDG)/utils.ts396
-rw-r--r--app/api/auth/[...nextauth]/saml/provider.ts4
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