From deb2d31dba913a3b831523f41b9bf2e286c53af1 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 2 Jul 2025 10:00:07 +0000 Subject: (김준회) MDG 수신 구조 개선 및 MDG SOAP 송신 액션 & 테스트 페이지 구성 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/admin/mdg/page.tsx | 248 +++++++++ .../(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts | 2 +- .../(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts | 2 +- .../(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts | 2 +- .../IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts | 2 +- .../(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts | 2 +- .../IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts | 2 +- .../route.ts | 2 +- .../(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts | 2 +- .../(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts | 2 +- .../(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts | 2 +- .../(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts | 2 +- app/api/(S-ERP)/(MDG)/utils.ts | 450 ---------------- lib/soap/mdg/send/vendor-master/action.ts | 584 +++++++++++++++++++++ lib/soap/mdg/utils.ts | 450 ++++++++++++++++ public/wsdl/P2MD3007_AO.csv | 122 +++++ 16 files changed, 1415 insertions(+), 461 deletions(-) create mode 100644 app/[lng]/admin/mdg/page.tsx delete mode 100644 app/api/(S-ERP)/(MDG)/utils.ts create mode 100644 lib/soap/mdg/send/vendor-master/action.ts create mode 100644 lib/soap/mdg/utils.ts create mode 100644 public/wsdl/P2MD3007_AO.csv diff --git a/app/[lng]/admin/mdg/page.tsx b/app/[lng]/admin/mdg/page.tsx new file mode 100644 index 00000000..27416f25 --- /dev/null +++ b/app/[lng]/admin/mdg/page.tsx @@ -0,0 +1,248 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { toast } from 'sonner' +import { Loader2, Send, RefreshCw } from 'lucide-react' + +// CSV 필드를 정의할 타입 +interface VendorFieldDef { + table: string; + name: string; + mandatory: boolean; + description: string; +} + +// CSV 파싱 함수 (간단 파서) +const parseCSV = (csv: string): VendorFieldDef[] => { + const lines = csv.trim().split('\n'); + // 첫 번째 라인은 헤더이므로 제거 + return lines.slice(1).map((line) => { + const parts = line.split(','); + const table = parts[1]?.trim(); + const name = parts[2]?.trim(); + const mandatory = parts[3]?.trim() === 'M'; + const description = parts.slice(6).join(',').trim(); + return { table, name, mandatory, description } as VendorFieldDef; + }); +}; + +// 기존 샘플 기본값 (필요 시 확장) +const sampleDefaults: Record = { + BP_HEADER: 'TEST001', + ZZSRMCD: 'EVCP', + TITLE: 'TEST', + BU_SORT1: 'TEST VENDOR', + NAME_ORG1: '테스트 벤더 회사', + KTOKK: 'Z001', + VEN_KFBUS: '제조업', + VEN_KFIND: 'IT', + MASTERFLAG: 'X', + IBND_TYPE: 'U', + ZZREQID: 'TESTUSER01', + ADDRNO: '0001', + AD_NATION: '1', + COUNTRY: 'KR', + LANGU_COM: 'K', + POST_COD1: '06292', + CITY1: '서울시', + DISTRICT: '강남구', + REGION: '11', + MC_STREET: '테헤란로 123', + T_COUNTRY: 'KR', + T_NUMBER: '02-1234-5678', + F_COUNTRY: 'KR', + F_NUMBER: '02-1234-5679', + U_ADDRESS: 'https://test.vendor.com', + E_ADDRESS: 'contact@test.vendor.com', + BP_TX_TYP: 'KR2', + TAXNUM: '123-45-67890', +}; + +// XML escape helper +const escapeXml = (unsafe: string) => unsafe.replace(/[<>&'"']/g, (c) => { + switch (c) { + case '<': return '<'; + case '>': return '>'; + case '&': return '&'; + case '"': return '"'; + case "'": return '''; + default: return c; + } +}); + +export default function MDGTestPage() { + const [formData, setFormData] = useState>({}); + const [fieldDefs, setFieldDefs] = useState([]); + const [resultXml, setResultXml] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + // CSV 로딩 및 초기 데이터 셋업 + useEffect(() => { + const load = async () => { + const res = await fetch('/wsdl/P2MD3007_AO.csv'); + const csvText = await res.text(); + const defs = parseCSV(csvText); + setFieldDefs(defs); + + const init: Record = {}; + defs.forEach((d) => { + init[d.name] = sampleDefaults[d.name] ?? ''; + }); + setFormData(init); + }; + + load(); + }, []); + + // 폼 데이터 업데이트 + const updateField = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + // 기본값으로 리셋 + const resetForm = () => { + const reset: Record = {}; + fieldDefs.forEach((d) => { + reset[d.name] = sampleDefaults[d.name] ?? ''; + }); + setFormData(reset); + toast.success('폼이 기본값으로 리셋되었습니다.'); + }; + + // 테스트 송신 실행 + const handleTestSend = async () => { + try { + setIsLoading(true); + + // 필수 필드 검증 + const requiredFields = fieldDefs.filter(d => d.mandatory).map(d => d.name); + const missingFields = requiredFields.filter(field => !formData[field]?.trim()); + + if (missingFields.length > 0) { + toast.error(`필수 필드가 누락되었습니다: ${missingFields.join(', ')}`); + return; + } + + // XML 생성 + const bodyContent = fieldDefs.map(f => { + const val = formData[f.name] ?? ''; + return `<${f.name}>${escapeXml(val)}`; + }).join('\n '); + + const supplierXml = `\n ${bodyContent}\n `; + + const envelope = `\n\n \n \n \n \n ${supplierXml}\n \n \n \n`; + + setResultXml(envelope); + toast.success('요청 XML이 생성되었습니다. 하단 영역을 확인하세요.'); + + } catch (error) { + console.error('테스트 송신 실패:', error); + toast.error('테스트 송신 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

MDG VENDOR 마스터 테스트

+

+ VENDOR 마스터 데이터를 MDG 시스템으로 테스트 송신합니다 +

+
+
+ + +
+
+ + {/* 동적 필드 렌더링 */} + {fieldDefs.length === 0 ? ( +

CSV 로딩 중...

+ ) : ( +
+ {Object.entries( + fieldDefs.reduce((acc: Record, cur) => { + acc[cur.table] = acc[cur.table] ? [...acc[cur.table], cur] : [cur]; + return acc; + }, {}) + ).map(([table, fields]) => ( + + + + {table} + {fields.some(f => f.mandatory) && ( + 필수 포함 + )} + + {table} 테이블 입력 + + + {fields.filter((f, idx, arr) => arr.findIndex(x => x.name === f.name) === idx).map((field) => ( +
+ + updateField(field.name, e.target.value)} + /> + {field.description && ( +

{field.description}

+ )} +
+ ))} +
+
+ ))} +
+ )} + + {/* 송신 결과 영역 */} + + + 송신 결과 + + MDG 시스템으로의 송신 결과가 여기에 표시됩니다 + + + + {resultXml ? ( +
+              {resultXml}
+            
+ ) : ( +
+

+ 테스트 송신 버튼을 클릭하면 결과가 표시됩니다. +

+
+ )} +
+
+
+ ); +} + 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 index fd4afb86..9d08527b 100644 --- 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 @@ -27,7 +27,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 type BpHeaderData = typeof CUSTOMER_MASTER_BP_HEADER.$inferInsert; 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 index ffb39895..28757fb5 100644 --- 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 @@ -18,7 +18,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 type DeptData = typeof DEPARTMENT_CODE_CMCTB_DEPT_MDG.$inferInsert; 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 index b9775765..fc6bc71f 100644 --- 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 @@ -41,7 +41,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 type EmpMdgData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG.$inferInsert; 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 index 3c58e3a1..22f151b3 100644 --- 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 @@ -16,7 +16,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 type EmpRefData = typeof EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF.$inferInsert; 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 index 97c2e636..cd1005e7 100644 --- 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 @@ -20,7 +20,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 (Insert와 XML을 통합) type MatlData = typeof EQUP_MASTER_MATL.$inferInsert; 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 index 19836c36..21063ff7 100644 --- 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 @@ -21,7 +21,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 (Insert와 XML을 통합) type MatlData = typeof MATERIAL_MASTER_PART_MATL.$inferInsert; 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 index ecbc23bc..428cd298 100644 --- 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 @@ -10,7 +10,7 @@ import { createSuccessResponse, ToXMLFields, withSoapLogging, -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 type CMCTBMatBseData = typeof MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE.$inferInsert; 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 4e7cdf35..204dffa3 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 @@ -19,7 +19,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 (Insert와 XML을 통합) type MatlData = typeof MODEL_MASTER_MATL.$inferInsert; 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 index 886e4851..987d4002 100644 --- 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 @@ -30,7 +30,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 type CctrData = typeof ORGANIZATION_MASTER_HRHMTB_CCTR.$inferInsert; 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 index 167c5c5d..93071c69 100644 --- 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 @@ -13,7 +13,7 @@ import { createErrorResponse, createSuccessResponse, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 type ProjectData = typeof PROJECT_MASTER_CMCTB_PROJ_MAST.$inferInsert; 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 index d59246c2..75f8cd62 100644 --- 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 @@ -26,7 +26,7 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "../utils"; +} from "@/lib/soap/mdg/utils"; // 스키마에서 직접 타입 추론 type VendorHeaderData = typeof VENDOR_MASTER_BP_HEADER.$inferInsert; diff --git a/app/api/(S-ERP)/(MDG)/utils.ts b/app/api/(S-ERP)/(MDG)/utils.ts deleted file mode 100644 index 437988dc..00000000 --- a/app/api/(S-ERP)/(MDG)/utils.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { XMLParser } from "fast-xml-parser"; -import { readFileSync } from "fs"; -import { NextResponse } from "next/server"; -import { join } from "path"; -import { eq } from "drizzle-orm"; -import db from "@/db/db"; -import { soapLogs, type LogDirection, type SoapLogInsert } from "@/db/schema/SOAP/soap"; - -// XML 파싱용 타입 유틸리티: 스키마에서 XML 타입 생성 -export type ToXMLFields = { - [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 { - // public/wsdl 에서 WSDL 제공함을 가정 - // 이게 WSDL 구현 표준인데, 보안 감사에서 반대한다면 제거 - 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 파서 생성 -// SAP XI 가 자동생성해 보내는 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, - 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) { - const envelopeData = parsedData?.[envelope] as Record | undefined; - if (envelopeData?.[body]) { - const result = extractFromSoapBody(envelopeData[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 변환 함수 -/** - * XML 데이터를 DB 삽입 가능한 형태로 변환 - * - * 아키텍처 설계: - * - 하위 테이블들은 별도의 필수 필드가 없다고 가정 (스키마에서 notNull() 제거 예정) - * - FK는 항상 최상위 테이블의 unique 필드를 참조 - * - 송신된 XML은 항상 전체 데이터셋을 포함 - * - 최상위 테이블의 unique 필드가 충돌하면 전체 삭제 후 재삽입 처리 - * - * FK 처리 방식: - * - XML에 FK 필드가 이미 포함된 경우: XML 값 우선 사용 (예: MATL 인터페이스) - * - XML에 FK 필드가 없는 경우: 상위에서 전달받은 FK 값 사용 (예: VENDOR 인터페이스) - * - 이를 통해 다양한 SAP 인터페이스 패턴에 대응 - * - * @param xmlData XML에서 파싱된 데이터 - * @param fkData 상위 테이블에서 전달받은 FK 데이터 - * @returns DB 삽입 가능한 형태로 변환된 데이터 - */ -export function convertXMLToDBData>( - xmlData: Record, - fkData?: Record -): 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)[key] = value || null; - } - } - - // FK 필드 처리 (XML 우선, 없으면 상위에서 전달받은 값 사용) - if (fkData) { - for (const [key, value] of Object.entries(fkData)) { - // XML에 해당 FK 필드가 없거나 비어있는 경우에만 상위 값 사용 - const existingValue = (result as Record)[key]; - if (!existingValue || existingValue === null || existingValue === '') { - (result as Record)[key] = value; - } - // XML에 이미 FK 필드가 있고 값이 있는 경우는 XML 값을 그대로 사용 - } - } - - return result; -} - -// 중첩 배열 처리 함수 (개선된 버전) -/** - * 중첩된 배열 데이터를 처리하여 DB 삽입 가능한 형태로 변환 - * - * 처리 방식: - * - 하위 테이블 데이터는 FK만 설정하면 됨 - * - 별도의 필수 필드 생성 로직 불필요 - * - 전체 데이터셋 기반으로 삭제 후 재삽입 처리 - * - * @param items 처리할 배열 데이터 - * @param converter 변환 함수 - * @param fkData FK 데이터 - * @returns 변환된 배열 데이터 - */ -export function processNestedArray( - items: T[] | undefined, - converter: (item: T, fkData?: Record) => U, - fkData?: Record -): 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 = ` - - - - soap:Server - ${error instanceof Error ? ('[from eVCP]: ' + error.message) : 'Unknown error'} - - -`; - - return new NextResponse(errorResponse, { - status: 500, - headers: { - 'Content-Type': 'text/xml; charset=utf-8', - }, - }); -} - -// 성공 응답 생성 -export function createSuccessResponse(namespace: string): NextResponse { - const xmlResponse = ` - - - -`; - - return new NextResponse(xmlResponse, { - headers: { - 'Content-Type': 'text/xml; charset=utf-8', - }, - }); -} - -// 하위 테이블 처리: FK 기준으로 전체 삭제 후 재삽입 -/** - * 하위 테이블 데이터를 전체 삭제 후 재삽입하는 함수 - * - * 처리 전략: - * - 송신 XML이 전체 데이터셋을 포함한다는 가정하에 설계 - * - 부분 업데이트보다 전체 교체를 통해 데이터 일관성 확보 - * - FK 기준으로 해당 부모 레코드의 모든 하위 데이터 교체 - * - * 처리 순서: - * 1. FK 기준으로 기존 데이터 전체 삭제 - * 2. 새로운 데이터 전체 삽입 - * - * @param tx 트랜잭션 객체 - * @param table 대상 테이블 스키마 - * @param data 삽입할 데이터 배열 - * @param parentField FK 필드명 (일반적으로 'VNDRCD') - * @param parentValue FK 값 (상위 테이블의 unique 필드 값) - */ -export async function replaceSubTableData>( - tx: Parameters[0]>[0], - table: any, // Drizzle 테이블 객체 - 복잡한 제네릭 타입으로 인해 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 { - 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 { - 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 { - 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( - direction: LogDirection, - system: string, - interfaceName: string, - requestData: string, - processor: () => Promise -): Promise { - 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/lib/soap/mdg/send/vendor-master/action.ts b/lib/soap/mdg/send/vendor-master/action.ts new file mode 100644 index 00000000..34ce242c --- /dev/null +++ b/lib/soap/mdg/send/vendor-master/action.ts @@ -0,0 +1,584 @@ +'use 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 { eq, sql, desc } from "drizzle-orm"; +import { withSoapLogging } from "../../utils"; +import fs from 'fs'; +import path from 'path'; + +// WSDL 엔드포인트 URL (WSDL에서 추출) +const MDG_ENDPOINT_URL = "http://shii8dvddb01.hec.serp.shi.samsung.net:50000/sap/xi/engine?type=entry&version=3.0&Sender.Service=P2038_D&Interface=http%3A%2F%2Fshi.samsung.co.kr%2FP2_MD%2FMDZ%5EP2MD3007_AO&QualityOfService=ExactlyOnce"; + +// CSV 파싱 및 필드 정의 ------------------------------------------------ +interface CsvField { + table: string; + field: string; + mandatory: boolean; +} + +function parseCsv(content: string): CsvField[] { + const lines = content.trim().split('\n'); + return lines.slice(1).map(line => { + const parts = line.split(','); + return { + table: parts[1]?.trim(), + field: parts[2]?.trim(), + mandatory: parts[3]?.trim() === 'M' + } as CsvField; + }); +} + +// 모듈 초기화 시 CSV 로드 +const CSV_PATH = path.join(process.cwd(), 'public', 'wsdl', 'P2MD3007_AO.csv'); +let CSV_FIELDS: CsvField[] = []; +try { + const csvRaw = fs.readFileSync(CSV_PATH, 'utf-8'); + CSV_FIELDS = parseCsv(csvRaw); +} catch (e) { + console.error('CSV 로딩 실패:', e); +} + +// XML escape helper +const escapeXml = (unsafe: string) => unsafe.replace(/[<>&'"']/g, (c) => { + switch (c) { + case '<': return '<'; + case '>': return '>'; + case '&': return '&'; + case '"': return '"'; + case "'": return '''; + default: return c; + } +}); + +// VENDOR 마스터 데이터를 MDG로 송신하는 액션 +export async function sendVendorMasterToMDG(vendorCodes: string[]): Promise<{ + success: boolean; + message: string; + results?: Array<{ vendorCode: string; success: boolean; error?: string }>; +}> { + try { + console.log(`🚀 VENDOR_MASTER 송신 시작: ${vendorCodes.length}개 벤더`); + + const results: Array<{ vendorCode: string; success: boolean; error?: string }> = []; + + // 각 VENDOR 코드별로 개별 전송 (MDG 시스템의 처리 제한 고려) + for (const vendorCode of vendorCodes) { + try { + console.log(`📤 VENDOR ${vendorCode} 데이터 조회 중...`); + + // 데이터베이스에서 VENDOR 데이터 조회 + const vendorData = await fetchVendorData(vendorCode); + + if (!vendorData) { + results.push({ + vendorCode, + success: false, + error: 'VENDOR 데이터를 찾을 수 없습니다.' + }); + continue; + } + + // XML 생성 + const soapXml = buildSoapXML(vendorData); + console.log(`📄 VENDOR ${vendorCode} XML 생성 완료`); + + // SOAP 요청 전송 + await withSoapLogging( + 'OUTBOUND', + 'MDG', + 'IF_MDZ_EVCP_VENDOR_MASTER', + soapXml, + async () => { + const response = await fetch(MDG_ENDPOINT_URL, { + method: 'POST', + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + 'SOAPAction': 'http://sap.com/xi/WebService/soap1.1', + }, + body: soapXml, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const responseText = await response.text(); + console.log(`✅ VENDOR ${vendorCode} MDG 전송 성공`); + + // SOAP Fault 체크 + if (responseText.includes('soap:Fault') || responseText.includes('SOAP:Fault')) { + throw new Error(`MDG SOAP Fault: ${responseText}`); + } + + return responseText; + } + ); + + results.push({ + vendorCode, + success: true + }); + + } catch (error) { + console.error(`❌ VENDOR ${vendorCode} 전송 실패:`, error); + results.push({ + vendorCode, + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }); + } + } + + const successCount = results.filter(r => r.success).length; + const failCount = results.length - successCount; + + console.log(`🎉 VENDOR_MASTER 송신 완료: 성공 ${successCount}개, 실패 ${failCount}개`); + + return { + success: failCount === 0, + message: `전송 완료: 성공 ${successCount}개, 실패 ${failCount}개`, + results + }; + + } catch (error) { + console.error('❌ VENDOR_MASTER 송신 중 전체 오류 발생:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +// 데이터베이스에서 VENDOR 데이터 조회 +async function fetchVendorData(vendorCode: string) { + try { + // 1. 헤더 데이터 조회 + const [vendorHeader] = await db + .select() + .from(VENDOR_MASTER_BP_HEADER) + .where(eq(VENDOR_MASTER_BP_HEADER.VNDRCD, vendorCode)) + .limit(1); + + if (!vendorHeader) { + return null; + } + + // 2. 관련 데이터 병렬 조회 + const [ + addresses, + adEmails, + adFaxes, + adPostals, + adTels, + adUrls, + bpTaxnums, + bpVengens, + bpCompnies, + bpWhtaxes, + bpPorgs, + zvpfns + ] = await Promise.all([ + db.select().from(VENDOR_MASTER_BP_HEADER_ADDRESS).where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL).where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_FAX).where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_FAX.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL).where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL).where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL).where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_BP_TAXNUM).where(eq(VENDOR_MASTER_BP_HEADER_BP_TAXNUM.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_BP_VENGEN).where(eq(VENDOR_MASTER_BP_HEADER_BP_VENGEN.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY).where(eq(VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY_BP_WHTAX).where(eq(VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY_BP_WHTAX.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG).where(eq(VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG.VNDRCD, vendorCode)), + db.select().from(VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG_ZVPFN).where(eq(VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG_ZVPFN.VNDRCD, vendorCode)) + ]); + + return { + vendorHeader, + addresses, + adEmails, + adFaxes, + adPostals, + adTels, + adUrls, + bpTaxnums, + bpVengens, + bpCompnies, + bpWhtaxes, + bpPorgs, + zvpfns + }; + + } catch (error) { + console.error(`VENDOR ${vendorCode} 데이터 조회 실패:`, error); + throw error; + } +} + +// SOAP XML 생성 (WSDL 구조에 맞춤) +function buildSoapXML(vendorData: NonNullable>>): string { + const { vendorHeader, addresses, adEmails, adFaxes, adPostals, adTels, adUrls, bpTaxnums, bpVengens } = vendorData; + + // 값 추출 매핑 ------------------------------------ + const mapping: Record = { + // Header + BP_HEADER: vendorHeader?.VNDRCD, + ZZSRMCD: 'EVCP', + TITLE: vendorHeader?.TITLE ?? '', + BU_SORT1: adPostals[0]?.VNDRNM_ABRV_1, + NAME_ORG1: adPostals[0]?.VNDRNM_1, + KTOKK: bpVengens[0]?.ACNT_GRP, + MASTERFLAG: 'X', + IBND_TYPE: 'U', + // Address mandatory (first) + ADDRNO: addresses[0]?.ADDRNO, + AD_NATION: adPostals[0]?.INTL_ADR_VER_ID, + COUNTRY: adPostals[0]?.NTN_CD, + LANGU_COM: adPostals[0]?.LANG_KEY, + POST_COD1: adPostals[0]?.CITY_ZIP_NO, + CITY1: adPostals[0]?.VNDRNM_1, + MC_STREET: adPostals[0]?.ADR_1, + // Phone/Fax mandatory fields + AD_CONSNO: '001', + T_COUNTRY: adTels[0]?.CTRY_CD ?? 'KR', + F_COUNTRY: adFaxes[0]?.CTRY_CD ?? 'KR', + // Tax + BP_TX_TYP: bpTaxnums[0]?.TX_NO_CTG ?? 'KR2', + TAXNUM: bpVengens[0]?.VAT_REG_NO, + // Default others can be added as needed + }; + + // 필드 순서에 따라 XML 생성 + const seen = new Set(); + const uniqueFields = CSV_FIELDS.filter(f => { + if (seen.has(f.field)) return false; + seen.add(f.field); + return true; + }); + + const fieldXml = uniqueFields.map(f => { + const val = mapping[f.field] ?? ''; + return `<${f.field}>${escapeXml(val ?? '')}`; + }).join('\n '); + + const supplierMasterXml = `\n ${fieldXml}\n `; + + const soapEnvelope = `\n\n \n \n \n \n ${supplierMasterXml}\n \n \n \n`; + + return soapEnvelope.trim(); +} + +// 특정 VENDOR만 송신하는 유틸리티 함수 +export async function sendSingleVendorToMDG(vendorCode: string) { + return await sendVendorMasterToMDG([vendorCode]); +} + +// 모든 VENDOR 송신하는 유틸리티 함수 (주의: 대량 데이터 처리) +export async function sendAllVendorsToMDG() { + try { + // 모든 VENDOR 코드 조회 + const vendors = await db + .select({ VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD }) + .from(VENDOR_MASTER_BP_HEADER); + + const vendorCodes = vendors.map(v => v.VNDRCD); + + if (vendorCodes.length === 0) { + return { + success: false, + message: '송신할 VENDOR 데이터가 없습니다.' + }; + } + + console.log(`⚠️ 전체 VENDOR 송신 요청: ${vendorCodes.length}개`); + + // 배치 처리 (10개씩 분할하여 처리) + const batchSize = 10; + const results: Array<{ vendorCode: string; success: boolean; error?: string }> = []; + + for (let i = 0; i < vendorCodes.length; i += batchSize) { + const batch = vendorCodes.slice(i, i + batchSize); + console.log(`📦 배치 ${Math.floor(i / batchSize) + 1} 처리 중... (${batch.length}개)`); + + const batchResult = await sendVendorMasterToMDG(batch); + if (batchResult.results) { + results.push(...batchResult.results); + } + + // 배치 간 잠깐 대기 (서버 부하 방지) + if (i + batchSize < vendorCodes.length) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + const successCount = results.filter(r => r.success).length; + const failCount = results.length - successCount; + + return { + success: failCount === 0, + message: `전체 송신 완료: 성공 ${successCount}개, 실패 ${failCount}개`, + results + }; + + } catch (error) { + console.error('전체 VENDOR 송신 중 오류:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +// 수정된 VENDOR만 송신하는 액션 (updatedAt != createdAt 조건) +export async function sendModifiedVendorsToMDG(): Promise<{ + success: boolean; + message: string; + results?: Array<{ vendorCode: string; success: boolean; error?: string }>; +}> { + try { + console.log('🔍 수정된 VENDOR 데이터 조회 중...'); + + // updatedAt과 createdAt이 다른 VENDOR 조회 (수정된 것들) + const modifiedVendors = await db + .select({ + VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD, + createdAt: VENDOR_MASTER_BP_HEADER.createdAt, + updatedAt: VENDOR_MASTER_BP_HEADER.updatedAt + }) + .from(VENDOR_MASTER_BP_HEADER) + .where( + // PostgreSQL에서 timestamp 비교 (밀리초 차이 고려) + sql`EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.updatedAt}) - EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.createdAt}) > 1` + ); + + const vendorCodes = modifiedVendors.map(v => v.VNDRCD); + + if (vendorCodes.length === 0) { + console.log('📝 수정된 VENDOR 데이터가 없습니다.'); + return { + success: true, + message: '수정된 VENDOR 데이터가 없습니다.' + }; + } + + console.log(`📋 수정된 VENDOR ${vendorCodes.length}개 발견:`, vendorCodes); + + // 수정된 VENDOR들의 수정 시간 로그 + modifiedVendors.forEach(vendor => { + console.log(` - ${vendor.VNDRCD}: 생성 ${vendor.createdAt?.toISOString()}, 수정 ${vendor.updatedAt?.toISOString()}`); + }); + + // 배치 처리로 송신 + const batchSize = 10; + const results: Array<{ vendorCode: string; success: boolean; error?: string }> = []; + + for (let i = 0; i < vendorCodes.length; i += batchSize) { + const batch = vendorCodes.slice(i, i + batchSize); + console.log(`📦 수정 데이터 배치 ${Math.floor(i / batchSize) + 1} 처리 중... (${batch.length}개)`); + + const batchResult = await sendVendorMasterToMDG(batch); + if (batchResult.results) { + results.push(...batchResult.results); + } + + // 배치 간 대기 + if (i + batchSize < vendorCodes.length) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + const successCount = results.filter(r => r.success).length; + const failCount = results.length - successCount; + + console.log(`🎯 수정된 VENDOR 송신 완료: 성공 ${successCount}개, 실패 ${failCount}개`); + + return { + success: failCount === 0, + message: `수정된 VENDOR 송신 완료: 성공 ${successCount}개, 실패 ${failCount}개`, + results + }; + + } catch (error) { + console.error('❌ 수정된 VENDOR 송신 중 오류:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +// 테스트용 N건 송신 액션 +export async function sendNVendorsToMDG( + count: number, + startFrom: number = 0 +): Promise<{ + success: boolean; + message: string; + results?: Array<{ vendorCode: string; success: boolean; error?: string }>; +}> { + try { + if (count <= 0) { + return { + success: false, + message: '송신할 건수는 1 이상이어야 합니다.' + }; + } + + console.log(`🧪 테스트용 VENDOR 송신: ${count}건 (${startFrom}번째부터)`); + + // N건의 VENDOR 코드 조회 + const vendors = await db + .select({ VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD }) + .from(VENDOR_MASTER_BP_HEADER) + .limit(count) + .offset(startFrom); + + const vendorCodes = vendors.map(v => v.VNDRCD); + + if (vendorCodes.length === 0) { + return { + success: false, + message: `${startFrom}번째부터 ${count}건의 VENDOR 데이터가 없습니다.` + }; + } + + console.log(`📋 테스트 대상 VENDOR ${vendorCodes.length}개:`, vendorCodes); + + // 송신 실행 + const result = await sendVendorMasterToMDG(vendorCodes); + + console.log(`🧪 테스트 송신 완료: ${vendorCodes.length}개 처리`); + + return { + ...result, + message: `테스트 송신 완료 (${vendorCodes.length}개): ${result.message}` + }; + + } catch (error) { + console.error('❌ 테스트 송신 중 오류:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +// 최신 수정된 N건 송신 액션 (최근 수정 순) +export async function sendRecentModifiedVendorsToMDG( + count: number = 5 +): Promise<{ + success: boolean; + message: string; + results?: Array<{ vendorCode: string; success: boolean; error?: string }>; +}> { + try { + console.log(`🕒 최근 수정된 VENDOR ${count}건 조회 중...`); + + // 최근 수정된 순으로 N건 조회 + const recentVendors = await db + .select({ + VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD, + updatedAt: VENDOR_MASTER_BP_HEADER.updatedAt + }) + .from(VENDOR_MASTER_BP_HEADER) + .where( + // 수정된 항목만 (updatedAt != createdAt) + sql`EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.updatedAt}) - EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.createdAt}) > 1` + ) + .orderBy(desc(VENDOR_MASTER_BP_HEADER.updatedAt)) + .limit(count); + + const vendorCodes = recentVendors.map(v => v.VNDRCD); + + if (vendorCodes.length === 0) { + return { + success: true, + message: '최근 수정된 VENDOR 데이터가 없습니다.' + }; + } + + console.log(`📋 최근 수정된 VENDOR ${vendorCodes.length}개:`, + recentVendors.map(v => `${v.VNDRCD}(${v.updatedAt?.toISOString()})`)); + + // 송신 실행 + const result = await sendVendorMasterToMDG(vendorCodes); + + console.log(`🕒 최근 수정 데이터 송신 완료`); + + return { + ...result, + message: `최근 수정된 ${vendorCodes.length}개 송신 완료: ${result.message}` + }; + + } catch (error) { + console.error('❌ 최근 수정 데이터 송신 중 오류:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +// 통계 조회 유틸리티 함수 +export async function getVendorSendStatistics(): Promise<{ + total: number; + modified: number; + lastModified?: Date; + oldestUnmodified?: Date; +}> { + try { + const [totalResult] = await db + .select({ count: sql`count(*)` }) + .from(VENDOR_MASTER_BP_HEADER); + + const [modifiedResult] = await db + .select({ count: sql`count(*)` }) + .from(VENDOR_MASTER_BP_HEADER) + .where( + sql`EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.updatedAt}) - EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.createdAt}) > 1` + ); + + const [lastModifiedResult] = await db + .select({ updatedAt: VENDOR_MASTER_BP_HEADER.updatedAt }) + .from(VENDOR_MASTER_BP_HEADER) + .where( + sql`EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.updatedAt}) - EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.createdAt}) > 1` + ) + .orderBy(desc(VENDOR_MASTER_BP_HEADER.updatedAt)) + .limit(1); + + const [oldestUnmodifiedResult] = await db + .select({ createdAt: VENDOR_MASTER_BP_HEADER.createdAt }) + .from(VENDOR_MASTER_BP_HEADER) + .where( + sql`EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.updatedAt}) - EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.createdAt}) <= 1` + ) + .orderBy(VENDOR_MASTER_BP_HEADER.createdAt) + .limit(1); + + return { + total: totalResult.count, + modified: modifiedResult.count, + lastModified: lastModifiedResult?.updatedAt || undefined, + oldestUnmodified: oldestUnmodifiedResult?.createdAt || undefined + }; + + } catch (error) { + console.error('통계 조회 실패:', error); + throw error; + } +} diff --git a/lib/soap/mdg/utils.ts b/lib/soap/mdg/utils.ts new file mode 100644 index 00000000..437988dc --- /dev/null +++ b/lib/soap/mdg/utils.ts @@ -0,0 +1,450 @@ +import { XMLParser } from "fast-xml-parser"; +import { readFileSync } from "fs"; +import { NextResponse } from "next/server"; +import { join } from "path"; +import { eq } from "drizzle-orm"; +import db from "@/db/db"; +import { soapLogs, type LogDirection, type SoapLogInsert } from "@/db/schema/SOAP/soap"; + +// XML 파싱용 타입 유틸리티: 스키마에서 XML 타입 생성 +export type ToXMLFields = { + [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 { + // public/wsdl 에서 WSDL 제공함을 가정 + // 이게 WSDL 구현 표준인데, 보안 감사에서 반대한다면 제거 + 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 파서 생성 +// SAP XI 가 자동생성해 보내는 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, + 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) { + const envelopeData = parsedData?.[envelope] as Record | undefined; + if (envelopeData?.[body]) { + const result = extractFromSoapBody(envelopeData[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 변환 함수 +/** + * XML 데이터를 DB 삽입 가능한 형태로 변환 + * + * 아키텍처 설계: + * - 하위 테이블들은 별도의 필수 필드가 없다고 가정 (스키마에서 notNull() 제거 예정) + * - FK는 항상 최상위 테이블의 unique 필드를 참조 + * - 송신된 XML은 항상 전체 데이터셋을 포함 + * - 최상위 테이블의 unique 필드가 충돌하면 전체 삭제 후 재삽입 처리 + * + * FK 처리 방식: + * - XML에 FK 필드가 이미 포함된 경우: XML 값 우선 사용 (예: MATL 인터페이스) + * - XML에 FK 필드가 없는 경우: 상위에서 전달받은 FK 값 사용 (예: VENDOR 인터페이스) + * - 이를 통해 다양한 SAP 인터페이스 패턴에 대응 + * + * @param xmlData XML에서 파싱된 데이터 + * @param fkData 상위 테이블에서 전달받은 FK 데이터 + * @returns DB 삽입 가능한 형태로 변환된 데이터 + */ +export function convertXMLToDBData>( + xmlData: Record, + fkData?: Record +): 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)[key] = value || null; + } + } + + // FK 필드 처리 (XML 우선, 없으면 상위에서 전달받은 값 사용) + if (fkData) { + for (const [key, value] of Object.entries(fkData)) { + // XML에 해당 FK 필드가 없거나 비어있는 경우에만 상위 값 사용 + const existingValue = (result as Record)[key]; + if (!existingValue || existingValue === null || existingValue === '') { + (result as Record)[key] = value; + } + // XML에 이미 FK 필드가 있고 값이 있는 경우는 XML 값을 그대로 사용 + } + } + + return result; +} + +// 중첩 배열 처리 함수 (개선된 버전) +/** + * 중첩된 배열 데이터를 처리하여 DB 삽입 가능한 형태로 변환 + * + * 처리 방식: + * - 하위 테이블 데이터는 FK만 설정하면 됨 + * - 별도의 필수 필드 생성 로직 불필요 + * - 전체 데이터셋 기반으로 삭제 후 재삽입 처리 + * + * @param items 처리할 배열 데이터 + * @param converter 변환 함수 + * @param fkData FK 데이터 + * @returns 변환된 배열 데이터 + */ +export function processNestedArray( + items: T[] | undefined, + converter: (item: T, fkData?: Record) => U, + fkData?: Record +): 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 = ` + + + + soap:Server + ${error instanceof Error ? ('[from eVCP]: ' + error.message) : 'Unknown error'} + + +`; + + return new NextResponse(errorResponse, { + status: 500, + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + }, + }); +} + +// 성공 응답 생성 +export function createSuccessResponse(namespace: string): NextResponse { + const xmlResponse = ` + + + +`; + + return new NextResponse(xmlResponse, { + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + }, + }); +} + +// 하위 테이블 처리: FK 기준으로 전체 삭제 후 재삽입 +/** + * 하위 테이블 데이터를 전체 삭제 후 재삽입하는 함수 + * + * 처리 전략: + * - 송신 XML이 전체 데이터셋을 포함한다는 가정하에 설계 + * - 부분 업데이트보다 전체 교체를 통해 데이터 일관성 확보 + * - FK 기준으로 해당 부모 레코드의 모든 하위 데이터 교체 + * + * 처리 순서: + * 1. FK 기준으로 기존 데이터 전체 삭제 + * 2. 새로운 데이터 전체 삽입 + * + * @param tx 트랜잭션 객체 + * @param table 대상 테이블 스키마 + * @param data 삽입할 데이터 배열 + * @param parentField FK 필드명 (일반적으로 'VNDRCD') + * @param parentValue FK 값 (상위 테이블의 unique 필드 값) + */ +export async function replaceSubTableData>( + tx: Parameters[0]>[0], + table: any, // Drizzle 테이블 객체 - 복잡한 제네릭 타입으로 인해 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 { + 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 { + 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 { + 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( + direction: LogDirection, + system: string, + interfaceName: string, + requestData: string, + processor: () => Promise +): Promise { + 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/public/wsdl/P2MD3007_AO.csv b/public/wsdl/P2MD3007_AO.csv new file mode 100644 index 00000000..261443b5 --- /dev/null +++ b/public/wsdl/P2MD3007_AO.csv @@ -0,0 +1,122 @@ +SEQ,Table,Field,M/O,Type,Size,Description +1,ZTA0MDZBP031,BP_HEADER,M,CHAR,10,Business Partner Number +2,ZTA0MDZBP031,ZZSRMCD,M,CHAR,20,Source System Code +3,ZTA0MDZBP031,TITLE,M,CHAR,4,Title +4,ZTA0MDZBP031,BU_SORT1,M,CHAR,20,Search Term 1 +5,ZTA0MDZBP031,BU_SORT2,,CHAR,20,Search Term 2 +6,ZTA0MDZBP031,NAME_ORG1,M,CHAR,40,Name 1 +7,ZTA0MDZBP031,NAME_ORG2,,CHAR,40,Name 2 +8,ZTA0MDZBP031,NAME_ORG3,,CHAR,40,Name 3 +9,ZTA0MDZBP031,NAME_ORG4,,CHAR,40,Name 4 +10,ZTA0MDZBP031,FOUND_DAT,,DATS,8,Date founded +11,ZTA0MDZBP031,LIFNR,,CHAR,10,Vendor Code +12,ZTA0MDZBP031,KTOKK,M,CHAR,4,Vendor account group +13,ZTA0MDZBP031,ZTYPE,,CHAR,2,Account Group type +14,ZTA0MDZBP031,VBUND,,CHAR,6,Company ID of Trading Partner +15,ZTA0MDZBP031,VEN_KFREP,,CHAR,10,Rep's Name +16,ZTA0MDZBP031,VEN_KFBUS,,CHAR,30,Type of Bus. +17,ZTA0MDZBP031,VEN_KFIND,,CHAR,30,Type of Ind. +18,ZTA0MDZBP031,ZZQMGRP,,CHAR,20,QM Group +19,ZTA0MDZBP031,ZZTELNO,,CHAR,30,QM Tel. No. +20,ZTA0MDZBP031,ZZEMAIL,,CHAR,30,QM E-Mail +21,ZTA0MDZBP031,ZZCNAME1,,CHAR,35,Contact Person 1st name +22,ZTA0MDZBP031,ZZCNAME2,,CHAR,35,Contact Person Name +23,ZTA0MDZBP031,ZZTELF1_C,,CHAR,16,Contact Person telephone number +24,ZTA0MDZBP031,KUNNR,,CHAR,10,Customer Code +25,ZTA0MDZBP031,KTOKD,,CHAR,4,Account Group +26,ZTA0MDZBP031,NIELS,,CHAR,2,Customer Type +27,ZTA0MDZBP031,CUS_VBUND,,CHAR,6,Company ID of Trading Partner +28,ZTA0MDZBP031,ZZPUGRP,,CHAR,20,Customer Group Name +29,ZTA0MDZBP031,J_1KFREPRE,,CHAR,10,Customer Rep's Name +30,ZTA0MDZBP031,J_1KFTBUS,,CHAR,30,CustomerType of Bus. +31,ZTA0MDZBP031,J_1KFTIND,,CHAR,30,CustomerType of Ind. +32,ZTA0MDZBP031,ZZBA,,CHAR,4,Business Area(GBM) +33,ZTA0MDZBP031,REGDT,,DATS,8,Registerd Date +34,ZTA0MDZBP031,REGTM,,TIMS,6,Registerd Time +35,ZTA0MDZBP031,REGUS,,CHAR,12,Registerd System +36,ZTA0MDZBP031,MASTERFLAG,M,CHAR,1,Master Flag +37,ZTA0MDZBP031,STATUS,,CHAR,1,Status +38,ZTA0MDZBP031,IBND_TYPE,M,CHAR,1,Inbound Type +39,ZTA0MDZBP031,RSLT,,CHAR,40,Inbound Result Reason +40,ZTA0MDZBP031,ZZCSTFLG,,CHAR,1,Construction Flag +41,ZTA0MDZBP031,ZZCVLFLG,,CHAR,1,Civil Flag +42,ZTA0MDZBP031,ZZPLTFLG,,CHAR,1,Plant Flag +43,ZTA0MDZBP031,ZZHUSFLG,,CHAR,1,House Flag +44,ZTA0MDZBP031,ZZIFAFLG,,CHAR,1,Information Agree Flag +45,ZTA0MDZBP031,ZZDMFLG,,CHAR,1,Preferred contract - DM +46,ZTA0MDZBP031,ZZSMSFLG,,CHAR,1,Preferred contract - SMS +47,ZTA0MDZBP031,ZZEMLFLG,,CHAR,1,Preferred contract - E-Mail +48,ZTA0MDZBP031,ZZPHNFLG,,CHAR,1,Preferred contract - Telephone +49,ZTA0MDZBP031,ZZPSPID,,CHAR,24,Project Code +50,ZTA0MDZBP031,ZZSTCEG_V,,CHAR,20,VAT Registration Number +51,ZTA0MDZBP031,ZZVNDTYP,,CHAR,20,Vendor Type +52,ZTA0MDZBP031,ZZREQID,M,CHAR,12,Registered User +53,ZTA0MDZBP031,ZZSUBSEQ,,CHAR,4,Sub Workplace Seq. +54,ZTA0MDZBP031,ZZORT01,,CHAR,35,City +55,ZTA0MDZBP031,ZZSTRAS,,CHAR,35,House number and street +56,ZTA0MDZBP031,ZZINBFLG,,CHAR,1,Inbound Flag Data Element +57,ZTA0MDZBP031,ZZIND01,,CHAR,1,Indicator 01 +58,ZTA0MDZBP031,ZZIND02,,CHAR,1,Indicator 02 +59,ZTA0MDZBP031,ZZIND03,,CHAR,1,Indicator 03 +60,ZTA0MDZBP031,ZZIND04,,CHAR,1,Indicator 04 +61,ZTA0MDZBP031,ZZIND05,,CHAR,1,Indicator 05 +62,ZTA0MDZBP032,BP_HEADER,M,CHAR,10,Business Partner Number +63,ZTA0MDZBP032,ZZSRMCD,M,CHAR,20,Source System Code +64,ZTA0MDZBP032,ADDRNO,M,CHAR,10,Address Number +65,ZTA0MDZBP032,AD_NATION,M,CHAR,1,International address version ID +66,ZTA0MDZBP032,COUNTRY,M,CHAR,3,Country Key +67,ZTA0MDZBP032,LANGU_COM,M,LANG,1,Language Key +68,ZTA0MDZBP032,POST_COD1,M,CHAR,10,Postal Code +69,ZTA0MDZBP032,POST_COD2,,CHAR,10,Postal Code +70,ZTA0MDZBP032,CITY1,M,CHAR,40,City +71,ZTA0MDZBP032,DISTRICT,,CHAR,40,District +72,ZTA0MDZBP032,REGION,,CHAR,3,"Region (State, Province, County)" +73,ZTA0MDZBP032,MC_STREET,M,CHAR,60,Street +74,ZTA0MDZBP032,HOUSE_NR1,,CHAR,10,House Number +75,ZTA0MDZBP032,PO_BOX,,CHAR,10,PO Box +76,ZTA0MDZBP032,TRANSPZONE,,CHAR,10,Transportation zone to or from which the goods are +77,ZTA0MDZBP032,TIME_ZONE,,CHAR,6,Address time zone +78,ZTA0MDZBP032,ZZCONTIN,,CHAR,20,Continent +79,ZTA0MDZBP032,NAME_ORG1,,CHAR,40,Name 1 +80,ZTA0MDZBP032,NAME_ORG2,,CHAR,40,Name 2 +81,ZTA0MDZBP032,NAME_ORG3,,CHAR,40,Name 3 +82,ZTA0MDZBP032,NAME_ORG4,,CHAR,40,Name 4 +83,ZTA0MDZBP032,BU_SORT1,M,CHAR,20,Search Term 1 +84,ZTA0MDZBP032,BU_SORT2,,CHAR,20,Search Term 2 +85,ZTA0MDZBP033,BP_HEADER,M,CHAR,10,Business Partner Number +86,ZTA0MDZBP033,ZZSRMCD,M,CHAR,20,Source System Code +87,ZTA0MDZBP033,ADDRNO,M,CHAR,10,Address Number +88,ZTA0MDZBP033,AD_CONSNO,M,NUMC,3,Sequence number +89,ZTA0MDZBP033,T_COUNTRY,M,CHAR,3,Country for telephone/fax number +90,ZTA0MDZBP033,T_NUMBER,,CHAR,30,Telephone no.: dialling code+number +91,ZTA0MDZBP033,T_EXTENS,,CHAR,10,Telephone no.: Extension +92,ZTA0MDZBP033,T_FLGMOB,,CHAR,1,Indicator: Telephone is a Mobile Telephone +93,ZTA0MDZBP034,BP_HEADER,M,CHAR,10,Business Partner Number +94,ZTA0MDZBP034,ZZSRMCD,M,CHAR,20,Source System Code +95,ZTA0MDZBP034,ADDRNO,M,CHAR,10,Address Number +96,ZTA0MDZBP034,AD_CONSNO,M,NUMC,3,Sequence number +97,ZTA0MDZBP034,F_COUNTRY,M,CHAR,3,Country for telephone/fax number +98,ZTA0MDZBP034,F_NUMBER,,CHAR,30,Fax number +99,ZTA0MDZBP034,F_EXTENS,,CHAR,10,Fax no.: Extension +100,ZTA0MDZBP035,BP_HEADER,M,CHAR,10,Business Partner Number +101,ZTA0MDZBP035,ZZSRMCD,M,CHAR,20,Source System Code +102,ZTA0MDZBP035,ADDRNO,M,CHAR,10,Address Number +103,ZTA0MDZBP035,AD_CONSNO,M,NUMC,3,Sequence number +104,ZTA0MDZBP035,U_ADDRESS,,LCHR,2048,Universal Resource Identifier (URI) +105,ZTA0MDZBP036,BP_HEADER,M,CHAR,10,Business Partner Number +106,ZTA0MDZBP036,ZZSRMCD,M,CHAR,20,Source System Code +107,ZTA0MDZBP036,ADDRNO,M,CHAR,10,Address Number +108,ZTA0MDZBP036,AD_CONSNO,M,NUMC,3,Sequence number +109,ZTA0MDZBP036,E_ADDRESS,,CHAR,241,E-Mail Address +110,ZTA0MDZBP037,BP_HEADER,M,CHAR,10,Business Partner Number +111,ZTA0MDZBP037,ZZSRMCD,M,CHAR,20,Source System Code +112,ZTA0MDZBP037,BP_TX_TYP,M,CHAR,4,Tax Number Category(KR2) +113,ZTA0MDZBP037,TAXNUM,,CHAR,20,Business Partner Tax Number(KR2) +114,ZTA0MDZBP037,BP_HEADER,M,CHAR,10,Business Partner Number +115,ZTA0MDZBP037,ZZSRMCD,M,CHAR,20,Source System Code +116,ZTA0MDZBP037,BP_TX_TYP,,CHAR,4,constant(KR1)-Resident Registration Number +117,ZTA0MDZBP037,TAXNUM,,CHAR,20,Business Partner Tax Number(KR1) +118,ZTA0MDZBP037,BP_HEADER,M,CHAR,10,Business Partner Number +119,ZTA0MDZBP037,ZZSRMCD,M,CHAR,20,Source System Code +120,ZTA0MDZBP037,BP_TX_TYP,,CHAR,4,constant(KR3)-Corporate Registration Number +121,ZTA0MDZBP037,TAXNUM,,CHAR,20,Business Partner Tax Number(KR3) -- cgit v1.2.3