summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-07-02 10:00:07 +0000
committerjoonhoekim <26rote@gmail.com>2025-07-02 10:00:07 +0000
commitdeb2d31dba913a3b831523f41b9bf2e286c53af1 (patch)
tree26b7c440445ef0bb32a54450018b449e0d62d7c9
parentc0c80aa0e43fd70cee6ccb94c66354eb4c25873c (diff)
(김준회) MDG 수신 구조 개선 및 MDG SOAP 송신 액션 & 테스트 페이지 구성
-rw-r--r--app/[lng]/admin/mdg/page.tsx248
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts2
-rw-r--r--lib/soap/mdg/send/vendor-master/action.ts584
-rw-r--r--lib/soap/mdg/utils.ts (renamed from app/api/(S-ERP)/(MDG)/utils.ts)0
-rw-r--r--public/wsdl/P2MD3007_AO.csv122
15 files changed, 965 insertions, 11 deletions
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<string, string> = {
+ 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 '&lt;';
+ case '>': return '&gt;';
+ case '&': return '&amp;';
+ case '"': return '&quot;';
+ case "'": return '&apos;';
+ default: return c;
+ }
+});
+
+export default function MDGTestPage() {
+ const [formData, setFormData] = useState<Record<string, string>>({});
+ const [fieldDefs, setFieldDefs] = useState<VendorFieldDef[]>([]);
+ const [resultXml, setResultXml] = useState<string>('');
+ 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<string, string> = {};
+ 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<string, string> = {};
+ 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)}</${f.name}>`;
+ }).join('\n ');
+
+ const supplierXml = `<SUPPLIER_MASTER>\n ${bodyContent}\n </SUPPLIER_MASTER>`;
+
+ const envelope = `<?xml version="1.0" encoding="UTF-8"?>\n<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:p1=\"http://shi.samsung.co.kr/P2_MD/MDZ\">\n <soap:Header/>\n <soap:Body>\n <p1:MT_P2MD3007_S>\n <P2MD3007_S>\n ${supplierXml}\n </P2MD3007_S>\n </p1:MT_P2MD3007_S>\n </soap:Body>\n</soap:Envelope>`;
+
+ setResultXml(envelope);
+ toast.success('요청 XML이 생성되었습니다. 하단 영역을 확인하세요.');
+
+ } catch (error) {
+ console.error('테스트 송신 실패:', error);
+ toast.error('테스트 송신 중 오류가 발생했습니다.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div className="container mx-auto p-6 space-y-6">
+ <div className="flex items-center justify-between">
+ <div>
+ <h1 className="text-3xl font-bold">MDG VENDOR 마스터 테스트</h1>
+ <p className="text-muted-foreground mt-2">
+ VENDOR 마스터 데이터를 MDG 시스템으로 테스트 송신합니다
+ </p>
+ </div>
+ <div className="flex gap-2">
+ <Button variant="outline" onClick={resetForm}>
+ <RefreshCw className="w-4 h-4 mr-2" />
+ 리셋
+ </Button>
+ <Button onClick={handleTestSend} disabled={isLoading}>
+ {isLoading ? (
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+ ) : (
+ <Send className="w-4 h-4 mr-2" />
+ )}
+ 테스트 송신
+ </Button>
+ </div>
+ </div>
+
+ {/* 동적 필드 렌더링 */}
+ {fieldDefs.length === 0 ? (
+ <p className="text-center text-muted-foreground">CSV 로딩 중...</p>
+ ) : (
+ <div className="space-y-6">
+ {Object.entries(
+ fieldDefs.reduce((acc: Record<string, VendorFieldDef[]>, cur) => {
+ acc[cur.table] = acc[cur.table] ? [...acc[cur.table], cur] : [cur];
+ return acc;
+ }, {})
+ ).map(([table, fields]) => (
+ <Card key={table}>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ {table}
+ {fields.some(f => f.mandatory) && (
+ <Badge variant="destructive">필수 포함</Badge>
+ )}
+ </CardTitle>
+ <CardDescription>{table} 테이블 입력</CardDescription>
+ </CardHeader>
+ <CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ {fields.filter((f, idx, arr) => arr.findIndex(x => x.name === f.name) === idx).map((field) => (
+ <div key={field.name}>
+ <Label htmlFor={field.name} className="flex items-center gap-1">
+ {field.name}
+ {field.mandatory && (
+ <Badge variant="destructive" className="ml-1">필수</Badge>
+ )}
+ </Label>
+ <Input
+ id={field.name}
+ value={formData[field.name] ?? ''}
+ onChange={(e) => updateField(field.name, e.target.value)}
+ />
+ {field.description && (
+ <p className="text-xs text-muted-foreground mt-1">{field.description}</p>
+ )}
+ </div>
+ ))}
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ )}
+
+ {/* 송신 결과 영역 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>송신 결과</CardTitle>
+ <CardDescription>
+ MDG 시스템으로의 송신 결과가 여기에 표시됩니다
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ {resultXml ? (
+ <pre className="p-4 bg-muted max-h-96 overflow-auto text-xs whitespace-pre-wrap">
+ {resultXml}
+ </pre>
+ ) : (
+ <div className="p-4 bg-muted rounded-lg">
+ <p className="text-sm text-muted-foreground">
+ 테스트 송신 버튼을 클릭하면 결과가 표시됩니다.
+ </p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
+
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/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 '&lt;';
+ case '>': return '&gt;';
+ case '&': return '&amp;';
+ case '"': return '&quot;';
+ case "'": return '&apos;';
+ 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<Awaited<ReturnType<typeof fetchVendorData>>>): string {
+ const { vendorHeader, addresses, adEmails, adFaxes, adPostals, adTels, adUrls, bpTaxnums, bpVengens } = vendorData;
+
+ // 값 추출 매핑 ------------------------------------
+ const mapping: Record<string, string | undefined> = {
+ // 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<string>();
+ 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 ?? '')}</${f.field}>`;
+ }).join('\n ');
+
+ const supplierMasterXml = `<SUPPLIER_MASTER>\n ${fieldXml}\n </SUPPLIER_MASTER>`;
+
+ const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>\n<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:p1="http://shi.samsung.co.kr/P2_MD/MDZ">\n <soap:Header/>\n <soap:Body>\n <p1:MT_P2MD3007_S>\n <P2MD3007_S>\n ${supplierMasterXml}\n </P2MD3007_S>\n </p1:MT_P2MD3007_S>\n </soap:Body>\n</soap:Envelope>`;
+
+ 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<number>`count(*)` })
+ .from(VENDOR_MASTER_BP_HEADER);
+
+ const [modifiedResult] = await db
+ .select({ count: sql<number>`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/app/api/(S-ERP)/(MDG)/utils.ts b/lib/soap/mdg/utils.ts
index 437988dc..437988dc 100644
--- a/app/api/(S-ERP)/(MDG)/utils.ts
+++ b/lib/soap/mdg/utils.ts
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)