summaryrefslogtreecommitdiff
path: root/lib/soap/sender.ts
blob: 1dfc87301786ad28b10f31a224f19308fb85623d (plain)
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
'use server'

import { withSoapLogging } from "@/lib/soap/utils";
import { XMLBuilder } from 'fast-xml-parser';

// 기본 인증 정보 타입
export interface SoapAuthConfig {
  username?: string;
  password?: string;
}

// SOAP 전송 설정 타입
export interface SoapSendConfig {
  endpoint: string;
  envelope: Record<string, unknown>;
  soapAction?: string;
  timeout?: number;
  retryCount?: number;
  retryDelay?: number;
  namespace?: string; // 네임스페이스를 동적으로 설정할 수 있도록 추가
  prefix: string; // 네임스페이스 접두사 (필수)
}

// 로깅 정보 타입
export interface SoapLogInfo {
  direction: 'INBOUND' | 'OUTBOUND';
  system: string;
  interface: string;
}

// 전송 결과 타입
export interface SoapSendResult {
  success: boolean;
  message: string;
  responseText?: string;
  statusCode?: number;
  headers?: Record<string, string>;
  endpoint?: string;
  requestXml?: string;
  requestHeaders?: Record<string, string>;
}

// 기본 환경변수에서 인증 정보 가져오기
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> {
  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
    );
    
    const xmlData = await generateSoapXml(soapEnvelope);
    
    console.log('📤 SOAP XML 전송 시작');
    console.log('🔍 전송 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}`;
      console.log('🔐 Basic Authentication 헤더 추가 완료');
    } else {
      console.warn('⚠️ SOAP 인증 정보가 설정되지 않았습니다.');
    }

    const fetchOptions: RequestInit = {
      method: 'POST',
      headers: requestHeaders,
      body: xmlData,
    };

    // Body 루트 요소(p1:MT_...)에 기본 네임스페이스를 부여하여 하위 무접두사 요소들도 동일 네임스페이스로 인식되도록 처리
    const envelopeObj = soapEnvelope as Record<string, any>;
    const bodyObj = envelopeObj['soap:Envelope']?.['soap:Body'] as Record<string, any> | undefined;
    if (bodyObj && typeof bodyObj === 'object') {
      const rootKeys = Object.keys(bodyObj);
      if (rootKeys.length > 0) {
        const rootKey = rootKeys[0];
        if (bodyObj[rootKey] && typeof bodyObj[rootKey] === 'object') {
          bodyObj[rootKey]['@_xmlns'] = namespace;
        }
      }
    }

    const result = await withSoapLogging(
      logInfo.direction,
      logInfo.system,
      logInfo.interface,
      xmlData,
      async () => {
        // 타임아웃 설정
        if (config.timeout) {
          const controller = new AbortController();
          const timeoutId = setTimeout(() => controller.abort(), config.timeout);
          
          try {
            const response = await fetch(config.endpoint, {
              ...fetchOptions,
              signal: controller.signal
            });
            clearTimeout(timeoutId);
            return response;
          } catch (error) {
            clearTimeout(timeoutId);
            if (error instanceof Error && error.name === 'AbortError') {
              throw new Error(`요청 타임아웃 (${config.timeout}ms)`);
            }
            throw error;
          }
        } else {
          return await fetch(config.endpoint, fetchOptions);
        }
      }
    );

    // 응답 처리
    const response = result as Response;
    const responseText = await response.text();
    
    console.log('📥 SOAP 응답 수신:', response.status, response.statusText);
    console.log('🔍 응답 XML (첫 500자):', responseText.substring(0, 500));

    // HTTP 상태 코드가 비정상이거나 SOAP Fault 포함 시 실패로 처리하되 본문을 그대로 반환
    if (!response.ok || responseText.includes('soap:Fault') || responseText.includes('SOAP:Fault')) {
      const responseHeaders: Record<string, string> = {};
      response.headers.forEach((value, key) => {
        responseHeaders[key] = value;
      });
      return {
        success: false,
        message: !response.ok ? `HTTP ${response.status}: ${response.statusText}` : 'SOAP Fault',
        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) {
    console.error('❌ SOAP XML 전송 실패:', error);
    return {
      success: false,
      message: error instanceof Error ? error.message : 'Unknown error'
    };
  }
}

// 재시도 로직이 포함된 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 {
      console.log(`🔄 SOAP 전송 시도 ${attempt}/${maxRetries}`);
      
      const result = await sendSoapXml(config, logInfo, auth);
      
      if (result.success) {
        return result;
      }
      
      // 마지막 시도가 아니면 재시도
      if (attempt < maxRetries) {
        console.log(`⏳ ${retryDelay}ms 후 재시도...`);
        await new Promise(resolve => setTimeout(resolve, retryDelay));
      }
      
    } catch (error) {
      console.error(`❌ SOAP 전송 시도 ${attempt} 실패:`, error);
      
      if (attempt === maxRetries) {
        return {
          success: false,
          message: `최대 재시도 횟수 초과: ${error instanceof Error ? error.message : 'Unknown error'}`
        };
      }
      
      // 마지막 시도가 아니면 재시도
      console.log(`⏳ ${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);
}