summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/admin/mdg/page.tsx256
-rw-r--r--lib/soap/mdg/send/vendor-master/action.ts541
-rw-r--r--lib/soap/mdg/utils.ts47
3 files changed, 545 insertions, 299 deletions
diff --git a/app/[lng]/admin/mdg/page.tsx b/app/[lng]/admin/mdg/page.tsx
new file mode 100644
index 00000000..8499f103
--- /dev/null
+++ b/app/[lng]/admin/mdg/page.tsx
@@ -0,0 +1,256 @@
+'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'
+import { sendTestVendorDataToMDG } from '@/lib/soap/mdg/send/vendor-master/action'
+
+// 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',
+ AD_CONSNO: '1',
+};
+
+export default function MDGTestPage() {
+ const [formData, setFormData] = useState<Record<string, string>>({});
+ const [fieldDefs, setFieldDefs] = useState<VendorFieldDef[]>([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [lastResult, setLastResult] = useState<string>('');
+
+ // 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);
+ setLastResult('');
+ toast.success('폼이 기본값으로 리셋되었습니다.');
+ };
+
+ // 테스트 송신 실행 (SOAP 라이브러리 사용)
+ const handleTestSend = async () => {
+ try {
+ setIsLoading(true);
+ setLastResult('');
+
+ // 필수 필드 검증
+ 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(', ')}`);
+ setIsLoading(false);
+ return;
+ }
+
+ // fast-xml-parser 기반 송신 함수 호출
+ const result = await sendTestVendorDataToMDG(formData);
+
+ if (!result.success) {
+ toast.error(`송신 실패: ${result.message}`);
+ setLastResult(`❌ [SAP XI] 송신 실패: ${result.message}`);
+ setIsLoading(false);
+ return;
+ }
+
+ toast.success('MDG 송신이 완료되었습니다.');
+ setLastResult(`✅ [SAP XI] 송신 성공: ${result.message}
+
+🔍 생성된 XML:
+${result.generatedXML}
+
+📄 응답 데이터:
+${typeof result.responseData === 'string' ? result.responseData : JSON.stringify(result.responseData, null, 2)}`);
+
+ } catch (error) {
+ console.error('테스트 송신 실패:', error);
+ const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
+ toast.error(`테스트 송신 중 오류가 발생했습니다: ${errorMessage}`);
+ setLastResult(`❌ [SAP XI] 송신 오류: ${errorMessage}`);
+ } 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 시스템으로 테스트 송신합니다.<br />
+ <strong>fast-xml-parser</strong>를 사용하여 SAP XI 호환 XML을 생성하고 전송합니다.
+ </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" />
+ )}
+ MDG 테스트 송신
+ </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 시스템 송신 결과가 여기에 표시됩니다 (node-soap 또는 fetch 방식)
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ {lastResult ? (
+ <pre className="p-4 bg-muted max-h-96 overflow-auto text-xs whitespace-pre-wrap">
+ {lastResult}
+ </pre>
+ ) : (
+ <div className="p-4 bg-muted rounded-lg">
+ <p className="text-sm text-muted-foreground">
+ 테스트 송신 버튼을 클릭하면 결과가 표시됩니다.<br />
+ - <strong>node-soap</strong>: 개선된 타임아웃 설정으로 테스트<br />
+ - <strong>fetch</strong>: 기존 정상 동작 방식으로 테스트
+ </p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
+
diff --git a/lib/soap/mdg/send/vendor-master/action.ts b/lib/soap/mdg/send/vendor-master/action.ts
index b353eab0..24112316 100644
--- a/lib/soap/mdg/send/vendor-master/action.ts
+++ b/lib/soap/mdg/send/vendor-master/action.ts
@@ -18,19 +18,18 @@ import {
} from "@/db/schema/MDG/mdg";
import { eq, sql, desc } from "drizzle-orm";
import { withSoapLogging } from "../../utils";
-import * as soap from 'soap';
import fs from 'fs';
import path from 'path';
-
-// WSDL 파일 경로
-const WSDL_PATH = path.join(process.cwd(), 'public', 'wsdl', 'P2MD3007_AO.wsdl');
+import { XMLBuilder } from 'fast-xml-parser';
// 환경변수에서 인증 정보 가져오기
const MDG_SOAP_USERNAME = process.env.MDG_SOAP_USERNAME;
const MDG_SOAP_PASSWORD = process.env.MDG_SOAP_PASSWORD;
-const MDG_SOAP_AUTH_TYPE = process.env.MDG_SOAP_AUTH_TYPE || 'basic'; // 'basic' | 'wssecurity'
-// CSV 파싱 및 필드 정의 ------------------------------------------------
+// SAP XI 엔드포인트 URL
+const MDG_ENDPOINT_URL = "http://shii8dvddb01.hec.serp.shi.samsung.net:50000/sap/xi/engine?type=entry&version=3.0&Sender.Service=P2038_Q&Interface=http%3A%2F%2Fshi.samsung.co.kr%2FP2_MD%2FMDZ%5EP2MD3007_AO&QualityOfService=ExactlyOnce";
+
+// CSV 파싱 및 필드 정의
interface CsvField {
table: string;
field: string;
@@ -59,151 +58,114 @@ try {
console.error('CSV 로딩 실패:', e);
}
-// SOAP 클라이언트 생성 및 인증 설정 함수
-async function createSoapClient(): Promise<soap.Client> {
- return new Promise((resolve, reject) => {
- soap.createClient(WSDL_PATH, (err, client) => {
- if (err) {
- reject(err);
- return;
- }
-
- if (!client) {
- reject(new Error('SOAP 클라이언트 생성 실패'));
- return;
- }
-
- // 인증 설정
- if (MDG_SOAP_USERNAME && MDG_SOAP_PASSWORD) {
- try {
- if (MDG_SOAP_AUTH_TYPE === 'wssecurity') {
- // WS-Security 인증 설정
- const wsSecurity = new soap.WSSecurity(MDG_SOAP_USERNAME, MDG_SOAP_PASSWORD);
- client.setSecurity(wsSecurity);
- console.log('🔐 WS-Security 인증 설정 완료');
- } else {
- // Basic Authentication 설정 (기본값)
- const basicAuth = new soap.BasicAuthSecurity(MDG_SOAP_USERNAME, MDG_SOAP_PASSWORD);
- client.setSecurity(basicAuth);
- console.log('🔐 Basic Authentication 설정 완료');
- }
- } catch (authError) {
- console.warn('⚠️ 인증 설정 중 오류:', authError);
+// SAP XI 호환 XML 생성 함수 (수정된 버전)
+function generateSAPXICompatibleXML(supplierMaster: Record<string, string>): string {
+ // XML 선언을 별도로 처리
+ const xmlDeclaration = '<?xml version="1.0" encoding="UTF-8"?>\n';
+
+ // SOAP Envelope 구조 정의
+ const soapEnvelope = {
+ 'soap:Envelope': {
+ '@_xmlns:soap': 'http://schemas.xmlsoap.org/soap/envelope/',
+ '@_xmlns:ns0': 'http://shi.samsung.co.kr/P2_MD/MDZ',
+ 'soap:Body': {
+ 'ns0:MT_P2MD3007_S': {
+ 'SUPPLIER_MASTER': supplierMaster
}
- } else {
- console.warn('⚠️ MDG SOAP 인증 정보가 환경변수에 설정되지 않았습니다.');
}
+ }
+ };
- resolve(client);
- });
+ const builder = new XMLBuilder({
+ ignoreAttributes: false,
+ format: true,
+ attributeNamePrefix: '@_',
+ textNodeName: '#text',
+ suppressEmptyNode: true, // 빈 노드는 self-closing 태그로 처리
+ suppressUnpairedNode: false,
+ indentBy: ' ', // 2칸 들여쓰기
+ processEntities: false, // 엔티티 변환 방지
+ suppressBooleanAttributes: false,
+ cdataPropName: false,
+ tagValueProcessor: (name, val) => val, // 값 처리기
+ attributeValueProcessor: (name, val) => val // 속성 처리기
});
+
+ const xmlBody = builder.build(soapEnvelope);
+
+ // XML 선언과 Body 결합
+ const completeXML = xmlDeclaration + xmlBody;
+
+ console.log('🔍 생성된 XML (전체):', completeXML);
+
+ return completeXML;
}
-// SOAP 응답 타입 정의
-interface SoapResponse {
- [key: string]: unknown;
-}
-
-// SOAP 오류 타입 정의
-interface SoapError {
- message: string;
- body?: string;
- statusCode?: number;
-}
-
-// VENDOR 마스터 데이터를 MDG로 송신하는 액션
-export async function sendVendorMasterToMDG(vendorCodes: string[]): Promise<{
+// XML을 MDG로 전송하는 함수 (성공했던 구조 사용)
+async function sendXMLToMDG(xmlData: string): Promise<{
success: boolean;
message: string;
- results?: Array<{ vendorCode: string; success: boolean; error?: string }>;
+ responseText?: 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;
- }
-
- // SOAP 요청 데이터 생성
- const soapData = buildSoapData(vendorData);
- console.log(`📄 VENDOR ${vendorCode} SOAP 데이터 생성 완료`);
-
- // 데이터 구조 검증 (디버깅용)
- const validation = validateSoapDataStructure(soapData);
- if (!validation.isValid) {
- console.warn(`⚠️ VENDOR ${vendorCode} 데이터 구조 문제:`, validation.issues);
+ const responseText = await withSoapLogging(
+ 'OUTBOUND',
+ 'S-ERP MDG',
+ 'IF_MDZ_EVCP_VENDOR_MASTER',
+ xmlData,
+ async () => {
+ // 성공했던 전송 방식 그대로 사용
+ const headers: Record<string, string> = {
+ 'Content-Type': 'text/xml; charset=utf-8',
+ 'SOAPAction': 'http://sap.com/xi/WebService/soap1.1',
+ };
+
+ // Basic Authentication 헤더 추가
+ if (MDG_SOAP_USERNAME && MDG_SOAP_PASSWORD) {
+ const credentials = Buffer.from(`${MDG_SOAP_USERNAME}:${MDG_SOAP_PASSWORD}`).toString('base64');
+ headers['Authorization'] = `Basic ${credentials}`;
+ console.log('🔐 Basic Authentication 헤더 추가 완료');
} else {
- console.log(`✅ VENDOR ${vendorCode} 데이터 구조 검증 통과`);
+ console.warn('⚠️ MDG SOAP 인증 정보가 환경변수에 설정되지 않았습니다.');
}
- // SOAP 클라이언트로 요청 전송
- await withSoapLogging(
- 'OUTBOUND',
- 'S-ERP MDG',
- 'IF_MDZ_EVCP_VENDOR_MASTER',
- JSON.stringify(soapData),
- async () => {
- const client = await createSoapClient();
-
- return new Promise<SoapResponse>((resolve, reject) => {
- client.P2MD3007_AO(soapData, (err: SoapError | null, result: SoapResponse) => {
- if (err) {
- reject(err);
- } else {
- console.log(`✅ VENDOR ${vendorCode} MDG 전송 성공`);
- resolve(result);
- }
- });
- });
- }
- );
+ console.log('📤 MDG 전송 시작');
+ console.log('🔍 전송 XML (첫 500자):', xmlData.substring(0, 500));
- results.push({
- vendorCode,
- success: true
+ const res = await fetch(MDG_ENDPOINT_URL, {
+ method: 'POST',
+ headers,
+ body: xmlData,
});
+
+ const text = await res.text();
- } catch (error) {
- console.error(`❌ VENDOR ${vendorCode} 전송 실패:`, error);
- results.push({
- vendorCode,
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error'
- });
+ console.log('📥 MDG 응답 수신:', res.status, res.statusText);
+ console.log('🔍 응답 XML (첫 500자):', text.substring(0, 500));
+
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
+ }
+
+ // SOAP Fault 검사
+ if (text.includes('soap:Fault') || text.includes('SOAP:Fault')) {
+ throw new Error(`MDG SOAP Fault: ${text}`);
+ }
+
+ return text;
}
- }
-
- 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
+ success: true,
+ message: '전송 성공',
+ responseText,
};
-
} catch (error) {
- console.error('❌ VENDOR_MASTER 송신 중 전체 오류 발생:', error);
+ console.error('❌ XML 전송 실패:', error);
return {
success: false,
- message: error instanceof Error ? error.message : 'Unknown error'
+ message: error instanceof Error ? error.message : 'Unknown error',
};
}
}
@@ -211,7 +173,6 @@ export async function sendVendorMasterToMDG(vendorCodes: string[]): Promise<{
// 데이터베이스에서 VENDOR 데이터 조회
async function fetchVendorData(vendorCode: string) {
try {
- // 1. 헤더 데이터 조회
const [vendorHeader] = await db
.select()
.from(VENDOR_MASTER_BP_HEADER)
@@ -222,7 +183,6 @@ async function fetchVendorData(vendorCode: string) {
return null;
}
- // 2. 관련 데이터 병렬 조회
const [
addresses,
adEmails,
@@ -273,22 +233,19 @@ async function fetchVendorData(vendorCode: string) {
}
}
-// SOAP 데이터 생성 (WSDL 구조에 맞춤)
-function buildSoapData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVendorData>>>) {
+// SUPPLIER_MASTER 데이터 생성
+function buildSupplierMasterData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVendorData>>>) {
const { vendorHeader, addresses, adFaxes, adPostals, adTels, bpTaxnums, bpVengens } = vendorData;
- // 값 추출 매핑 ------------------------------------
const mapping: Record<string, string | undefined> = {
- // Header
BP_HEADER: vendorHeader?.VNDRCD,
ZZSRMCD: 'EVCP',
- TITLE: '', // vendorHeader에 TITLE 필드가 없음
+ TITLE: '',
BU_SORT1: adPostals[0]?.VNDRNM_ABRV_1 ?? undefined,
NAME_ORG1: adPostals[0]?.VNDRNM_1 ?? undefined,
KTOKK: bpVengens[0]?.ACNT_GRP ?? undefined,
MASTERFLAG: 'X',
IBND_TYPE: 'U',
- // Address mandatory (first)
ADDRNO: addresses[0]?.ADDRNO,
AD_NATION: adPostals[0]?.INTL_ADR_VER_ID ?? undefined,
COUNTRY: adPostals[0]?.NTN_CD ?? undefined,
@@ -296,17 +253,13 @@ function buildSoapData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVe
POST_COD1: adPostals[0]?.CITY_ZIP_NO ?? undefined,
CITY1: adPostals[0]?.VNDRNM_1 ?? undefined,
MC_STREET: adPostals[0]?.ADR_1 ?? undefined,
- // Phone/Fax mandatory fields
AD_CONSNO: '001',
T_COUNTRY: adTels[0]?.NTN_CD ?? 'KR',
F_COUNTRY: adFaxes[0]?.NTN_CD ?? 'KR',
- // Tax
BP_TX_TYP: bpTaxnums[0]?.TX_NO_CTG ?? 'KR2',
TAXNUM: bpVengens[0]?.VAT_REG_NO ?? undefined,
- // Default others can be added as needed
};
- // 필드 순서에 따라 데이터 생성
const seen = new Set<string>();
const uniqueFields = CSV_FIELDS.filter(f => {
if (seen.has(f.field)) return false;
@@ -319,22 +272,158 @@ function buildSoapData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVe
supplierMaster[f.field] = mapping[f.field] ?? '';
});
- // SOAP 요청 구조 생성 (P2MD3007_S 중복 제거)
- // SOAP 클라이언트가 MT_P2MD3007_S로 래핑하므로, 여기서는 SUPPLIER_MASTER만 반환
- return {
- SUPPLIER_MASTER: supplierMaster
- };
+ return supplierMaster;
+}
+
+// ========================================
+// 메인 송신 함수들
+// ========================================
+
+// 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 }> = [];
+
+ for (const vendorCode of vendorCodes) {
+ try {
+ console.log(`📤 VENDOR ${vendorCode} 데이터 조회 중...`);
+
+ const vendorData = await fetchVendorData(vendorCode);
+
+ if (!vendorData) {
+ results.push({
+ vendorCode,
+ success: false,
+ error: 'VENDOR 데이터를 찾을 수 없습니다.'
+ });
+ continue;
+ }
+
+ const supplierMaster = buildSupplierMasterData(vendorData);
+ console.log(`📄 VENDOR ${vendorCode} 데이터 생성 완료`);
+
+ const generatedXML = generateSAPXICompatibleXML(supplierMaster);
+ const result = await sendXMLToMDG(generatedXML);
+
+ if (result.success) {
+ console.log(`✅ VENDOR ${vendorCode} MDG 전송 성공`);
+ results.push({
+ vendorCode,
+ success: true
+ });
+ } else {
+ console.error(`❌ VENDOR ${vendorCode} 전송 실패: ${result.message}`);
+ results.push({
+ vendorCode,
+ success: false,
+ error: result.message
+ });
+ }
+
+ } 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'
+ };
+ }
+}
+
+// 테스트용 폼 데이터 송신 함수
+export async function sendTestVendorDataToMDG(formData: Record<string, string>): Promise<{
+ success: boolean;
+ message: string;
+ responseData?: unknown;
+ generatedXML?: string;
+}> {
+ try {
+ console.log('🚀 테스트용 VENDOR 데이터 송신 시작');
+
+ let csvFields: CsvField[] = [];
+ try {
+ const csvRaw = fs.readFileSync(CSV_PATH, 'utf-8');
+ csvFields = parseCsv(csvRaw);
+ } catch (e) {
+ console.error('CSV 로딩 실패:', e);
+ return {
+ success: false,
+ message: 'CSV 필드 정의 파일을 로드할 수 없습니다.'
+ };
+ }
+
+ const seen = new Set<string>();
+ const uniqueFields = csvFields.filter(f => {
+ if (seen.has(f.field)) return false;
+ seen.add(f.field);
+ return true;
+ });
+
+ const supplierMaster: Record<string, string> = {};
+ uniqueFields.forEach(f => {
+ supplierMaster[f.field] = formData[f.field] ?? '';
+ });
+
+ const generatedXML = generateSAPXICompatibleXML(supplierMaster);
+
+ console.log('📄 SAP XI 호환 XML 생성 완료');
+
+ const result = await sendXMLToMDG(generatedXML);
+
+ return {
+ success: result.success,
+ message: result.success ? '테스트 송신이 완료되었습니다.' : result.message,
+ responseData: result.responseText,
+ generatedXML
+ };
+
+ } catch (error) {
+ console.error('❌ 테스트 송신 실패:', error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
}
-// 특정 VENDOR만 송신하는 유틸리티 함수
+// ========================================
+// 유틸리티 함수들
+// ========================================
+
+// 특정 VENDOR만 송신
export async function sendSingleVendorToMDG(vendorCode: string) {
return await sendVendorMasterToMDG([vendorCode]);
}
-// 모든 VENDOR 송신하는 유틸리티 함수 (주의: 대량 데이터 처리)
+// 모든 VENDOR 송신 (주의: 대량 데이터 처리)
export async function sendAllVendorsToMDG() {
try {
- // 모든 VENDOR 코드 조회
const vendors = await db
.select({ VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD })
.from(VENDOR_MASTER_BP_HEADER);
@@ -350,7 +439,6 @@ export async function sendAllVendorsToMDG() {
console.log(`⚠️ 전체 VENDOR 송신 요청: ${vendorCodes.length}개`);
- // 배치 처리 (10개씩 분할하여 처리)
const batchSize = 10;
const results: Array<{ vendorCode: string; success: boolean; error?: string }> = [];
@@ -363,7 +451,6 @@ export async function sendAllVendorsToMDG() {
results.push(...batchResult.results);
}
- // 배치 간 잠깐 대기 (서버 부하 방지)
if (i + batchSize < vendorCodes.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
@@ -387,7 +474,7 @@ export async function sendAllVendorsToMDG() {
}
}
-// 수정된 VENDOR만 송신하는 액션 (updatedAt != createdAt 조건)
+// 수정된 VENDOR만 송신
export async function sendModifiedVendorsToMDG(): Promise<{
success: boolean;
message: string;
@@ -396,7 +483,6 @@ export async function sendModifiedVendorsToMDG(): Promise<{
try {
console.log('🔍 수정된 VENDOR 데이터 조회 중...');
- // updatedAt과 createdAt이 다른 VENDOR 조회 (수정된 것들)
const modifiedVendors = await db
.select({
VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD,
@@ -405,7 +491,6 @@ export async function sendModifiedVendorsToMDG(): Promise<{
})
.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`
);
@@ -421,12 +506,6 @@ export async function sendModifiedVendorsToMDG(): Promise<{
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 }> = [];
@@ -439,7 +518,6 @@ export async function sendModifiedVendorsToMDG(): Promise<{
results.push(...batchResult.results);
}
- // 배치 간 대기
if (i + batchSize < vendorCodes.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
@@ -465,7 +543,7 @@ export async function sendModifiedVendorsToMDG(): Promise<{
}
}
-// 테스트용 N건 송신 액션
+// 테스트용 N건 송신
export async function sendNVendorsToMDG(
count: number,
startFrom: number = 0
@@ -484,7 +562,6 @@ export async function sendNVendorsToMDG(
console.log(`🧪 테스트용 VENDOR 송신: ${count}건 (${startFrom}번째부터)`);
- // N건의 VENDOR 코드 조회
const vendors = await db
.select({ VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD })
.from(VENDOR_MASTER_BP_HEADER)
@@ -502,7 +579,6 @@ export async function sendNVendorsToMDG(
console.log(`📋 테스트 대상 VENDOR ${vendorCodes.length}개:`, vendorCodes);
- // 송신 실행
const result = await sendVendorMasterToMDG(vendorCodes);
console.log(`🧪 테스트 송신 완료: ${vendorCodes.length}개 처리`);
@@ -521,7 +597,7 @@ export async function sendNVendorsToMDG(
}
}
-// 최신 수정된 N건 송신 액션 (최근 수정 순)
+// 최신 수정된 N건 송신
export async function sendRecentModifiedVendorsToMDG(
count: number = 5
): Promise<{
@@ -532,7 +608,6 @@ export async function sendRecentModifiedVendorsToMDG(
try {
console.log(`🕒 최근 수정된 VENDOR ${count}건 조회 중...`);
- // 최근 수정된 순으로 N건 조회
const recentVendors = await db
.select({
VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD,
@@ -540,7 +615,6 @@ export async function sendRecentModifiedVendorsToMDG(
})
.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))
@@ -558,7 +632,6 @@ export async function sendRecentModifiedVendorsToMDG(
console.log(`📋 최근 수정된 VENDOR ${vendorCodes.length}개:`,
recentVendors.map(v => `${v.VNDRCD}(${v.updatedAt?.toISOString()})`));
- // 송신 실행
const result = await sendVendorMasterToMDG(vendorCodes);
console.log(`🕒 최근 수정 데이터 송신 완료`);
@@ -577,35 +650,7 @@ export async function sendRecentModifiedVendorsToMDG(
}
}
-// SOAP 데이터 구조 검증 유틸리티 함수
-export function validateSoapDataStructure(soapData: Record<string, unknown>): {
- isValid: boolean;
- structure: string;
- issues: string[];
-} {
- const issues: string[] = [];
-
- // 올바른 구조인지 확인
- if (!soapData.SUPPLIER_MASTER) {
- issues.push('SUPPLIER_MASTER 필드가 없습니다.');
- }
-
- // P2MD3007_S 중복 확인 (이제는 없어야 함)
- if (soapData.P2MD3007_S) {
- issues.push('P2MD3007_S 래핑이 발견되었습니다. 중복 구조로 인해 문제가 발생할 수 있습니다.');
- }
-
- // 구조 출력
- const structure = JSON.stringify(soapData, null, 2);
-
- return {
- isValid: issues.length === 0,
- structure,
- issues
- };
-}
-
-// 통계 조회 유틸리티 함수
+// 통계 조회
export async function getVendorSendStatistics(): Promise<{
total: number;
modified: number;
@@ -655,106 +700,4 @@ export async function getVendorSendStatistics(): Promise<{
}
}
-// 테스트용 폼 데이터 송신 함수 (SOAP 라이브러리 사용)
-export async function sendTestVendorDataToMDG(formData: Record<string, string>): Promise<{
- success: boolean;
- message: string;
- responseData?: unknown;
-}> {
- try {
- console.log('🧪 테스트용 VENDOR 데이터 SOAP 송신 시작');
-
- // CSV 파일 동적 로드 (더 안전함)
- let csvFields: CsvField[] = [];
- try {
- const csvRaw = fs.readFileSync(CSV_PATH, 'utf-8');
- csvFields = parseCsv(csvRaw);
- } catch (e) {
- console.error('CSV 로딩 실패:', e);
- return {
- success: false,
- message: 'CSV 필드 정의 파일을 로드할 수 없습니다.'
- };
- }
-
- // 필수 필드 검증
- const requiredFields = csvFields.filter(f => f.mandatory).map(f => f.field);
- const missingFields = requiredFields.filter(field => !formData[field]?.trim());
-
- if (missingFields.length > 0) {
- return {
- success: false,
- message: `필수 필드가 누락되었습니다: ${missingFields.join(', ')}`
- };
- }
-
- // 필드 순서에 따라 데이터 생성
- const seen = new Set<string>();
- const uniqueFields = csvFields.filter(f => {
- if (seen.has(f.field)) return false;
- seen.add(f.field);
- return true;
- });
-
- const supplierMaster: Record<string, string> = {};
- uniqueFields.forEach(f => {
- supplierMaster[f.field] = formData[f.field] ?? '';
- });
-
- // SOAP 요청 구조 생성
- const soapData = {
- SUPPLIER_MASTER: supplierMaster
- };
-
- console.log('📄 테스트 SOAP 데이터 생성 완료');
-
- // 데이터 구조 검증 (디버깅용)
- const validation = validateSoapDataStructure(soapData);
- if (!validation.isValid) {
- console.warn('⚠️ 테스트 데이터 구조 문제:', validation.issues);
- return {
- success: false,
- message: `데이터 구조 오류: ${validation.issues.join(', ')}`
- };
- } else {
- console.log('✅ 테스트 데이터 구조 검증 통과');
- }
-
- // SOAP 클라이언트로 요청 전송
- const responseData = await withSoapLogging(
- 'OUTBOUND',
- 'MDG',
- 'IF_MDZ_EVCP_VENDOR_MASTER_TEST',
- JSON.stringify(soapData),
- async () => {
- const client = await createSoapClient();
-
- return new Promise<SoapResponse>((resolve, reject) => {
- client.P2MD3007_AO(soapData, (err: SoapError | null, result: SoapResponse) => {
- if (err) {
- reject(err);
- } else {
- console.log('✅ 테스트 MDG 전송 성공');
- resolve(result);
- }
- });
- });
- }
- );
-
- return {
- success: true,
- message: '테스트 송신이 완료되었습니다.',
- responseData
- };
-
- } catch (error) {
- console.error('❌ 테스트 송신 실패:', error);
- return {
- success: false,
- message: error instanceof Error ? error.message : 'Unknown error'
- };
- }
-}
-
diff --git a/lib/soap/mdg/utils.ts b/lib/soap/mdg/utils.ts
index 437988dc..02dd088e 100644
--- a/lib/soap/mdg/utils.ts
+++ b/lib/soap/mdg/utils.ts
@@ -447,4 +447,51 @@ export async function withSoapLogging<T>(
throw error;
}
+}
+
+/**
+ * SOAP 로그 단순 기록 함수 (이미 완료된 작업에 대한 로깅)
+ * @param direction 수신/송신 구분
+ * @param system 시스템명
+ * @param interfaceName 인터페이스명
+ * @param requestData 요청 XML 데이터
+ * @param responseData 응답 XML 데이터 (선택사항)
+ * @param isSuccess 성공 여부
+ * @param errorMessage 에러 메시지 (실패시)
+ */
+export async function logSoapExecution(
+ direction: LogDirection,
+ system: string,
+ interfaceName: string,
+ requestData: string,
+ responseData?: string,
+ isSuccess: boolean = true,
+ errorMessage?: string
+): Promise<void> {
+ try {
+ const logData: SoapLogInsert = {
+ direction,
+ system,
+ interface: interfaceName,
+ startedAt: new Date(),
+ endedAt: new Date(),
+ isSuccess,
+ requestData,
+ responseData: responseData || null,
+ errorMessage: errorMessage || null,
+ };
+
+ await db.insert(soapLogs).values(logData);
+
+ console.log(`📝 SOAP 로그 기록 완료 [${direction}] ${system}/${interfaceName} - 성공: ${isSuccess}`);
+
+ // 로그 정리 (백그라운드)
+ cleanupOldSoapLogs().catch(error =>
+ console.error('백그라운드 로그 정리 실패:', error)
+ );
+
+ } catch (error) {
+ console.error('SOAP 로그 기록 실패:', error);
+ // 로그 기록 실패는 메인 로직에 영향을 주지 않도록 throw 하지 않음
+ }
} \ No newline at end of file