diff options
| author | joonhoekim <26rote@gmail.com> | 2025-07-14 12:24:27 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-07-14 12:24:27 +0000 |
| commit | d5d27847a7eded9db59462fa744b76772bc9ce1d (patch) | |
| tree | 55580d7bdfbb48808de7d7bafab65394dabcf500 | |
| parent | 92403c19aad9a69f342ba135848fc6b75ed3e400 (diff) | |
(김준회) json2xml 로직을 fast-xml-parser로 교체하고 node-soap 미사용, XML 파싱 에러 해결
| -rw-r--r-- | app/[lng]/admin/mdg/page.tsx | 256 | ||||
| -rw-r--r-- | lib/soap/mdg/send/vendor-master/action.ts | 541 | ||||
| -rw-r--r-- | lib/soap/mdg/utils.ts | 47 |
3 files changed, 545 insertions, 299 deletions
diff --git a/app/[lng]/admin/mdg/page.tsx b/app/[lng]/admin/mdg/page.tsx new file mode 100644 index 00000000..8499f103 --- /dev/null +++ b/app/[lng]/admin/mdg/page.tsx @@ -0,0 +1,256 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { toast } from 'sonner' +import { Loader2, Send, RefreshCw } from 'lucide-react' +import { sendTestVendorDataToMDG } from '@/lib/soap/mdg/send/vendor-master/action' + +// CSV 필드를 정의할 타입 +interface VendorFieldDef { + table: string; + name: string; + mandatory: boolean; + description: string; +} + +// CSV 파싱 함수 (간단 파서) +const parseCSV = (csv: string): VendorFieldDef[] => { + const lines = csv.trim().split('\n'); + // 첫 번째 라인은 헤더이므로 제거 + return lines.slice(1).map((line) => { + const parts = line.split(','); + const table = parts[1]?.trim(); + const name = parts[2]?.trim(); + const mandatory = parts[3]?.trim() === 'M'; + const description = parts.slice(6).join(',').trim(); + return { table, name, mandatory, description } as VendorFieldDef; + }); +}; + +// 기존 샘플 기본값 (필요 시 확장) +const sampleDefaults: Record<string, string> = { + BP_HEADER: 'TEST001', + ZZSRMCD: 'EVCP', + TITLE: 'TEST', + BU_SORT1: 'TEST VENDOR', + NAME_ORG1: '테스트 벤더 회사', + KTOKK: 'Z001', + VEN_KFBUS: '제조업', + VEN_KFIND: 'IT', + MASTERFLAG: 'X', + IBND_TYPE: 'U', + ZZREQID: 'TESTUSER01', + ADDRNO: '0001', + AD_NATION: '1', + COUNTRY: 'KR', + LANGU_COM: 'K', + POST_COD1: '06292', + CITY1: '서울시', + DISTRICT: '강남구', + REGION: '11', + MC_STREET: '테헤란로 123', + T_COUNTRY: 'KR', + T_NUMBER: '02-1234-5678', + F_COUNTRY: 'KR', + F_NUMBER: '02-1234-5679', + U_ADDRESS: 'https://test.vendor.com', + E_ADDRESS: 'contact@test.vendor.com', + BP_TX_TYP: 'KR2', + TAXNUM: '123-45-67890', + AD_CONSNO: '1', +}; + +export default function MDGTestPage() { + const [formData, setFormData] = useState<Record<string, string>>({}); + const [fieldDefs, setFieldDefs] = useState<VendorFieldDef[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [lastResult, setLastResult] = useState<string>(''); + + // CSV 로딩 및 초기 데이터 셋업 + useEffect(() => { + const load = async () => { + const res = await fetch('/wsdl/P2MD3007_AO.csv'); + const csvText = await res.text(); + const defs = parseCSV(csvText); + setFieldDefs(defs); + + const init: Record<string, string> = {}; + defs.forEach((d) => { + init[d.name] = sampleDefaults[d.name] ?? ''; + }); + setFormData(init); + }; + + load(); + }, []); + + // 폼 데이터 업데이트 + const updateField = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + // 기본값으로 리셋 + const resetForm = () => { + const reset: Record<string, string> = {}; + fieldDefs.forEach((d) => { + reset[d.name] = sampleDefaults[d.name] ?? ''; + }); + setFormData(reset); + setLastResult(''); + toast.success('폼이 기본값으로 리셋되었습니다.'); + }; + + // 테스트 송신 실행 (SOAP 라이브러리 사용) + const handleTestSend = async () => { + try { + setIsLoading(true); + setLastResult(''); + + // 필수 필드 검증 + const requiredFields = fieldDefs.filter(d => d.mandatory).map(d => d.name); + const missingFields = requiredFields.filter(field => !formData[field]?.trim()); + + if (missingFields.length > 0) { + toast.error(`필수 필드가 누락되었습니다: ${missingFields.join(', ')}`); + setIsLoading(false); + return; + } + + // fast-xml-parser 기반 송신 함수 호출 + const result = await sendTestVendorDataToMDG(formData); + + if (!result.success) { + toast.error(`송신 실패: ${result.message}`); + setLastResult(`❌ [SAP XI] 송신 실패: ${result.message}`); + setIsLoading(false); + return; + } + + toast.success('MDG 송신이 완료되었습니다.'); + setLastResult(`✅ [SAP XI] 송신 성공: ${result.message} + +🔍 생성된 XML: +${result.generatedXML} + +📄 응답 데이터: +${typeof result.responseData === 'string' ? result.responseData : JSON.stringify(result.responseData, null, 2)}`); + + } catch (error) { + console.error('테스트 송신 실패:', error); + const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류'; + toast.error(`테스트 송신 중 오류가 발생했습니다: ${errorMessage}`); + setLastResult(`❌ [SAP XI] 송신 오류: ${errorMessage}`); + } finally { + setIsLoading(false); + } + }; + + + + + + return ( + <div className="container mx-auto p-6 space-y-6"> + <div className="flex items-center justify-between"> + <div> + <h1 className="text-3xl font-bold">MDG VENDOR 마스터 송신</h1> + <p className="text-muted-foreground mt-2"> + VENDOR 마스터 데이터를 MDG 시스템으로 테스트 송신합니다.<br /> + <strong>fast-xml-parser</strong>를 사용하여 SAP XI 호환 XML을 생성하고 전송합니다. + </p> + </div> + <div className="flex gap-2"> + <Button variant="outline" onClick={resetForm}> + <RefreshCw className="w-4 h-4 mr-2" /> + 리셋 + </Button> + <Button onClick={handleTestSend} disabled={isLoading}> + {isLoading ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <Send className="w-4 h-4 mr-2" /> + )} + MDG 테스트 송신 + </Button> + </div> + </div> + + {/* 동적 필드 렌더링 */} + {fieldDefs.length === 0 ? ( + <p className="text-center text-muted-foreground">CSV 로딩 중...</p> + ) : ( + <div className="space-y-6"> + {Object.entries( + fieldDefs.reduce((acc: Record<string, VendorFieldDef[]>, cur) => { + acc[cur.table] = acc[cur.table] ? [...acc[cur.table], cur] : [cur]; + return acc; + }, {}) + ).map(([table, fields]) => ( + <Card key={table}> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + {table} + {fields.some(f => f.mandatory) && ( + <Badge variant="destructive">필수 포함</Badge> + )} + </CardTitle> + <CardDescription>{table} 테이블 입력</CardDescription> + </CardHeader> + <CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + {fields.filter((f, idx, arr) => arr.findIndex(x => x.name === f.name) === idx).map((field) => ( + <div key={field.name}> + <Label htmlFor={field.name} className="flex items-center gap-1"> + {field.name} + {field.mandatory && ( + <Badge variant="destructive" className="ml-1">필수</Badge> + )} + </Label> + <Input + id={field.name} + value={formData[field.name] ?? ''} + onChange={(e) => updateField(field.name, e.target.value)} + /> + {field.description && ( + <p className="text-xs text-muted-foreground mt-1">{field.description}</p> + )} + </div> + ))} + </CardContent> + </Card> + ))} + </div> + )} + + {/* 송신 결과 영역 */} + <Card> + <CardHeader> + <CardTitle>송신 결과</CardTitle> + <CardDescription> + MDG 시스템 송신 결과가 여기에 표시됩니다 (node-soap 또는 fetch 방식) + </CardDescription> + </CardHeader> + <CardContent> + {lastResult ? ( + <pre className="p-4 bg-muted max-h-96 overflow-auto text-xs whitespace-pre-wrap"> + {lastResult} + </pre> + ) : ( + <div className="p-4 bg-muted rounded-lg"> + <p className="text-sm text-muted-foreground"> + 테스트 송신 버튼을 클릭하면 결과가 표시됩니다.<br /> + - <strong>node-soap</strong>: 개선된 타임아웃 설정으로 테스트<br /> + - <strong>fetch</strong>: 기존 정상 동작 방식으로 테스트 + </p> + </div> + )} + </CardContent> + </Card> + </div> + ); +} + diff --git a/lib/soap/mdg/send/vendor-master/action.ts b/lib/soap/mdg/send/vendor-master/action.ts index b353eab0..24112316 100644 --- a/lib/soap/mdg/send/vendor-master/action.ts +++ b/lib/soap/mdg/send/vendor-master/action.ts @@ -18,19 +18,18 @@ import { } from "@/db/schema/MDG/mdg"; import { eq, sql, desc } from "drizzle-orm"; import { withSoapLogging } from "../../utils"; -import * as soap from 'soap'; import fs from 'fs'; import path from 'path'; - -// WSDL 파일 경로 -const WSDL_PATH = path.join(process.cwd(), 'public', 'wsdl', 'P2MD3007_AO.wsdl'); +import { XMLBuilder } from 'fast-xml-parser'; // 환경변수에서 인증 정보 가져오기 const MDG_SOAP_USERNAME = process.env.MDG_SOAP_USERNAME; const MDG_SOAP_PASSWORD = process.env.MDG_SOAP_PASSWORD; -const MDG_SOAP_AUTH_TYPE = process.env.MDG_SOAP_AUTH_TYPE || 'basic'; // 'basic' | 'wssecurity' -// CSV 파싱 및 필드 정의 ------------------------------------------------ +// SAP XI 엔드포인트 URL +const MDG_ENDPOINT_URL = "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"; + +// CSV 파싱 및 필드 정의 interface CsvField { table: string; field: string; @@ -59,151 +58,114 @@ try { console.error('CSV 로딩 실패:', e); } -// SOAP 클라이언트 생성 및 인증 설정 함수 -async function createSoapClient(): Promise<soap.Client> { - return new Promise((resolve, reject) => { - soap.createClient(WSDL_PATH, (err, client) => { - if (err) { - reject(err); - return; - } - - if (!client) { - reject(new Error('SOAP 클라이언트 생성 실패')); - return; - } - - // 인증 설정 - if (MDG_SOAP_USERNAME && MDG_SOAP_PASSWORD) { - try { - if (MDG_SOAP_AUTH_TYPE === 'wssecurity') { - // WS-Security 인증 설정 - const wsSecurity = new soap.WSSecurity(MDG_SOAP_USERNAME, MDG_SOAP_PASSWORD); - client.setSecurity(wsSecurity); - console.log('🔐 WS-Security 인증 설정 완료'); - } else { - // Basic Authentication 설정 (기본값) - const basicAuth = new soap.BasicAuthSecurity(MDG_SOAP_USERNAME, MDG_SOAP_PASSWORD); - client.setSecurity(basicAuth); - console.log('🔐 Basic Authentication 설정 완료'); - } - } catch (authError) { - console.warn('⚠️ 인증 설정 중 오류:', authError); +// SAP XI 호환 XML 생성 함수 (수정된 버전) +function generateSAPXICompatibleXML(supplierMaster: Record<string, string>): string { + // XML 선언을 별도로 처리 + const xmlDeclaration = '<?xml version="1.0" encoding="UTF-8"?>\n'; + + // SOAP Envelope 구조 정의 + const soapEnvelope = { + 'soap:Envelope': { + '@_xmlns:soap': 'http://schemas.xmlsoap.org/soap/envelope/', + '@_xmlns:ns0': 'http://shi.samsung.co.kr/P2_MD/MDZ', + 'soap:Body': { + 'ns0:MT_P2MD3007_S': { + 'SUPPLIER_MASTER': supplierMaster } - } else { - console.warn('⚠️ MDG SOAP 인증 정보가 환경변수에 설정되지 않았습니다.'); } + } + }; - resolve(client); - }); + const builder = new XMLBuilder({ + ignoreAttributes: false, + format: true, + attributeNamePrefix: '@_', + textNodeName: '#text', + suppressEmptyNode: true, // 빈 노드는 self-closing 태그로 처리 + suppressUnpairedNode: false, + indentBy: ' ', // 2칸 들여쓰기 + processEntities: false, // 엔티티 변환 방지 + suppressBooleanAttributes: false, + cdataPropName: false, + tagValueProcessor: (name, val) => val, // 값 처리기 + attributeValueProcessor: (name, val) => val // 속성 처리기 }); + + const xmlBody = builder.build(soapEnvelope); + + // XML 선언과 Body 결합 + const completeXML = xmlDeclaration + xmlBody; + + console.log('🔍 생성된 XML (전체):', completeXML); + + return completeXML; } -// SOAP 응답 타입 정의 -interface SoapResponse { - [key: string]: unknown; -} - -// SOAP 오류 타입 정의 -interface SoapError { - message: string; - body?: string; - statusCode?: number; -} - -// VENDOR 마스터 데이터를 MDG로 송신하는 액션 -export async function sendVendorMasterToMDG(vendorCodes: string[]): Promise<{ +// XML을 MDG로 전송하는 함수 (성공했던 구조 사용) +async function sendXMLToMDG(xmlData: string): Promise<{ success: boolean; message: string; - results?: Array<{ vendorCode: string; success: boolean; error?: string }>; + responseText?: string; }> { try { - console.log(`🚀 VENDOR_MASTER 송신 시작: ${vendorCodes.length}개 벤더`); - - const results: Array<{ vendorCode: string; success: boolean; error?: string }> = []; - - // 각 VENDOR 코드별로 개별 전송 (MDG 시스템의 처리 제한 고려) - for (const vendorCode of vendorCodes) { - try { - console.log(`📤 VENDOR ${vendorCode} 데이터 조회 중...`); - - // 데이터베이스에서 VENDOR 데이터 조회 - const vendorData = await fetchVendorData(vendorCode); - - if (!vendorData) { - results.push({ - vendorCode, - success: false, - error: 'VENDOR 데이터를 찾을 수 없습니다.' - }); - continue; - } - - // SOAP 요청 데이터 생성 - const soapData = buildSoapData(vendorData); - console.log(`📄 VENDOR ${vendorCode} SOAP 데이터 생성 완료`); - - // 데이터 구조 검증 (디버깅용) - const validation = validateSoapDataStructure(soapData); - if (!validation.isValid) { - console.warn(`⚠️ VENDOR ${vendorCode} 데이터 구조 문제:`, validation.issues); + const responseText = await withSoapLogging( + 'OUTBOUND', + 'S-ERP MDG', + 'IF_MDZ_EVCP_VENDOR_MASTER', + xmlData, + async () => { + // 성공했던 전송 방식 그대로 사용 + const headers: Record<string, string> = { + 'Content-Type': 'text/xml; charset=utf-8', + 'SOAPAction': 'http://sap.com/xi/WebService/soap1.1', + }; + + // Basic Authentication 헤더 추가 + if (MDG_SOAP_USERNAME && MDG_SOAP_PASSWORD) { + const credentials = Buffer.from(`${MDG_SOAP_USERNAME}:${MDG_SOAP_PASSWORD}`).toString('base64'); + headers['Authorization'] = `Basic ${credentials}`; + console.log('🔐 Basic Authentication 헤더 추가 완료'); } else { - console.log(`✅ VENDOR ${vendorCode} 데이터 구조 검증 통과`); + console.warn('⚠️ MDG SOAP 인증 정보가 환경변수에 설정되지 않았습니다.'); } - // SOAP 클라이언트로 요청 전송 - await withSoapLogging( - 'OUTBOUND', - 'S-ERP MDG', - 'IF_MDZ_EVCP_VENDOR_MASTER', - JSON.stringify(soapData), - async () => { - const client = await createSoapClient(); - - return new Promise<SoapResponse>((resolve, reject) => { - client.P2MD3007_AO(soapData, (err: SoapError | null, result: SoapResponse) => { - if (err) { - reject(err); - } else { - console.log(`✅ VENDOR ${vendorCode} MDG 전송 성공`); - resolve(result); - } - }); - }); - } - ); + console.log('📤 MDG 전송 시작'); + console.log('🔍 전송 XML (첫 500자):', xmlData.substring(0, 500)); - results.push({ - vendorCode, - success: true + const res = await fetch(MDG_ENDPOINT_URL, { + method: 'POST', + headers, + body: xmlData, }); + + const text = await res.text(); - } catch (error) { - console.error(`❌ VENDOR ${vendorCode} 전송 실패:`, error); - results.push({ - vendorCode, - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }); + console.log('📥 MDG 응답 수신:', res.status, res.statusText); + console.log('🔍 응답 XML (첫 500자):', text.substring(0, 500)); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + // SOAP Fault 검사 + if (text.includes('soap:Fault') || text.includes('SOAP:Fault')) { + throw new Error(`MDG SOAP Fault: ${text}`); + } + + return text; } - } - - const successCount = results.filter(r => r.success).length; - const failCount = results.length - successCount; - - console.log(`🎉 VENDOR_MASTER 송신 완료: 성공 ${successCount}개, 실패 ${failCount}개`); - + ); + return { - success: failCount === 0, - message: `전송 완료: 성공 ${successCount}개, 실패 ${failCount}개`, - results + success: true, + message: '전송 성공', + responseText, }; - } catch (error) { - console.error('❌ VENDOR_MASTER 송신 중 전체 오류 발생:', error); + console.error('❌ XML 전송 실패:', error); return { success: false, - message: error instanceof Error ? error.message : 'Unknown error' + message: error instanceof Error ? error.message : 'Unknown error', }; } } @@ -211,7 +173,6 @@ export async function sendVendorMasterToMDG(vendorCodes: string[]): Promise<{ // 데이터베이스에서 VENDOR 데이터 조회 async function fetchVendorData(vendorCode: string) { try { - // 1. 헤더 데이터 조회 const [vendorHeader] = await db .select() .from(VENDOR_MASTER_BP_HEADER) @@ -222,7 +183,6 @@ async function fetchVendorData(vendorCode: string) { return null; } - // 2. 관련 데이터 병렬 조회 const [ addresses, adEmails, @@ -273,22 +233,19 @@ async function fetchVendorData(vendorCode: string) { } } -// SOAP 데이터 생성 (WSDL 구조에 맞춤) -function buildSoapData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVendorData>>>) { +// SUPPLIER_MASTER 데이터 생성 +function buildSupplierMasterData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVendorData>>>) { const { vendorHeader, addresses, adFaxes, adPostals, adTels, bpTaxnums, bpVengens } = vendorData; - // 값 추출 매핑 ------------------------------------ const mapping: Record<string, string | undefined> = { - // Header BP_HEADER: vendorHeader?.VNDRCD, ZZSRMCD: 'EVCP', - TITLE: '', // vendorHeader에 TITLE 필드가 없음 + TITLE: '', BU_SORT1: adPostals[0]?.VNDRNM_ABRV_1 ?? undefined, NAME_ORG1: adPostals[0]?.VNDRNM_1 ?? undefined, KTOKK: bpVengens[0]?.ACNT_GRP ?? undefined, MASTERFLAG: 'X', IBND_TYPE: 'U', - // Address mandatory (first) ADDRNO: addresses[0]?.ADDRNO, AD_NATION: adPostals[0]?.INTL_ADR_VER_ID ?? undefined, COUNTRY: adPostals[0]?.NTN_CD ?? undefined, @@ -296,17 +253,13 @@ function buildSoapData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVe POST_COD1: adPostals[0]?.CITY_ZIP_NO ?? undefined, CITY1: adPostals[0]?.VNDRNM_1 ?? undefined, MC_STREET: adPostals[0]?.ADR_1 ?? undefined, - // Phone/Fax mandatory fields AD_CONSNO: '001', T_COUNTRY: adTels[0]?.NTN_CD ?? 'KR', F_COUNTRY: adFaxes[0]?.NTN_CD ?? 'KR', - // Tax BP_TX_TYP: bpTaxnums[0]?.TX_NO_CTG ?? 'KR2', TAXNUM: bpVengens[0]?.VAT_REG_NO ?? undefined, - // Default others can be added as needed }; - // 필드 순서에 따라 데이터 생성 const seen = new Set<string>(); const uniqueFields = CSV_FIELDS.filter(f => { if (seen.has(f.field)) return false; @@ -319,22 +272,158 @@ function buildSoapData(vendorData: NonNullable<Awaited<ReturnType<typeof fetchVe supplierMaster[f.field] = mapping[f.field] ?? ''; }); - // SOAP 요청 구조 생성 (P2MD3007_S 중복 제거) - // SOAP 클라이언트가 MT_P2MD3007_S로 래핑하므로, 여기서는 SUPPLIER_MASTER만 반환 - return { - SUPPLIER_MASTER: supplierMaster - }; + return supplierMaster; +} + +// ======================================== +// 메인 송신 함수들 +// ======================================== + +// VENDOR 마스터 데이터를 MDG로 송신하는 액션 +export async function sendVendorMasterToMDG(vendorCodes: string[]): Promise<{ + success: boolean; + message: string; + results?: Array<{ vendorCode: string; success: boolean; error?: string }>; +}> { + try { + console.log(`🚀 VENDOR_MASTER 송신 시작: ${vendorCodes.length}개 벤더`); + + const results: Array<{ vendorCode: string; success: boolean; error?: string }> = []; + + for (const vendorCode of vendorCodes) { + try { + console.log(`📤 VENDOR ${vendorCode} 데이터 조회 중...`); + + const vendorData = await fetchVendorData(vendorCode); + + if (!vendorData) { + results.push({ + vendorCode, + success: false, + error: 'VENDOR 데이터를 찾을 수 없습니다.' + }); + continue; + } + + const supplierMaster = buildSupplierMasterData(vendorData); + console.log(`📄 VENDOR ${vendorCode} 데이터 생성 완료`); + + const generatedXML = generateSAPXICompatibleXML(supplierMaster); + const result = await sendXMLToMDG(generatedXML); + + if (result.success) { + console.log(`✅ VENDOR ${vendorCode} MDG 전송 성공`); + results.push({ + vendorCode, + success: true + }); + } else { + console.error(`❌ VENDOR ${vendorCode} 전송 실패: ${result.message}`); + results.push({ + vendorCode, + success: false, + error: result.message + }); + } + + } catch (error) { + console.error(`❌ VENDOR ${vendorCode} 전송 실패:`, error); + results.push({ + vendorCode, + 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(`🎉 VENDOR_MASTER 송신 완료: 성공 ${successCount}개, 실패 ${failCount}개`); + + return { + success: failCount === 0, + message: `전송 완료: 성공 ${successCount}개, 실패 ${failCount}개`, + results + }; + + } catch (error) { + console.error('❌ VENDOR_MASTER 송신 중 전체 오류 발생:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +// 테스트용 폼 데이터 송신 함수 +export async function sendTestVendorDataToMDG(formData: Record<string, string>): Promise<{ + success: boolean; + message: string; + responseData?: unknown; + generatedXML?: string; +}> { + try { + console.log('🚀 테스트용 VENDOR 데이터 송신 시작'); + + let csvFields: CsvField[] = []; + try { + const csvRaw = fs.readFileSync(CSV_PATH, 'utf-8'); + csvFields = parseCsv(csvRaw); + } catch (e) { + console.error('CSV 로딩 실패:', e); + return { + success: false, + message: 'CSV 필드 정의 파일을 로드할 수 없습니다.' + }; + } + + const seen = new Set<string>(); + const uniqueFields = csvFields.filter(f => { + if (seen.has(f.field)) return false; + seen.add(f.field); + return true; + }); + + const supplierMaster: Record<string, string> = {}; + uniqueFields.forEach(f => { + supplierMaster[f.field] = formData[f.field] ?? ''; + }); + + const generatedXML = generateSAPXICompatibleXML(supplierMaster); + + console.log('📄 SAP XI 호환 XML 생성 완료'); + + const result = await sendXMLToMDG(generatedXML); + + return { + success: result.success, + message: result.success ? '테스트 송신이 완료되었습니다.' : result.message, + responseData: result.responseText, + generatedXML + }; + + } catch (error) { + console.error('❌ 테스트 송신 실패:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error' + }; + } } -// 특정 VENDOR만 송신하는 유틸리티 함수 +// ======================================== +// 유틸리티 함수들 +// ======================================== + +// 특정 VENDOR만 송신 export async function sendSingleVendorToMDG(vendorCode: string) { return await sendVendorMasterToMDG([vendorCode]); } -// 모든 VENDOR 송신하는 유틸리티 함수 (주의: 대량 데이터 처리) +// 모든 VENDOR 송신 (주의: 대량 데이터 처리) export async function sendAllVendorsToMDG() { try { - // 모든 VENDOR 코드 조회 const vendors = await db .select({ VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD }) .from(VENDOR_MASTER_BP_HEADER); @@ -350,7 +439,6 @@ export async function sendAllVendorsToMDG() { console.log(`⚠️ 전체 VENDOR 송신 요청: ${vendorCodes.length}개`); - // 배치 처리 (10개씩 분할하여 처리) const batchSize = 10; const results: Array<{ vendorCode: string; success: boolean; error?: string }> = []; @@ -363,7 +451,6 @@ export async function sendAllVendorsToMDG() { results.push(...batchResult.results); } - // 배치 간 잠깐 대기 (서버 부하 방지) if (i + batchSize < vendorCodes.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } @@ -387,7 +474,7 @@ export async function sendAllVendorsToMDG() { } } -// 수정된 VENDOR만 송신하는 액션 (updatedAt != createdAt 조건) +// 수정된 VENDOR만 송신 export async function sendModifiedVendorsToMDG(): Promise<{ success: boolean; message: string; @@ -396,7 +483,6 @@ export async function sendModifiedVendorsToMDG(): Promise<{ try { console.log('🔍 수정된 VENDOR 데이터 조회 중...'); - // updatedAt과 createdAt이 다른 VENDOR 조회 (수정된 것들) const modifiedVendors = await db .select({ VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD, @@ -405,7 +491,6 @@ export async function sendModifiedVendorsToMDG(): Promise<{ }) .from(VENDOR_MASTER_BP_HEADER) .where( - // PostgreSQL에서 timestamp 비교 (밀리초 차이 고려) sql`EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.updatedAt}) - EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.createdAt}) > 1` ); @@ -421,12 +506,6 @@ export async function sendModifiedVendorsToMDG(): Promise<{ console.log(`📋 수정된 VENDOR ${vendorCodes.length}개 발견:`, vendorCodes); - // 수정된 VENDOR들의 수정 시간 로그 - modifiedVendors.forEach(vendor => { - console.log(` - ${vendor.VNDRCD}: 생성 ${vendor.createdAt?.toISOString()}, 수정 ${vendor.updatedAt?.toISOString()}`); - }); - - // 배치 처리로 송신 const batchSize = 10; const results: Array<{ vendorCode: string; success: boolean; error?: string }> = []; @@ -439,7 +518,6 @@ export async function sendModifiedVendorsToMDG(): Promise<{ results.push(...batchResult.results); } - // 배치 간 대기 if (i + batchSize < vendorCodes.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } @@ -465,7 +543,7 @@ export async function sendModifiedVendorsToMDG(): Promise<{ } } -// 테스트용 N건 송신 액션 +// 테스트용 N건 송신 export async function sendNVendorsToMDG( count: number, startFrom: number = 0 @@ -484,7 +562,6 @@ export async function sendNVendorsToMDG( console.log(`🧪 테스트용 VENDOR 송신: ${count}건 (${startFrom}번째부터)`); - // N건의 VENDOR 코드 조회 const vendors = await db .select({ VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD }) .from(VENDOR_MASTER_BP_HEADER) @@ -502,7 +579,6 @@ export async function sendNVendorsToMDG( console.log(`📋 테스트 대상 VENDOR ${vendorCodes.length}개:`, vendorCodes); - // 송신 실행 const result = await sendVendorMasterToMDG(vendorCodes); console.log(`🧪 테스트 송신 완료: ${vendorCodes.length}개 처리`); @@ -521,7 +597,7 @@ export async function sendNVendorsToMDG( } } -// 최신 수정된 N건 송신 액션 (최근 수정 순) +// 최신 수정된 N건 송신 export async function sendRecentModifiedVendorsToMDG( count: number = 5 ): Promise<{ @@ -532,7 +608,6 @@ export async function sendRecentModifiedVendorsToMDG( try { console.log(`🕒 최근 수정된 VENDOR ${count}건 조회 중...`); - // 최근 수정된 순으로 N건 조회 const recentVendors = await db .select({ VNDRCD: VENDOR_MASTER_BP_HEADER.VNDRCD, @@ -540,7 +615,6 @@ export async function sendRecentModifiedVendorsToMDG( }) .from(VENDOR_MASTER_BP_HEADER) .where( - // 수정된 항목만 (updatedAt != createdAt) sql`EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.updatedAt}) - EXTRACT(EPOCH FROM ${VENDOR_MASTER_BP_HEADER.createdAt}) > 1` ) .orderBy(desc(VENDOR_MASTER_BP_HEADER.updatedAt)) @@ -558,7 +632,6 @@ export async function sendRecentModifiedVendorsToMDG( console.log(`📋 최근 수정된 VENDOR ${vendorCodes.length}개:`, recentVendors.map(v => `${v.VNDRCD}(${v.updatedAt?.toISOString()})`)); - // 송신 실행 const result = await sendVendorMasterToMDG(vendorCodes); console.log(`🕒 최근 수정 데이터 송신 완료`); @@ -577,35 +650,7 @@ export async function sendRecentModifiedVendorsToMDG( } } -// SOAP 데이터 구조 검증 유틸리티 함수 -export function validateSoapDataStructure(soapData: Record<string, unknown>): { - isValid: boolean; - structure: string; - issues: string[]; -} { - const issues: string[] = []; - - // 올바른 구조인지 확인 - if (!soapData.SUPPLIER_MASTER) { - issues.push('SUPPLIER_MASTER 필드가 없습니다.'); - } - - // P2MD3007_S 중복 확인 (이제는 없어야 함) - if (soapData.P2MD3007_S) { - issues.push('P2MD3007_S 래핑이 발견되었습니다. 중복 구조로 인해 문제가 발생할 수 있습니다.'); - } - - // 구조 출력 - const structure = JSON.stringify(soapData, null, 2); - - return { - isValid: issues.length === 0, - structure, - issues - }; -} - -// 통계 조회 유틸리티 함수 +// 통계 조회 export async function getVendorSendStatistics(): Promise<{ total: number; modified: number; @@ -655,106 +700,4 @@ export async function getVendorSendStatistics(): Promise<{ } } -// 테스트용 폼 데이터 송신 함수 (SOAP 라이브러리 사용) -export async function sendTestVendorDataToMDG(formData: Record<string, string>): Promise<{ - success: boolean; - message: string; - responseData?: unknown; -}> { - try { - console.log('🧪 테스트용 VENDOR 데이터 SOAP 송신 시작'); - - // CSV 파일 동적 로드 (더 안전함) - let csvFields: CsvField[] = []; - try { - const csvRaw = fs.readFileSync(CSV_PATH, 'utf-8'); - csvFields = parseCsv(csvRaw); - } catch (e) { - console.error('CSV 로딩 실패:', e); - return { - success: false, - message: 'CSV 필드 정의 파일을 로드할 수 없습니다.' - }; - } - - // 필수 필드 검증 - const requiredFields = csvFields.filter(f => f.mandatory).map(f => f.field); - const missingFields = requiredFields.filter(field => !formData[field]?.trim()); - - if (missingFields.length > 0) { - return { - success: false, - message: `필수 필드가 누락되었습니다: ${missingFields.join(', ')}` - }; - } - - // 필드 순서에 따라 데이터 생성 - const seen = new Set<string>(); - const uniqueFields = csvFields.filter(f => { - if (seen.has(f.field)) return false; - seen.add(f.field); - return true; - }); - - const supplierMaster: Record<string, string> = {}; - uniqueFields.forEach(f => { - supplierMaster[f.field] = formData[f.field] ?? ''; - }); - - // SOAP 요청 구조 생성 - const soapData = { - SUPPLIER_MASTER: supplierMaster - }; - - console.log('📄 테스트 SOAP 데이터 생성 완료'); - - // 데이터 구조 검증 (디버깅용) - const validation = validateSoapDataStructure(soapData); - if (!validation.isValid) { - console.warn('⚠️ 테스트 데이터 구조 문제:', validation.issues); - return { - success: false, - message: `데이터 구조 오류: ${validation.issues.join(', ')}` - }; - } else { - console.log('✅ 테스트 데이터 구조 검증 통과'); - } - - // SOAP 클라이언트로 요청 전송 - const responseData = await withSoapLogging( - 'OUTBOUND', - 'MDG', - 'IF_MDZ_EVCP_VENDOR_MASTER_TEST', - JSON.stringify(soapData), - async () => { - const client = await createSoapClient(); - - return new Promise<SoapResponse>((resolve, reject) => { - client.P2MD3007_AO(soapData, (err: SoapError | null, result: SoapResponse) => { - if (err) { - reject(err); - } else { - console.log('✅ 테스트 MDG 전송 성공'); - resolve(result); - } - }); - }); - } - ); - - return { - success: true, - message: '테스트 송신이 완료되었습니다.', - responseData - }; - - } catch (error) { - console.error('❌ 테스트 송신 실패:', error); - return { - success: false, - message: error instanceof Error ? error.message : 'Unknown error' - }; - } -} - diff --git a/lib/soap/mdg/utils.ts b/lib/soap/mdg/utils.ts index 437988dc..02dd088e 100644 --- a/lib/soap/mdg/utils.ts +++ b/lib/soap/mdg/utils.ts @@ -447,4 +447,51 @@ export async function withSoapLogging<T>( throw error; } +} + +/** + * SOAP 로그 단순 기록 함수 (이미 완료된 작업에 대한 로깅) + * @param direction 수신/송신 구분 + * @param system 시스템명 + * @param interfaceName 인터페이스명 + * @param requestData 요청 XML 데이터 + * @param responseData 응답 XML 데이터 (선택사항) + * @param isSuccess 성공 여부 + * @param errorMessage 에러 메시지 (실패시) + */ +export async function logSoapExecution( + direction: LogDirection, + system: string, + interfaceName: string, + requestData: string, + responseData?: string, + isSuccess: boolean = true, + errorMessage?: string +): Promise<void> { + try { + const logData: SoapLogInsert = { + direction, + system, + interface: interfaceName, + startedAt: new Date(), + endedAt: new Date(), + isSuccess, + requestData, + responseData: responseData || null, + errorMessage: errorMessage || null, + }; + + await db.insert(soapLogs).values(logData); + + console.log(`📝 SOAP 로그 기록 완료 [${direction}] ${system}/${interfaceName} - 성공: ${isSuccess}`); + + // 로그 정리 (백그라운드) + cleanupOldSoapLogs().catch(error => + console.error('백그라운드 로그 정리 실패:', error) + ); + + } catch (error) { + console.error('SOAP 로그 기록 실패:', error); + // 로그 기록 실패는 메인 로직에 영향을 주지 않도록 throw 하지 않음 + } }
\ No newline at end of file |
