diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-18 18:34:52 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-18 18:34:52 +0900 |
| commit | 76a6606def50caa4df28014b869a06e5da30ab18 (patch) | |
| tree | d1dd318b959c91dad3be22c7a60383da434a4ad7 /lib/soap | |
| parent | c7d76d044531aab65dde9ba1007f3b2d86da6326 (diff) | |
(김준회) 견적: PO 생성 요청부 개선, 에러처리
Diffstat (limited to 'lib/soap')
| -rw-r--r-- | lib/soap/ecc/send/create-po.ts | 49 | ||||
| -rw-r--r-- | lib/soap/sender.ts | 103 | ||||
| -rw-r--r-- | lib/soap/types.ts | 64 | ||||
| -rw-r--r-- | lib/soap/utils.ts | 26 |
4 files changed, 146 insertions, 96 deletions
diff --git a/lib/soap/ecc/send/create-po.ts b/lib/soap/ecc/send/create-po.ts index 0984a208..1e21d39c 100644 --- a/lib/soap/ecc/send/create-po.ts +++ b/lib/soap/ecc/send/create-po.ts @@ -44,23 +44,11 @@ export interface POItemData { EBELP?: string; // Series PO Item Seq } -// PR 반환 데이터 타입 -export interface PRReturnData { - ANFNR: string; // PR Request Number (M) - ANFPS: string; // Item Number of PR Request (M) - EBELN: string; // Purchase Requisition Number (M) - EBELP: string; // Item Number of Purchase Requisition (M) - MSGTY: string; // Message Type (M) - MSGTXT?: string; // Message Text -} - // PO 생성 요청 데이터 타입 +// 참고: T_PR_RETURN, EV_ERDAT, EV_ERZET는 응답용 필드이므로 요청에 포함하지 않음 export interface POCreateRequest { T_Bidding_HEADER: POHeaderData[]; T_Bidding_ITEM: POItemData[]; - T_PR_RETURN: PRReturnData[]; - EV_ERDAT?: string; // Extract Date - EV_ERZET?: string; // Extract Time } @@ -72,10 +60,8 @@ function createPOSoapBodyContent(poData: POCreateRequest): Record<string, unknow return { 'p1:MT_P2MM3015_S': { // WSDL에서 사용하는 p1 접두사 적용 'T_Bidding_HEADER': poData.T_Bidding_HEADER, - 'T_Bidding_ITEM': poData.T_Bidding_ITEM, - 'T_PR_RETURN': poData.T_PR_RETURN, - ...(poData.EV_ERDAT && { 'EV_ERDAT': poData.EV_ERDAT }), - ...(poData.EV_ERZET && { 'EV_ERZET': poData.EV_ERZET }) + 'T_Bidding_ITEM': poData.T_Bidding_ITEM + // T_PR_RETURN, EV_ERDAT, EV_ERZET는 응답용 필드이므로 요청에 포함하지 않음 } }; } @@ -112,19 +98,7 @@ function validatePOData(poData: POCreateRequest): { isValid: boolean; errors: st }); } - // PR 반환 데이터 검증 - if (!poData.T_PR_RETURN || poData.T_PR_RETURN.length === 0) { - errors.push('T_PR_RETURN은 필수입니다.'); - } else { - poData.T_PR_RETURN.forEach((prReturn, index) => { - const requiredFields = ['ANFNR', 'ANFPS', 'EBELN', 'EBELP', 'MSGTY']; - requiredFields.forEach(field => { - if (!prReturn[field as keyof PRReturnData]) { - errors.push(`T_PR_RETURN[${index}].${field}는 필수입니다.`); - } - }); - }); - } + // T_PR_RETURN은 응답용 필드이므로 검증하지 않음 return { isValid: errors.length === 0, @@ -165,7 +139,7 @@ async function sendPOToECC(poData: POCreateRequest): Promise<SoapSendResult> { }; console.log(`📤 PO 생성 요청 전송 시작 - ANFNR: ${poData.T_Bidding_HEADER[0]?.ANFNR}`); - console.log(`🔍 헤더 ${poData.T_Bidding_HEADER.length}개, 아이템 ${poData.T_Bidding_ITEM.length}개, PR 반환 ${poData.T_PR_RETURN.length}개`); + console.log(`🔍 헤더 ${poData.T_Bidding_HEADER.length}개, 아이템 ${poData.T_Bidding_ITEM.length}개`); // SOAP XML 전송 const result = await sendSoapXml(config, logInfo); @@ -341,17 +315,8 @@ export async function createTestPurchaseOrder(): Promise<{ LFDAT: getCurrentSAPDate(), ZCON_NO_PO: 'CON001', EBELP: '00001' - }], - T_PR_RETURN: [{ - ANFNR: 'TEST001', - ANFPS: '00001', - EBELN: 'PR001', - EBELP: '00001', - MSGTY: 'S', - MSGTXT: 'Test message' - }], - EV_ERDAT: getCurrentSAPDate(), - EV_ERZET: getCurrentSAPTime() + }] + // T_PR_RETURN, EV_ERDAT, EV_ERZET는 응답용 필드이므로 요청에 포함하지 않음 }; const result = await sendPOToECC(testPOData); diff --git a/lib/soap/sender.ts b/lib/soap/sender.ts index d12665cb..c0be780d 100644 --- a/lib/soap/sender.ts +++ b/lib/soap/sender.ts @@ -3,43 +3,13 @@ import { withSoapLogging } from "@/lib/soap/utils"; import { XMLBuilder } from 'fast-xml-parser'; import { debugLog, debugError, debugWarn, debugSuccess } from '@/lib/debug-utils'; - -// 기본 인증 정보 타입 -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>; -} +import type { + SoapAuthConfig, + SoapSendConfig, + SoapLogInfo, + SoapSendResult +} from './types'; +import { SoapResponseError } from './types'; // 기본 환경변수에서 인증 정보 가져오기 function getDefaultAuth(): SoapAuthConfig { @@ -154,6 +124,7 @@ export async function sendSoapXml( } } + let responseText = ''; const result = await withSoapLogging( logInfo.direction, logInfo.system, @@ -161,17 +132,17 @@ export async function sendSoapXml( xmlData, async () => { // 타임아웃 설정 + let response: Response; if (config.timeout) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), config.timeout); try { - const response = await fetch(config.endpoint, { + response = await fetch(config.endpoint, { ...fetchOptions, signal: controller.signal }); clearTimeout(timeoutId); - return response; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { @@ -180,14 +151,20 @@ export async function sendSoapXml( throw error; } } else { - return await fetch(config.endpoint, fetchOptions); + response = await fetch(config.endpoint, fetchOptions); } - } + + // 응답 텍스트 읽기 (로깅을 위해 여기서 읽음) + responseText = await response.text(); + + return response; + }, + // 응답 데이터 추출 함수: responseText를 반환 + () => responseText ); // 응답 처리 const response = result as Response; - const responseText = await response.text(); // 응답 헤더 수집 (디버깅용) const responseHeadersDebug: Record<string, string> = {}; @@ -203,22 +180,52 @@ export async function sendSoapXml( }); debugLog('🔍 응답 바디 (전체):', responseText); - // HTTP 상태 코드가 비정상이거나 SOAP Fault 포함 시 실패로 처리하되 본문을 그대로 반환 - if (!response.ok || responseText.includes('soap:Fault') || responseText.includes('SOAP:Fault')) { + // 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; }); - return { - success: false, - message: !response.ok ? `HTTP ${response.status}: ${response.statusText}` : 'SOAP Fault', + + // 에러 메시지 결정 및 상세 메시지 추출 + 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 - }; + }); } // 응답 헤더 수집 diff --git a/lib/soap/types.ts b/lib/soap/types.ts new file mode 100644 index 00000000..dac8f83b --- /dev/null +++ b/lib/soap/types.ts @@ -0,0 +1,64 @@ +// SOAP 관련 타입 정의 + +// 기본 인증 정보 타입 +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>; +} + +// SOAP 에러 타입 (응답 정보 포함) +export class SoapResponseError extends Error { + responseText?: string; + statusCode?: number; + headers?: Record<string, string>; + endpoint?: string; + requestXml?: string; + requestHeaders?: Record<string, string>; + + constructor(message: string, details?: { + responseText?: string; + statusCode?: number; + headers?: Record<string, string>; + endpoint?: string; + requestXml?: string; + requestHeaders?: Record<string, string>; + }) { + super(message); + this.name = 'SoapResponseError'; + if (details) { + Object.assign(this, details); + } + } +} + diff --git a/lib/soap/utils.ts b/lib/soap/utils.ts index 57e3b280..809dd46d 100644 --- a/lib/soap/utils.ts +++ b/lib/soap/utils.ts @@ -466,6 +466,7 @@ export async function cleanupOldSoapLogs(): Promise<void> { * @param interfaceName 인터페이스명 * @param requestData 요청 데이터 * @param processor 실제 비즈니스 로직 함수 + * @param extractResponse 응답 데이터 추출 함수 (선택사항) * @returns 처리 결과 */ export async function withSoapLogging<T>( @@ -473,7 +474,8 @@ export async function withSoapLogging<T>( system: string, interfaceName: string, requestData: string, - processor: () => Promise<T> + processor: () => Promise<T>, + extractResponse?: (result: T) => string | undefined ): Promise<T> { let logId: number | null = null; @@ -484,10 +486,16 @@ export async function withSoapLogging<T>( // 2. 실제 처리 실행 const result = await processor(); - // 3. 성공 로그 완료 - await completeSoapLog(logId, true); + // 3. 응답 데이터 추출 (제공된 경우) + let responseData: string | undefined; + if (extractResponse) { + responseData = extractResponse(result); + } + + // 4. 성공 로그 완료 (응답 데이터 포함) + await completeSoapLog(logId, true, responseData); - // 4. 로그 정리 (백그라운드) + // 5. 로그 정리 (백그라운드) cleanupOldSoapLogs().catch(error => console.error('백그라운드 로그 정리 실패:', error) ); @@ -495,12 +503,18 @@ export async function withSoapLogging<T>( return result; } catch (error) { - // 5. 실패 로그 완료 + // 6. 실패 로그 완료 if (logId !== null) { + // 에러 객체에 응답 데이터가 포함되어 있는지 확인 + let errorResponseData: string | undefined; + if (error && typeof error === 'object' && 'responseText' in error) { + errorResponseData = (error as { responseText?: string }).responseText; + } + await completeSoapLog( logId, false, - undefined, + errorResponseData, error instanceof Error ? error.message : 'Unknown error' ); } |
