'use server' import { withSoapLogging } from "@/lib/soap/utils"; import { XMLBuilder } from 'fast-xml-parser'; import { debugLog, debugError, debugWarn, debugSuccess } from '@/lib/debug-utils'; import type { SoapAuthConfig, SoapSendConfig, SoapLogInfo, SoapSendResult } from './types'; import { SoapResponseError } from './types'; // 기본 환경변수에서 인증 정보 가져오기 function getDefaultAuth(): SoapAuthConfig { return { username: process.env.MDG_SOAP_USERNAME, password: process.env.MDG_SOAP_PASSWORD }; } // 공통 XML 빌더 생성 function createXmlBuilder() { return new XMLBuilder({ ignoreAttributes: false, format: true, attributeNamePrefix: '@_', textNodeName: '#text', suppressEmptyNode: true, suppressUnpairedNode: false, indentBy: ' ', processEntities: false, suppressBooleanAttributes: false, cdataPropName: false, tagValueProcessor: (name, val) => val, attributeValueProcessor: (name, val) => val }); } // SOAP Envelope 생성 function createSoapEnvelope( namespace: string, bodyContent: Record, prefix: string ): Record { return { 'soap:Envelope': { '@_xmlns:soap': 'http://schemas.xmlsoap.org/soap/envelope/', [`@_xmlns:${prefix}`]: namespace, // 동적 접두사 생성 'soap:Body': bodyContent } }; } // XML 생성 export async function generateSoapXml( envelope: Record, xmlDeclaration: string = '\n' ): Promise { const builder = createXmlBuilder(); const xmlBody = builder.build(envelope); return xmlDeclaration + xmlBody; } // SOAP XML 전송 공통 함수 export async function sendSoapXml( config: SoapSendConfig, logInfo: SoapLogInfo, auth?: SoapAuthConfig ): Promise { let xmlData: string | undefined; try { // 인증 정보 설정 (기본값 사용) const authConfig = auth || getDefaultAuth(); // XML 생성 (네임스페이스와 접두사를 동적으로 설정) const namespace = config.namespace || 'http://shi.samsung.co.kr/P2_MD/MDZ'; const soapEnvelope = createSoapEnvelope( namespace, config.envelope, config.prefix ); xmlData = await generateSoapXml(soapEnvelope); debugLog('📤 SOAP XML 전송 시작'); debugLog('🔍 전송 XML (첫 500자):', xmlData.substring(0, 500)); // 요청 헤더 및 fetch 옵션을 사전에 구성 const requestHeaders: Record = { 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': config.soapAction || 'http://sap.com/xi/WebService/soap1.1', }; // Basic Authentication 헤더 추가 if (authConfig.username && authConfig.password) { const credentials = Buffer.from(`${authConfig.username}:${authConfig.password}`).toString('base64'); requestHeaders['Authorization'] = `Basic ${credentials}`; debugSuccess('🔐 Basic Authentication 헤더 추가 완료'); } else { debugWarn('⚠️ SOAP 인증 정보가 설정되지 않았습니다.'); } const fetchOptions: RequestInit = { method: 'POST', headers: requestHeaders, body: xmlData, }; // Body 루트 요소(p1:MT_...)에 기본 네임스페이스를 부여하여 하위 무접두사 요소들도 동일 네임스페이스로 인식되도록 처리 const envelopeObj = soapEnvelope as Record; const bodyObj = envelopeObj['soap:Envelope'] as Record | undefined; if (bodyObj && typeof bodyObj === 'object') { const soapBody = bodyObj['soap:Body'] as Record | undefined; if (soapBody && typeof soapBody === 'object') { const rootKeys = Object.keys(soapBody); if (rootKeys.length > 0) { const rootKey = rootKeys[0]; const rootValue = soapBody[rootKey]; if (rootValue && typeof rootValue === 'object') { (rootValue as Record)['@_xmlns'] = namespace; } } } } let responseText = ''; const result = await withSoapLogging( logInfo.direction, logInfo.system, logInfo.interface, xmlData, async () => { // 타임아웃 설정 let response: Response; if (config.timeout) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), config.timeout); try { response = await fetch(config.endpoint, { ...fetchOptions, signal: controller.signal }); clearTimeout(timeoutId); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new Error(`요청 타임아웃 (${config.timeout}ms)`); } throw error; } } else { response = await fetch(config.endpoint, fetchOptions); } // 응답 텍스트 읽기 (로깅을 위해 여기서 읽음) responseText = await response.text(); return response; }, // 응답 데이터 추출 함수: responseText를 반환 () => responseText ); // 응답 처리 const response = result as Response; // 응답 헤더 수집 (디버깅용) const responseHeadersDebug: Record = {}; response.headers.forEach((value, key) => { responseHeadersDebug[key] = value; }); debugLog('📥 SOAP 응답 수신:', { status: response.status, statusText: response.statusText, headers: responseHeadersDebug, bodyLength: responseText.length }); debugLog('🔍 응답 바디 (전체):', responseText); // SAP 응답 에러 체크: E 또는 E 패턴 감지 const hasSapError = responseText.includes('E') || responseText.includes('E'); // HTTP 상태 코드가 비정상이거나 SOAP Fault 또는 SAP 에러 포함 시 실패로 처리 if (!response.ok || responseText.includes('soap:Fault') || responseText.includes('SOAP:Fault') || hasSapError) { const responseHeaders: Record = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); // 에러 메시지 결정 및 상세 메시지 추출 let errorMessage = ''; if (!response.ok) { errorMessage = `HTTP ${response.status}: ${response.statusText}`; } else if (hasSapError) { // SAP 응답에서 에러 메시지 추출 시도 const msgtxtMatch = responseText.match(/(.*?)<\/MSGTXT>/); const evMessageMatch = responseText.match(/(.*?)<\/EV_MESSAGE>/); const detailMessage = msgtxtMatch?.[1] || evMessageMatch?.[1] || ''; errorMessage = 'SAP 응답 에러: MSGTY=E 또는 EV_TYPE=E 감지'; if (detailMessage) { errorMessage += ` - ${detailMessage}`; } } else { // SOAP Fault에서 메시지 추출 시도 const faultStringMatch = responseText.match(/(.*?)<\/faultstring>/); const faultMessage = faultStringMatch?.[1] || 'SOAP Fault'; errorMessage = faultMessage; } debugError('❌ SOAP 응답 에러 감지:', errorMessage); // 커스텀 에러 객체 생성 (responseText 포함) throw new SoapResponseError(errorMessage, { responseText, statusCode: response.status, headers: responseHeaders, endpoint: config.endpoint, requestXml: xmlData, requestHeaders }); } // 응답 헤더 수집 const responseHeaders: Record = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); return { success: true, message: '전송 성공', responseText, statusCode: response.status, headers: responseHeaders, endpoint: config.endpoint, requestXml: xmlData, requestHeaders }; } catch (error) { debugError('❌ SOAP XML 전송 실패:', error); return { success: false, message: error instanceof Error ? error.message : 'Unknown error', requestXml: xmlData // 에러가 발생해도 생성된 XML이 있다면 반환 }; } } // 재시도 로직이 포함된 SOAP 전송 함수 export async function sendSoapXmlWithRetry( config: SoapSendConfig, logInfo: SoapLogInfo, auth?: SoapAuthConfig ): Promise { const maxRetries = config.retryCount || 3; const retryDelay = config.retryDelay || 1000; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { debugLog(`🔄 SOAP 전송 시도 ${attempt}/${maxRetries}`); const result = await sendSoapXml(config, logInfo, auth); if (result.success) { return result; } // 마지막 시도가 아니면 재시도 if (attempt < maxRetries) { debugLog(`⏳ ${retryDelay}ms 후 재시도...`); await new Promise(resolve => setTimeout(resolve, retryDelay)); } } catch (error) { debugError(`❌ SOAP 전송 시도 ${attempt} 실패:`, error); if (attempt === maxRetries) { return { success: false, message: `최대 재시도 횟수 초과: ${error instanceof Error ? error.message : 'Unknown error'}` }; } // 마지막 시도가 아니면 재시도 debugLog(`⏳ ${retryDelay}ms 후 재시도...`); await new Promise(resolve => setTimeout(resolve, retryDelay)); } } return { success: false, message: '알 수 없는 오류로 전송 실패' }; } // 간단한 SOAP 전송 함수 (기본 설정 사용) export async function sendSimpleSoapXml( endpoint: string, bodyContent: Record, logInfo: SoapLogInfo, options?: { namespace?: string; prefix?: string; soapAction?: string; auth?: SoapAuthConfig; timeout?: number; } ): Promise { const config: SoapSendConfig = { endpoint, envelope: bodyContent, soapAction: options?.soapAction, timeout: options?.timeout || 30000, // 기본 30초 namespace: options?.namespace, // 네임스페이스 옵션 추가 prefix: options?.prefix || 'p1', // 기본 접두사 p1 }; const auth = options?.auth || getDefaultAuth(); return await sendSoapXml(config, logInfo, auth); } // MDG 전용 SOAP 전송 함수 (기존 action.ts와 호환) export async function sendMdgSoapXml( bodyContent: Record, logInfo: SoapLogInfo, auth?: SoapAuthConfig ): Promise { const config: SoapSendConfig = { endpoint: "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", envelope: bodyContent, soapAction: 'http://sap.com/xi/WebService/soap1.1', timeout: 60000, // MDG는 60초 타임아웃 namespace: 'http://shi.samsung.co.kr/P2_MD/MDZ', // MDG 전용 네임스페이스 명시 prefix: 'p1', // MDG 전용 접두사 }; return await sendSoapXml(config, logInfo, auth); }