1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
|
'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<string, unknown>,
prefix: string
): Record<string, unknown> {
return {
'soap:Envelope': {
'@_xmlns:soap': 'http://schemas.xmlsoap.org/soap/envelope/',
[`@_xmlns:${prefix}`]: namespace, // 동적 접두사 생성
'soap:Body': bodyContent
}
};
}
// XML 생성
export async function generateSoapXml(
envelope: Record<string, unknown>,
xmlDeclaration: string = '<?xml version="1.0" encoding="UTF-8"?>\n'
): Promise<string> {
const builder = createXmlBuilder();
const xmlBody = builder.build(envelope);
return xmlDeclaration + xmlBody;
}
// SOAP XML 전송 공통 함수
export async function sendSoapXml(
config: SoapSendConfig,
logInfo: SoapLogInfo,
auth?: SoapAuthConfig
): Promise<SoapSendResult> {
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<string, string> = {
'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<string, unknown>;
const bodyObj = envelopeObj['soap:Envelope'] as Record<string, unknown> | undefined;
if (bodyObj && typeof bodyObj === 'object') {
const soapBody = bodyObj['soap:Body'] as Record<string, unknown> | 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<string, unknown>)['@_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<string, string> = {};
response.headers.forEach((value, key) => {
responseHeadersDebug[key] = value;
});
debugLog('📥 SOAP 응답 수신:', {
status: response.status,
statusText: response.statusText,
headers: responseHeadersDebug,
bodyLength: responseText.length
});
debugLog('🔍 응답 바디 (전체):', responseText);
// SAP 응답 에러 체크: <MSGTY>E</MSGTY> 또는 <EV_TYPE>E</EV_TYPE> 패턴 감지
const hasSapError = responseText.includes('<MSGTY>E</MSGTY>') ||
responseText.includes('<EV_TYPE>E</EV_TYPE>');
// HTTP 상태 코드가 비정상이거나 SOAP Fault 또는 SAP 에러 포함 시 실패로 처리
if (!response.ok ||
responseText.includes('soap:Fault') ||
responseText.includes('SOAP:Fault') ||
hasSapError) {
const responseHeaders: Record<string, string> = {};
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>(.*?)<\/MSGTXT>/);
const evMessageMatch = responseText.match(/<EV_MESSAGE>(.*?)<\/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>(.*?)<\/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<string, string> = {};
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<SoapSendResult> {
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<string, unknown>,
logInfo: SoapLogInfo,
options?: {
namespace?: string;
prefix?: string;
soapAction?: string;
auth?: SoapAuthConfig;
timeout?: number;
}
): Promise<SoapSendResult> {
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<string, unknown>,
logInfo: SoapLogInfo,
auth?: SoapAuthConfig
): Promise<SoapSendResult> {
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);
}
|