summaryrefslogtreecommitdiff
path: root/lib/soap/ecc/send/rfq-info.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/soap/ecc/send/rfq-info.ts')
-rw-r--r--lib/soap/ecc/send/rfq-info.ts469
1 files changed, 469 insertions, 0 deletions
diff --git a/lib/soap/ecc/send/rfq-info.ts b/lib/soap/ecc/send/rfq-info.ts
new file mode 100644
index 00000000..43fe821f
--- /dev/null
+++ b/lib/soap/ecc/send/rfq-info.ts
@@ -0,0 +1,469 @@
+'use server'
+
+/**
+ * RFQ(Request for Quotation) 정보 전송 시스템
+ *
+ * 기준 문서:
+ * - WSDL: IF_EVCP_ECC_RFQ_INFORMATION.wsdl
+ * - I/F 정의서: IF_EVCP_ECC_RFQ_INFORMATION.csv (송신측)
+ *
+ * 주요 구조:
+ * - T_RFQ_HEADER: RFQ 헤더 정보 (ANFNR, LIFNR, WAERS 등)
+ * - T_RFQ_ITEM: RFQ 아이템 정보 (ANFNR, ANFPS, NETPR 등)
+ *
+ * 주의사항:
+ * - EV_TYPE, EV_MESSAGE는 응답 전용 필드 (송신시 사용 안함)
+ * - 필수 필드(M)와 선택 필드(O) 구분 적용
+ */
+
+import { sendSoapXml, type SoapSendConfig, type SoapLogInfo, type SoapSendResult } from "@/lib/soap/sender";
+import { getCurrentSAPDate } from "@/lib/soap/utils";
+
+// ECC RFQ 정보 전송 엔드포인트
+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";
+
+// RFQ 헤더 데이터 타입
+export interface RFQHeaderData {
+ ANFNR: string; // RFQ Number (필수) - CHAR(10)
+ LIFNR: string; // Vendor's account number (필수) - CHAR(10)
+ WAERS: string; // Currency Key (필수) - CUKY(5)
+ ZTERM: string; // Terms of Payment Key (필수) - CHAR(4)
+ INCO1: string; // Incoterms (Part 1) (필수) - CHAR(3)
+ INCO2: string; // Incoterms (Part 2) (필수) - CHAR(28)
+ MWSKZ: string; // Tax on Sales/Purchases Code (필수) - CHAR(2)
+ LANDS: string; // Country for Tax Return (필수) - CHAR(3)
+ VSTEL?: string; // Place of Shipping (선택) - CHAR(3)
+ LSTEL?: string; // Place of Destination (선택) - CHAR(3)
+}
+
+// RFQ 아이템 데이터 타입
+export interface RFQItemData {
+ ANFNR: string; // RFQ Number (필수) - CHAR(10)
+ ANFPS: string; // Item Number of RFQ (필수) - NUMC(5,0)
+ NETPR: string; // Net Price in Purchasing Document (in Document Currency) (필수) - CURR(11,2)
+ NETWR: string; // Net Order Value in PO Currency (필수) - CURR(13,2)
+ BRTWR: string; // Gross order value in PO currency (필수) - CURR(13,2)
+ LFDAT?: string; // Item delivery date (선택) - DATS(8)
+}
+
+// RFQ 정보 전송 요청 데이터 타입
+export interface RFQInfoRequest {
+ T_RFQ_HEADER: RFQHeaderData[];
+ T_RFQ_ITEM: RFQItemData[];
+}
+
+// RFQ 응답 데이터 타입 (WSDL P2MM3014_S_response 기준)
+// 주의: CSV 정의서에 따르면 EV_TYPE, EV_MESSAGE는 "수신측 응답으로 보낼 내용이 아님"
+export interface RFQInfoResponse {
+ EV_TYPE?: string; // Message Type - CHAR(1) (응답 전용, 송신시 사용 안함)
+ EV_MESSAGE?: string; // Message Text - CHAR(100) (응답 전용, 송신시 사용 안함)
+}
+
+// SOAP Body Content 생성 함수
+function createRFQSoapBodyContent(rfqData: RFQInfoRequest): Record<string, unknown> {
+ return {
+ 'ns0:MT_P2MM3014_S': {
+ 'T_RFQ_HEADER': rfqData.T_RFQ_HEADER,
+ 'T_RFQ_ITEM': rfqData.T_RFQ_ITEM
+ }
+ };
+}
+
+
+
+// RFQ 데이터 기본 검증 함수 (필수 필드만 확인)
+function validateRFQData(rfqData: RFQInfoRequest): { isValid: boolean; errors: string[] } {
+ const errors: string[] = [];
+
+ // 헤더 데이터 검증
+ if (!rfqData.T_RFQ_HEADER || rfqData.T_RFQ_HEADER.length === 0) {
+ errors.push('T_RFQ_HEADER는 필수입니다.');
+ } else {
+ rfqData.T_RFQ_HEADER.forEach((header, index) => {
+ // 필수 필드 존재 여부만 검증
+ const requiredFields = ['ANFNR', 'LIFNR', 'WAERS', 'ZTERM', 'INCO1', 'INCO2', 'MWSKZ', 'LANDS'];
+ requiredFields.forEach(field => {
+ if (!header[field as keyof RFQHeaderData]) {
+ errors.push(`T_RFQ_HEADER[${index}].${field}는 필수입니다.`);
+ }
+ });
+ });
+ }
+
+ // 아이템 데이터 검증
+ if (!rfqData.T_RFQ_ITEM || rfqData.T_RFQ_ITEM.length === 0) {
+ errors.push('T_RFQ_ITEM은 필수입니다.');
+ } else {
+ rfqData.T_RFQ_ITEM.forEach((item, index) => {
+ // 필수 필드 존재 여부만 검증
+ const requiredFields = ['ANFNR', 'ANFPS', 'NETPR', 'NETWR', 'BRTWR'];
+ requiredFields.forEach(field => {
+ if (!item[field as keyof RFQItemData]) {
+ errors.push(`T_RFQ_ITEM[${index}].${field}는 필수입니다.`);
+ }
+ });
+ });
+ }
+
+ // RFQ 번호 일관성 검증
+ if (rfqData.T_RFQ_HEADER.length > 0 && rfqData.T_RFQ_ITEM.length > 0) {
+ const headerRFQNumbers = new Set(rfqData.T_RFQ_HEADER.map(h => h.ANFNR));
+ const itemRFQNumbers = new Set(rfqData.T_RFQ_ITEM.map(i => i.ANFNR));
+
+ for (const itemRFQ of itemRFQNumbers) {
+ if (!headerRFQNumbers.has(itemRFQ)) {
+ errors.push(`T_RFQ_ITEM의 ANFNR '${itemRFQ}'에 해당하는 T_RFQ_HEADER가 없습니다.`);
+ }
+ }
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors
+ };
+}
+
+// ECC로 RFQ 정보 SOAP XML 전송하는 함수
+async function sendRFQToECC(rfqData: RFQInfoRequest): Promise<SoapSendResult> {
+ try {
+ // 데이터 검증
+ const validation = validateRFQData(rfqData);
+ if (!validation.isValid) {
+ return {
+ success: false,
+ message: `데이터 검증 실패: ${validation.errors.join(', ')}`
+ };
+ }
+
+ // SOAP Body Content 생성
+ const soapBodyContent = createRFQSoapBodyContent(rfqData);
+
+ // SOAP 전송 설정
+ const config: SoapSendConfig = {
+ endpoint: ECC_RFQ_ENDPOINT,
+ envelope: soapBodyContent,
+ soapAction: 'http://sap.com/xi/WebService/soap1.1',
+ timeout: 60000, // RFQ 정보 전송은 60초 타임아웃
+ retryCount: 3,
+ retryDelay: 2000
+ };
+
+ // 로그 정보
+ const logInfo: SoapLogInfo = {
+ direction: 'OUTBOUND',
+ system: 'S-ERP ECC',
+ interface: 'IF_EVCP_ECC_RFQ_INFORMATION'
+ };
+
+ console.log(`📤 RFQ 정보 전송 시작 - ANFNR: ${rfqData.T_RFQ_HEADER[0]?.ANFNR}`);
+ console.log(`🔍 헤더 ${rfqData.T_RFQ_HEADER.length}개, 아이템 ${rfqData.T_RFQ_ITEM.length}개`);
+
+ // SOAP XML 전송
+ const result = await sendSoapXml(config, logInfo);
+
+ if (result.success) {
+ console.log(`✅ RFQ 정보 전송 성공 - ANFNR: ${rfqData.T_RFQ_HEADER[0]?.ANFNR}`);
+ } else {
+ console.error(`❌ RFQ 정보 전송 실패 - ANFNR: ${rfqData.T_RFQ_HEADER[0]?.ANFNR}, 오류: ${result.message}`);
+ }
+
+ return result;
+
+ } catch (error) {
+ console.error('❌ RFQ 정보 전송 중 오류 발생:', error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
+}
+
+// ========================================
+// 메인 RFQ 정보 전송 함수들
+// ========================================
+
+// 단일 RFQ 정보 전송 처리
+export async function sendRFQInformation(rfqData: RFQInfoRequest): Promise<{
+ success: boolean;
+ message: string;
+ responseData?: string;
+ rfq_number?: string;
+}> {
+ try {
+ console.log(`🚀 RFQ 정보 전송 요청 시작 - ANFNR: ${rfqData.T_RFQ_HEADER[0]?.ANFNR}`);
+
+ const result = await sendRFQToECC(rfqData);
+
+ return {
+ success: result.success,
+ message: result.success ? 'RFQ 정보가 성공적으로 전송되었습니다.' : result.message,
+ responseData: result.responseText,
+ rfq_number: rfqData.T_RFQ_HEADER[0]?.ANFNR
+ };
+
+ } catch (error) {
+ console.error('❌ RFQ 정보 전송 처리 실패:', error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
+}
+
+// 여러 RFQ 배치 정보 전송 처리
+export async function sendMultipleRFQInformation(rfqDataList: RFQInfoRequest[]): Promise<{
+ success: boolean;
+ message: string;
+ results?: Array<{ rfq_number: string; success: boolean; error?: string }>;
+}> {
+ try {
+ console.log(`🚀 배치 RFQ 정보 전송 시작: ${rfqDataList.length}개`);
+
+ const results: Array<{ rfq_number: string; success: boolean; error?: string }> = [];
+
+ for (const rfqData of rfqDataList) {
+ try {
+ const rfq_number = rfqData.T_RFQ_HEADER[0]?.ANFNR || 'Unknown';
+ console.log(`📤 RFQ 정보 처리 중: ${rfq_number}`);
+
+ const result = await sendRFQToECC(rfqData);
+
+ if (result.success) {
+ console.log(`✅ RFQ 정보 전송 성공: ${rfq_number}`);
+ results.push({
+ rfq_number,
+ success: true
+ });
+ } else {
+ console.error(`❌ RFQ 정보 전송 실패: ${rfq_number}, 오류: ${result.message}`);
+ results.push({
+ rfq_number,
+ success: false,
+ error: result.message
+ });
+ }
+
+ // 배치 처리간 지연 (시스템 부하 방지)
+ if (rfqDataList.length > 1) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ } catch (error) {
+ const rfq_number = rfqData.T_RFQ_HEADER[0]?.ANFNR || 'Unknown';
+ console.error(`❌ RFQ 정보 처리 실패: ${rfq_number}`, error);
+ results.push({
+ rfq_number,
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error'
+ });
+ }
+ }
+
+ const successCount = results.filter(r => r.success).length;
+ const failCount = results.length - successCount;
+
+ console.log(`🎉 배치 RFQ 정보 전송 완료: 성공 ${successCount}개, 실패 ${failCount}개`);
+
+ return {
+ success: failCount === 0,
+ message: `배치 RFQ 정보 전송 완료: 성공 ${successCount}개, 실패 ${failCount}개`,
+ results
+ };
+
+ } catch (error) {
+ console.error('❌ 배치 RFQ 정보 전송 중 전체 오류 발생:', error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
+}
+
+// 테스트용 RFQ 정보 전송 함수 (샘플 데이터 포함)
+export async function sendTestRFQInformation(): Promise<{
+ success: boolean;
+ message: string;
+ responseData?: string;
+ testData?: RFQInfoRequest;
+}> {
+ try {
+ console.log('🧪 테스트용 RFQ 정보 전송 시작');
+
+ // 테스트용 샘플 데이터 생성
+ const testRFQData: RFQInfoRequest = {
+ T_RFQ_HEADER: [{
+ ANFNR: 'RFQ0000001', // CHAR(10) - RFQ Number
+ LIFNR: '1000000001', // CHAR(10) - Vendor's account number
+ WAERS: 'KRW', // CUKY(5) - Currency Key
+ ZTERM: '0001', // CHAR(4) - Terms of Payment Key
+ INCO1: 'FOB', // CHAR(3) - Incoterms (Part 1)
+ INCO2: 'Seoul, Korea', // CHAR(28) - Incoterms (Part 2)
+ MWSKZ: 'V0', // CHAR(2) - Tax on Sales/Purchases Code
+ LANDS: 'KR', // CHAR(3) - Country for Tax Return
+ VSTEL: '001', // CHAR(3) - Place of Shipping (선택)
+ LSTEL: '001' // CHAR(3) - Place of Destination (선택)
+ }],
+ T_RFQ_ITEM: [{
+ ANFNR: 'RFQ0000001', // CHAR(10) - RFQ Number
+ ANFPS: '00001', // NUMC(5,0) - Item Number of RFQ
+ NETPR: '1000.00', // CURR(11,2) - Net Price in Purchasing Document
+ NETWR: '1000.00', // CURR(13,2) - Net Order Value in PO Currency
+ BRTWR: '1100.00', // CURR(13,2) - Gross order value in PO currency
+ LFDAT: getCurrentSAPDate() // DATS(8) - Item delivery date (선택)
+ }]
+ };
+
+ const result = await sendRFQToECC(testRFQData);
+
+ return {
+ success: result.success,
+ message: result.success ? '테스트 RFQ 정보가 성공적으로 전송되었습니다.' : result.message,
+ responseData: result.responseText,
+ testData: testRFQData
+ };
+
+ } catch (error) {
+ console.error('❌ 테스트 RFQ 정보 전송 실패:', error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
+}
+
+// RFQ 정보 전송 상태 확인 함수 (향후 확장용)
+export async function checkRFQInformationStatus(rfqNumber: string): Promise<{
+ success: boolean;
+ message: string;
+ status?: string;
+}> {
+ try {
+ console.log(`🔍 RFQ 정보 전송 상태 확인: ${rfqNumber}`);
+
+ // 향후 ECC에서 상태 조회 API가 제공될 경우 구현
+ // 현재는 기본 응답만 반환
+
+ return {
+ success: true,
+ message: 'RFQ 정보 전송 상태 확인 기능은 향후 구현 예정입니다.',
+ status: 'PENDING'
+ };
+
+ } catch (error) {
+ console.error('❌ RFQ 상태 확인 실패:', error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : 'Unknown error'
+ };
+ }
+}
+
+// ========================================
+// 유틸리티 함수들
+// ========================================
+
+// RFQ 데이터 생성 헬퍼 함수
+function createRFQData(
+ headers: RFQHeaderData[],
+ items: RFQItemData[]
+): RFQInfoRequest {
+ return {
+ T_RFQ_HEADER: headers,
+ T_RFQ_ITEM: items
+ };
+}
+
+// RFQ 헤더 데이터 생성 헬퍼 함수
+function createRFQHeader(
+ rfqNumber: string, // ANFNR - CHAR(10)
+ vendorNumber: string, // LIFNR - CHAR(10)
+ currency: string = 'KRW', // WAERS - CUKY(5)
+ paymentTerms: string = '0001', // ZTERM - CHAR(4)
+ incoterms1: string = 'FOB', // INCO1 - CHAR(3)
+ incoterms2: string = 'Seoul, Korea', // INCO2 - CHAR(28)
+ taxCode: string = 'V0', // MWSKZ - CHAR(2)
+ countryForTax: string = 'KR', // LANDS - CHAR(3) (Country for Tax Return)
+ options?: {
+ shippingPlace?: string; // VSTEL - CHAR(3) (Place of Shipping)
+ destinationPlace?: string; // LSTEL - CHAR(3) (Place of Destination)
+ }
+): RFQHeaderData {
+ return {
+ ANFNR: rfqNumber,
+ LIFNR: vendorNumber,
+ WAERS: currency,
+ ZTERM: paymentTerms,
+ INCO1: incoterms1,
+ INCO2: incoterms2,
+ MWSKZ: taxCode,
+ LANDS: countryForTax,
+ VSTEL: options?.shippingPlace,
+ LSTEL: options?.destinationPlace
+ };
+}
+
+// RFQ 아이템 데이터 생성 헬퍼 함수
+function createRFQItem(
+ rfqNumber: string, // ANFNR - CHAR(10)
+ itemNumber: string, // ANFPS - NUMC(5,0)
+ netPrice: string, // NETPR - CURR(11,2) Net Price in Purchasing Document (in Document Currency)
+ netOrderValue: string, // NETWR - CURR(13,2) Net Order Value in PO Currency
+ grossOrderValue: string, // BRTWR - CURR(13,2) Gross order value in PO currency
+ deliveryDate?: string // LFDAT - DATS(8) Item delivery date (선택)
+): RFQItemData {
+ return {
+ ANFNR: rfqNumber,
+ ANFPS: itemNumber,
+ NETPR: netPrice,
+ NETWR: netOrderValue,
+ BRTWR: grossOrderValue,
+ LFDAT: deliveryDate
+ };
+}
+
+// RFQ 번호 기준으로 헤더와 아이템 매칭 검증
+function validateRFQMatching(headers: RFQHeaderData[], items: RFQItemData[]): {
+ isValid: boolean;
+ errors: string[];
+ orphanItems: string[];
+} {
+ const errors: string[] = [];
+ const orphanItems: string[] = [];
+
+ const headerRFQNumbers = new Set(headers.map(h => h.ANFNR));
+
+ for (const item of items) {
+ if (!headerRFQNumbers.has(item.ANFNR)) {
+ orphanItems.push(item.ANFNR);
+ errors.push(`RFQ 아이템 '${item.ANFNR}-${item.ANFPS}'에 해당하는 헤더가 없습니다.`);
+ }
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors,
+ orphanItems
+ };
+}
+
+// RFQ 데이터 요약 정보 생성
+function getRFQDataSummary(rfqData: RFQInfoRequest): {
+ rfqNumbers: string[];
+ totalHeaders: number;
+ totalItems: number;
+ itemsPerRFQ: Record<string, number>;
+} {
+ const rfqNumbers = [...new Set(rfqData.T_RFQ_HEADER.map(h => h.ANFNR))];
+ const itemsPerRFQ: Record<string, number> = {};
+
+ for (const rfqNumber of rfqNumbers) {
+ itemsPerRFQ[rfqNumber] = rfqData.T_RFQ_ITEM.filter(i => i.ANFNR === rfqNumber).length;
+ }
+
+ return {
+ rfqNumbers,
+ totalHeaders: rfqData.T_RFQ_HEADER.length,
+ totalItems: rfqData.T_RFQ_ITEM.length,
+ itemsPerRFQ
+ };
+}