summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.development20
-rw-r--r--.env.production6
-rw-r--r--app/[lng]/admin/mdg/page.tsx57
-rw-r--r--app/api/mdg/send-vendor-xml/route.ts28
-rw-r--r--app/api/mdg/send-vendor/route.ts28
-rw-r--r--lib/soap/mdg/send/vendor-master/action.ts193
6 files changed, 260 insertions, 72 deletions
diff --git a/.env.development b/.env.development
index 8d5b04b8..fe5f5342 100644
--- a/.env.development
+++ b/.env.development
@@ -33,14 +33,14 @@ SEDP_API_USER_ID=EVCPUSER
SEDP_API_PASSWORD=evcpusr@2025
# Oracle DB 연결 설정 (개발용 - 로컬 컨테이너)
-# ORACLE_USER=system
-# ORACLE_PASSWORD=oracle
-# ORACLE_CONNECTION_STRING=localhost:1521/XEPDB1
+ORACLE_USER=system
+ORACLE_PASSWORD=oracle
+ORACLE_CONNECTION_STRING=localhost:1521/XEPDB1
# Oracle DB 연결 설정 (SHI 품질)
-ORACLE_USER=shievcp
-ORACLE_PASSWORD=evp_2025
-ORACLE_CONNECTION_STRING=60.100.89.191:7971/SEVMQ
+# ORACLE_USER=shievcp
+# ORACLE_PASSWORD=evp_2025
+# ORACLE_CONNECTION_STRING=60.100.89.191:7971/SEVMQ
# 기본 DOLCE 동기화 값
@@ -71,7 +71,7 @@ OCR_SECRET_KEY=QVZzbkFtVFV1UWl2THNCY01lYVVGUUxpWmdyUkxHYVA=
SAML_MOCKING_IDP=true
# ! SSO Redirect 주소로 활용되며, 상단에서 적절한 URL을 쓴다면 이 변수는 주석처리할 것
-# NEXTAUTH_URL="http://localhost:3000"
+NEXTAUTH_URL="http://localhost:3000"
# SAML 2.0 SP로서 신청할 때 기입하는 사항
# 메타데이터 XML에서 추출 가능하나, 개발 편의성을 위해 추출로직 제거하고 환경변수에 하드코딩함
@@ -116,5 +116,11 @@ J7n0UsGgLd+uUDeo2nLqq5KeaXNcmACVcy2AASog78dzKwQmmGuC9Rp3zIoKOGdo
QwIDAQAB"
# === SOAP 인터페이스 설정 ===
+# MDG SOAP 인증 정보 (개발/품질/운영 비밀번호가 다름)
+MDG_SOAP_AUTH_TYPE=wssecurity # 'basic' 또는 'wssecurity'
+MDG_SOAP_USERNAME=P2038_01 # 개발/품질/운영 공통
+# MDG_SOAP_PASSWORD=STG4857602 # 개발
+MDG_SOAP_PASSWORD=SEW2765890 # 품질
+# MDG_SOAP_PASSWORD=POI9807861 # 운영
SOAP_LOG_MAX_RECORDS=500
# === SOAP 인터페이스 설정 === \ No newline at end of file
diff --git a/.env.production b/.env.production
index 47f80e16..bff3c24c 100644
--- a/.env.production
+++ b/.env.production
@@ -124,5 +124,11 @@ J7n0UsGgLd+uUDeo2nLqq5KeaXNcmACVcy2AASog78dzKwQmmGuC9Rp3zIoKOGdo
QwIDAQAB"
# === SOAP 인터페이스 설정 ===
+# MDG SOAP 인증 정보 (개발/품질/운영 비밀번호가 다름)
+MDG_SOAP_AUTH_TYPE=wssecurity # 'basic' 또는 'wssecurity'
+MDG_SOAP_USERNAME=P2038_01 # 개발/품질/운영 공통
+# MDG_SOAP_PASSWORD=STG4857602 # 개발
+MDG_SOAP_PASSWORD=SEW2765890 # 품질
+# MDG_SOAP_PASSWORD=POI9807861 # 운영
SOAP_LOG_MAX_RECORDS=500
# === SOAP 인터페이스 설정 === \ No newline at end of file
diff --git a/app/[lng]/admin/mdg/page.tsx b/app/[lng]/admin/mdg/page.tsx
index 27416f25..e2926deb 100644
--- a/app/[lng]/admin/mdg/page.tsx
+++ b/app/[lng]/admin/mdg/page.tsx
@@ -61,6 +61,7 @@ const sampleDefaults: Record<string, string> = {
E_ADDRESS: 'contact@test.vendor.com',
BP_TX_TYP: 'KR2',
TAXNUM: '123-45-67890',
+ AD_CONSNO: '1',
};
// XML escape helper
@@ -99,6 +100,25 @@ export default function MDGTestPage() {
load();
}, []);
+ // XML 생성 유틸리티 (폼 데이터 -> SOAP Envelope)
+ const buildEnvelopeXml = (currentForm: Record<string, string>, defs: VendorFieldDef[]) => {
+ if (defs.length === 0) return '';
+ const bodyContent = defs.map((f) => {
+ const val = currentForm[f.name] ?? '';
+ return `<${f.name}>${escapeXml(val)}</${f.name}>`;
+ }).join('\n ');
+
+ const supplierXml = `<SUPPLIER_MASTER>\n ${bodyContent}\n </SUPPLIER_MASTER>`;
+
+ return `<?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>`;
+ };
+
+ // 폼 데이터 변경 시 실시간 XML 생성
+ useEffect(() => {
+ const xml = buildEnvelopeXml(formData, fieldDefs);
+ setResultXml(xml);
+ }, [formData, fieldDefs]);
+
// 폼 데이터 업데이트
const updateField = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
@@ -114,33 +134,42 @@ export default function MDGTestPage() {
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(', ')}`);
+ setIsLoading(false);
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>`;
+ // 서버 API 호출해 송신
+ const res = await fetch('/api/mdg/send-vendor-xml', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ envelope: resultXml }),
+ });
+
+ const json = await res.json();
+
+ if (!res.ok || !json.success) {
+ // 상세 오류 메시지 추출 (vendorCode 기반 또는 직접 오류 메시지)
+ const detailMsg = json?.results?.[0]?.error ?? json?.message ?? json?.responseText ?? '송신 실패';
+ toast.error(`송신 실패: ${detailMsg}`);
+ setIsLoading(false);
+ return;
+ }
- 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>`;
+ toast.success('MDG 송신이 완료되었습니다.');
- setResultXml(envelope);
- toast.success('요청 XML이 생성되었습니다. 하단 영역을 확인하세요.');
-
} catch (error) {
console.error('테스트 송신 실패:', error);
toast.error('테스트 송신 중 오류가 발생했습니다.');
diff --git a/app/api/mdg/send-vendor-xml/route.ts b/app/api/mdg/send-vendor-xml/route.ts
new file mode 100644
index 00000000..7f8d1daf
--- /dev/null
+++ b/app/api/mdg/send-vendor-xml/route.ts
@@ -0,0 +1,28 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { sendVendorEnvelopeToMDG } from '@/lib/soap/mdg/send/vendor-master/action';
+
+export async function POST(request: NextRequest) {
+ try {
+ const { envelope } = await request.json();
+
+ if (!envelope || typeof envelope !== 'string') {
+ return NextResponse.json(
+ { success: false, message: 'envelope(XML) is required' },
+ { status: 400 }
+ );
+ }
+
+ const result = await sendVendorEnvelopeToMDG(envelope);
+
+ return NextResponse.json(result);
+ } catch (error) {
+ console.error('[send-vendor-xml] error:', error);
+ return NextResponse.json(
+ {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/mdg/send-vendor/route.ts b/app/api/mdg/send-vendor/route.ts
new file mode 100644
index 00000000..2442b733
--- /dev/null
+++ b/app/api/mdg/send-vendor/route.ts
@@ -0,0 +1,28 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { sendSingleVendorToMDG } from '@/lib/soap/mdg/send/vendor-master/action';
+
+export async function POST(request: NextRequest) {
+ try {
+ const { vendorCode } = await request.json();
+
+ if (!vendorCode || typeof vendorCode !== 'string') {
+ return NextResponse.json(
+ { success: false, message: 'vendorCode is required' },
+ { status: 400 }
+ );
+ }
+
+ const result = await sendSingleVendorToMDG(vendorCode);
+
+ return NextResponse.json(result);
+ } catch (error) {
+ console.error('[send-vendor] error:', error);
+ return NextResponse.json(
+ {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ );
+ }
+} \ 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
index 34ce242c..e96b93fc 100644
--- a/lib/soap/mdg/send/vendor-master/action.ts
+++ b/lib/soap/mdg/send/vendor-master/action.ts
@@ -18,11 +18,17 @@ 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 엔드포인트 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";
+// WSDL 파일 경로
+const WSDL_PATH = path.join(process.cwd(), 'public', 'wsdl', 'P2MD3007_AO.wsdl');
+
+// 환경변수에서 인증 정보 가져오기
+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 파싱 및 필드 정의 ------------------------------------------------
interface CsvField {
@@ -53,17 +59,57 @@ try {
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;
- }
-});
+// 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);
+ }
+ } else {
+ console.warn('⚠️ MDG SOAP 인증 정보가 환경변수에 설정되지 않았습니다.');
+ }
+
+ resolve(client);
+ });
+ });
+}
+
+// SOAP 응답 타입 정의
+interface SoapResponse {
+ [key: string]: unknown;
+}
+
+// SOAP 오류 타입 정의
+interface SoapError {
+ message: string;
+ body?: string;
+ statusCode?: number;
+}
// VENDOR 마스터 데이터를 MDG로 송신하는 액션
export async function sendVendorMasterToMDG(vendorCodes: string[]): Promise<{
@@ -93,39 +139,29 @@ export async function sendVendorMasterToMDG(vendorCodes: string[]): Promise<{
continue;
}
- // XML 생성
- const soapXml = buildSoapXML(vendorData);
- console.log(`📄 VENDOR ${vendorCode} XML 생성 완료`);
+ // SOAP 요청 데이터 생성
+ const soapData = buildSoapData(vendorData);
+ console.log(`📄 VENDOR ${vendorCode} SOAP 데이터 생성 완료`);
- // SOAP 요청 전송
+ // SOAP 클라이언트로 요청 전송
await withSoapLogging(
'OUTBOUND',
'MDG',
'IF_MDZ_EVCP_VENDOR_MASTER',
- soapXml,
+ JSON.stringify(soapData),
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 client = await createSoapClient();
- 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;
+ 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);
+ }
+ });
+ });
}
);
@@ -229,8 +265,8 @@ async function fetchVendorData(vendorCode: string) {
}
}
-// SOAP XML 생성 (WSDL 구조에 맞춤)
-function buildSoapXML(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVendorData>>>): string {
+// SOAP 데이터 생성 (WSDL 구조에 맞춤)
+function buildSoapData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVendorData>>>) {
const { vendorHeader, addresses, adEmails, adFaxes, adPostals, adTels, adUrls, bpTaxnums, bpVengens } = vendorData;
// 값 추출 매핑 ------------------------------------
@@ -262,7 +298,7 @@ function buildSoapXML(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVen
// 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;
@@ -270,16 +306,17 @@ function buildSoapXML(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVen
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>`;
+ const supplierMaster: Record<string, any> = {};
+ uniqueFields.forEach(f => {
+ supplierMaster[f.field] = mapping[f.field] ?? '';
+ });
- return soapEnvelope.trim();
+ // SOAP 요청 구조 생성
+ return {
+ P2MD3007_S: {
+ SUPPLIER_MASTER: supplierMaster
+ }
+ };
}
// 특정 VENDOR만 송신하는 유틸리티 함수
@@ -582,3 +619,57 @@ export async function getVendorSendStatistics(): Promise<{
throw error;
}
}
+
+// 직접 XML 전송 함수 (기존 호환성 유지)
+export async function sendVendorEnvelopeToMDG(envelope: string): Promise<{
+ success: boolean;
+ message: string;
+ responseText?: string;
+}> {
+ try {
+ const responseText = await withSoapLogging(
+ 'OUTBOUND',
+ 'MDG',
+ 'IF_MDZ_EVCP_VENDOR_MASTER_TEST', // 테스트용 인터페이스명
+ envelope,
+ async () => {
+ // 직접 XML 전송의 경우 기존 fetch 방식 유지
+ 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";
+
+ const res = await fetch(MDG_ENDPOINT_URL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/xml; charset=utf-8',
+ 'SOAPAction': 'http://sap.com/xi/WebService/soap1.1',
+ },
+ body: envelope,
+ });
+
+ const text = await res.text();
+
+ 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;
+ }
+ );
+
+ return {
+ success: true,
+ message: '전송 성공',
+ responseText,
+ };
+ } catch (error) {
+ console.error('XML 전송 실패:', error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+}