diff options
| -rw-r--r-- | .env.development | 20 | ||||
| -rw-r--r-- | .env.production | 6 | ||||
| -rw-r--r-- | app/[lng]/admin/mdg/page.tsx | 57 | ||||
| -rw-r--r-- | app/api/mdg/send-vendor-xml/route.ts | 28 | ||||
| -rw-r--r-- | app/api/mdg/send-vendor/route.ts | 28 | ||||
| -rw-r--r-- | lib/soap/mdg/send/vendor-master/action.ts | 193 |
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 '<'; - case '>': return '>'; - case '&': return '&'; - case '"': return '"'; - case "'": return '''; - 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', + }; + } +} |
