summaryrefslogtreecommitdiff
path: root/lib/soap
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-18 18:34:52 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-18 18:34:52 +0900
commit76a6606def50caa4df28014b869a06e5da30ab18 (patch)
treed1dd318b959c91dad3be22c7a60383da434a4ad7 /lib/soap
parentc7d76d044531aab65dde9ba1007f3b2d86da6326 (diff)
(김준회) 견적: PO 생성 요청부 개선, 에러처리
Diffstat (limited to 'lib/soap')
-rw-r--r--lib/soap/ecc/send/create-po.ts49
-rw-r--r--lib/soap/sender.ts103
-rw-r--r--lib/soap/types.ts64
-rw-r--r--lib/soap/utils.ts26
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'
);
}