diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-03 07:05:05 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-03 07:05:05 +0000 |
| commit | c5fc7195505d82c60fc9dffc3744782b91d7ebb7 (patch) | |
| tree | 588350714f121f8c394a9634fb2d669309420a37 | |
| parent | 0ed64cf896ad5b18dbee958aaa7fec17ffd9dfa9 (diff) | |
(김준회) ECC 송신건 엔드포인트 오류 수정 및 테스트 송신페이지 개선
| -rw-r--r-- | app/[lng]/admin/ecc/page.tsx | 44 | ||||
| -rw-r--r-- | lib/soap/ecc/send/cancel-rfq.ts | 10 | ||||
| -rw-r--r-- | lib/soap/ecc/send/create-po.ts | 10 | ||||
| -rw-r--r-- | lib/soap/ecc/send/pcr-confirm.ts | 10 | ||||
| -rw-r--r-- | lib/soap/ecc/send/rfq-info.ts | 10 | ||||
| -rw-r--r-- | lib/soap/sender.ts | 88 |
6 files changed, 136 insertions, 36 deletions
diff --git a/app/[lng]/admin/ecc/page.tsx b/app/[lng]/admin/ecc/page.tsx index bb3168c4..257ecfea 100644 --- a/app/[lng]/admin/ecc/page.tsx +++ b/app/[lng]/admin/ecc/page.tsx @@ -25,6 +25,11 @@ interface TestResult { responseData?: string timestamp: string duration?: number + statusCode?: number + endpoint?: string + headers?: Record<string, string> + requestXml?: string + requestHeaders?: Record<string, string> } export default function ECCSenderTestPage() { @@ -46,7 +51,12 @@ export default function ECCSenderTestPage() { message: result.message, responseData: result.responseData, timestamp: new Date().toLocaleString('ko-KR'), - duration + duration, + statusCode: result.statusCode, + endpoint: result.endpoint, + headers: result.headers, + requestXml: result.requestXml, + requestHeaders: result.requestHeaders } setTestResults(prev => ({ ...prev, [testName]: testResult })) @@ -681,6 +691,11 @@ function TestResultCard({ result, title }: { result: TestResult; title: string } <XCircle className="h-4 w-4 text-red-600" /> )} <span className="font-semibold">{title}</span> + {typeof result.statusCode !== 'undefined' && ( + <Badge variant="outline" className="text-xs"> + HTTP {result.statusCode} + </Badge> + )} <Badge variant="outline" className="text-xs"> {result.duration}ms </Badge> @@ -691,6 +706,33 @@ function TestResultCard({ result, title }: { result: TestResult; title: string } <AlertDescription className="mt-2"> <div className="space-y-2"> <p>{result.message}</p> + {result.endpoint && ( + <p className="text-xs text-muted-foreground break-all"><span className="font-medium">Endpoint:</span> {result.endpoint}</p> + )} + {result.headers && ( + <details className="text-xs"> + <summary className="cursor-pointer font-medium">응답 헤더 보기</summary> + <pre className="mt-2 p-2 bg-gray-100 rounded text-xs overflow-auto max-h-40"> + {Object.entries(result.headers).map(([k, v]) => `${k}: ${v}`).join('\n')} + </pre> + </details> + )} + {result.requestHeaders && ( + <details className="text-xs"> + <summary className="cursor-pointer font-medium">요청 헤더 보기</summary> + <pre className="mt-2 p-2 bg-gray-100 rounded text-xs overflow-auto max-h-40"> + {Object.entries(result.requestHeaders).map(([k, v]) => `${k}: ${v}`).join('\n')} + </pre> + </details> + )} + {result.requestXml && ( + <details className="text-xs"> + <summary className="cursor-pointer font-medium">요청 XML 보기</summary> + <pre className="mt-2 p-2 bg-gray-100 rounded text-xs overflow-auto max-h-60 whitespace-pre-wrap break-words"> + {result.requestXml} + </pre> + </details> + )} {result.responseData && ( <details className="text-xs"> <summary className="cursor-pointer font-medium">응답 데이터 보기</summary> diff --git a/lib/soap/ecc/send/cancel-rfq.ts b/lib/soap/ecc/send/cancel-rfq.ts index bcbe0c7a..aeba6dd6 100644 --- a/lib/soap/ecc/send/cancel-rfq.ts +++ b/lib/soap/ecc/send/cancel-rfq.ts @@ -3,7 +3,7 @@ import { sendSoapXml, type SoapSendConfig, type SoapLogInfo, type SoapSendResult } from "@/lib/soap/sender"; // ECC RFQ 취소 엔드포인트 (WSDL에 명시된 P2038_D 사용) -const ECC_CANCEL_RFQ_ENDPOINT = "http://shii8dvddb01.hec.serp.shi.samsung.net:50000/sap/xi/engine?type=entry&version=3.0&Sender.Service=P2038_D&Interface=http%3A%2F%2Fshi.samsung.co.kr%2FP2_MM%2FMMM%5EP2MM3016_SO&QualityOfService=ExactlyOnce"; +const ECC_CANCEL_RFQ_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_MM%2FMMM%5EP2MM3016_SO"; // RFQ 취소 요청 데이터 타입 export interface CancelRFQRequest { @@ -118,6 +118,10 @@ export async function cancelRFQ(rfqNumber: string): Promise<{ success: boolean; message: string; responseData?: string; + statusCode?: number; + headers?: Record<string, string>; + endpoint?: string; + requestXml?: string; rfqNumber?: string; }> { try { @@ -135,6 +139,10 @@ export async function cancelRFQ(rfqNumber: string): Promise<{ success: result.success, message: result.success ? 'RFQ 취소 요청이 성공적으로 전송되었습니다.' : result.message, responseData: result.responseText, + statusCode: result.statusCode, + headers: result.headers, + endpoint: result.endpoint, + requestXml: result.requestXml, rfqNumber }; diff --git a/lib/soap/ecc/send/create-po.ts b/lib/soap/ecc/send/create-po.ts index 0d992fb3..444d9cc1 100644 --- a/lib/soap/ecc/send/create-po.ts +++ b/lib/soap/ecc/send/create-po.ts @@ -4,7 +4,7 @@ import { sendSoapXml, type SoapSendConfig, type SoapLogInfo, type SoapSendResult import { getCurrentSAPDate, getCurrentSAPTime } from "@/lib/soap/utils"; // ECC PO 생성 엔드포인트 (WSDL에 명시된 P2038_D 사용) -const ECC_PO_ENDPOINT = "http://shii8dvddb01.hec.serp.shi.samsung.net:50000/sap/xi/engine?type=entry&version=3.0&Sender.Service=P2038_D&Interface=http%3A%2F%2Fshi.samsung.co.kr%2FP2_MM%2FMMM%5EP2MM3015_SO&QualityOfService=ExactlyOnce"; +const ECC_PO_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_MM%2FMMM%5EP2MM3015_SO"; // PO 헤더 데이터 타입 export interface POHeaderData { @@ -198,6 +198,10 @@ export async function createPurchaseOrder(poData: POCreateRequest): Promise<{ success: boolean; message: string; responseData?: string; + statusCode?: number; + headers?: Record<string, string>; + endpoint?: string; + requestXml?: string; bidding_number?: string; }> { try { @@ -209,6 +213,10 @@ export async function createPurchaseOrder(poData: POCreateRequest): Promise<{ success: result.success, message: result.success ? 'PO 생성 요청이 성공적으로 전송되었습니다.' : result.message, responseData: result.responseText, + statusCode: result.statusCode, + headers: result.headers, + endpoint: result.endpoint, + requestXml: result.requestXml, bidding_number: poData.T_Bidding_HEADER[0]?.ANFNR }; diff --git a/lib/soap/ecc/send/pcr-confirm.ts b/lib/soap/ecc/send/pcr-confirm.ts index 72d4a574..439ec6f8 100644 --- a/lib/soap/ecc/send/pcr-confirm.ts +++ b/lib/soap/ecc/send/pcr-confirm.ts @@ -4,7 +4,7 @@ import { sendSoapXml, type SoapSendConfig, type SoapLogInfo, type SoapSendResult } from "@/lib/soap/sender"; // ECC PCR 확인 엔드포인트 (WSDL에 명시된 P2038_D 사용) -const ECC_PCR_CONFIRM_ENDPOINT = "http://shii8dvddb01.hec.serp.shi.samsung.net:50000/sap/xi/engine?type=entry&version=3.0&Sender.Service=P2038_D&Interface=http%3A%2F%2Fshi.samsung.co.kr%2FP2_MM%2FMMM%5EP2MM3019_SO&QualityOfService=ExactlyOnce"; +const ECC_PCR_CONFIRM_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_MM%2FMMM%5EP2MM3019_SO"; // PCR 확인 요청 데이터 타입 export interface PCRConfirmRequest { @@ -227,6 +227,10 @@ export async function confirmPCR(pcrItem: { success: boolean; message: string; responseData?: string; + statusCode?: number; + headers?: Record<string, string>; + endpoint?: string; + requestXml?: string; pcrRequest?: string; }> { try { @@ -242,6 +246,10 @@ export async function confirmPCR(pcrItem: { success: result.success, message: result.success ? 'PCR 확인 요청이 성공적으로 전송되었습니다.' : result.message, responseData: result.responseText, + statusCode: result.statusCode, + headers: result.headers, + endpoint: result.endpoint, + requestXml: result.requestXml, pcrRequest: `${pcrItem.PCR_REQ}-${pcrItem.PCR_REQ_SEQ}` }; diff --git a/lib/soap/ecc/send/rfq-info.ts b/lib/soap/ecc/send/rfq-info.ts index 777c1dfe..ffc47fc6 100644 --- a/lib/soap/ecc/send/rfq-info.ts +++ b/lib/soap/ecc/send/rfq-info.ts @@ -20,7 +20,7 @@ import { sendSoapXml, type SoapSendConfig, type SoapLogInfo, type SoapSendResult import { getCurrentSAPDate } from "@/lib/soap/utils"; // ECC RFQ 정보 전송 엔드포인트 (WSDL에 명시된 P2038_D 사용) -const ECC_RFQ_ENDPOINT = "http://shii8dvddb01.hec.serp.shi.samsung.net:50000/sap/xi/engine?type=entry&version=3.0&Sender.Service=P2038_D&Interface=http%3A%2F%2Fshi.samsung.co.kr%2FP2_MM%2FMMM%5EP2MM3014_SO&QualityOfService=ExactlyOnce"; +const ECC_RFQ_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_MM%2FMMM%5EP2MM3014_SO"; // RFQ 헤더 데이터 타입 export interface RFQHeaderData { @@ -189,6 +189,10 @@ export async function sendRFQInformation(rfqData: RFQInfoRequest): Promise<{ success: boolean; message: string; responseData?: string; + statusCode?: number; + headers?: Record<string, string>; + endpoint?: string; + requestXml?: string; rfq_number?: string; }> { try { @@ -200,6 +204,10 @@ export async function sendRFQInformation(rfqData: RFQInfoRequest): Promise<{ success: result.success, message: result.success ? 'RFQ 정보가 성공적으로 전송되었습니다.' : result.message, responseData: result.responseText, + statusCode: result.statusCode, + headers: result.headers, + endpoint: result.endpoint, + requestXml: result.requestXml, rfq_number: rfqData.T_RFQ_HEADER[0]?.ANFNR }; diff --git a/lib/soap/sender.ts b/lib/soap/sender.ts index 3728429b..1dfc8730 100644 --- a/lib/soap/sender.ts +++ b/lib/soap/sender.ts @@ -35,6 +35,9 @@ export interface SoapSendResult { responseText?: string; statusCode?: number; headers?: Record<string, string>; + endpoint?: string; + requestXml?: string; + requestHeaders?: Record<string, string>; } // 기본 환경변수에서 인증 정보 가져오기 @@ -111,34 +114,46 @@ export async function sendSoapXml( 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 () => { - // 헤더 설정 - const headers: 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'); - headers['Authorization'] = `Basic ${credentials}`; - console.log('🔐 Basic Authentication 헤더 추가 완료'); - } else { - console.warn('⚠️ SOAP 인증 정보가 설정되지 않았습니다.'); - } - - // fetch 옵션 설정 - const fetchOptions: RequestInit = { - method: 'POST', - headers, - body: xmlData, - }; - // 타임아웃 설정 if (config.timeout) { const controller = new AbortController(); @@ -171,14 +186,22 @@ export async function sendSoapXml( console.log('📥 SOAP 응답 수신:', response.status, response.statusText); console.log('🔍 응답 XML (첫 500자):', responseText.substring(0, 500)); - // HTTP 상태 코드 확인 - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - // SOAP Fault 검사 - if (responseText.includes('soap:Fault') || responseText.includes('SOAP:Fault')) { - throw new Error(`SOAP Fault: ${responseText}`); + // 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 + }; } // 응답 헤더 수집 @@ -192,7 +215,10 @@ export async function sendSoapXml( message: '전송 성공', responseText, statusCode: response.status, - headers: responseHeaders + headers: responseHeaders, + endpoint: config.endpoint, + requestXml: xmlData, + requestHeaders }; } catch (error) { |
