diff options
Diffstat (limited to 'app/api/(S-ERP)/(MDG)/utils.ts')
| -rw-r--r-- | app/api/(S-ERP)/(MDG)/utils.ts | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/app/api/(S-ERP)/(MDG)/utils.ts b/app/api/(S-ERP)/(MDG)/utils.ts new file mode 100644 index 00000000..bcb1dd45 --- /dev/null +++ b/app/api/(S-ERP)/(MDG)/utils.ts @@ -0,0 +1,396 @@ +import { XMLParser } from "fast-xml-parser"; +import { readFileSync } from "fs"; +import { NextRequest, NextResponse } from "next/server"; +import { join } from "path"; +import { eq, desc } from "drizzle-orm"; +import db from "@/db/db"; +import { soapLogs, type LogDirection, type SoapLogInsert } from "@/db/schema/SOAP/soap"; + +// XML 파싱용 타입 유틸리티: 스키마에서 XML 타입 생성 +export type ToXMLFields<T> = { + [K in keyof T]?: T[K] extends string | null | undefined ? string : never; +}; + +// SOAP Body 데이터 타입 (범용) +export interface SoapBodyData { + [key: string]: unknown; +} + +// WSDL 파일 제공 함수 +export function serveWsdl(wsdlFileName: string) { + try { + const wsdlPath = join(process.cwd(), 'public', 'wsdl', wsdlFileName); + const wsdlContent = readFileSync(wsdlPath, 'utf-8'); + + return new NextResponse(wsdlContent, { + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + }, + }); + } catch (error) { + console.error('Failed to read WSDL file:', error); + return new NextResponse('WSDL file not found', { status: 404 }); + } +} + +// XML 파서 생성 (기본 설정) +export function createXMLParser(arrayTags: string[] = []) { + return new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + parseAttributeValue: false, + trimValues: true, + isArray: (name: string) => arrayTags.includes(name), + parseTagValue: false, + allowBooleanAttributes: true, + }); +} + +// SOAP Body나 루트에서 요청 데이터 추출 (범용) +export function extractRequestData( + parsedData: Record<string, unknown>, + requestKeyPattern: string +): SoapBodyData | null { + // SOAP 구조 체크 + const soapPaths = [ + ['soap:Envelope', 'soap:Body'], + ['SOAP:Envelope', 'SOAP:Body'], + ['Envelope', 'Body'], + ['soapenv:Envelope', 'soapenv:Body'] + ]; + + for (const [envelope, body] of soapPaths) { + if (parsedData?.[envelope]?.[body]) { + const result = extractFromSoapBody(parsedData[envelope][body] as SoapBodyData, requestKeyPattern); + if (result) return result; + } + } + + // 직접 요청 데이터 체크 + const requestKeys = [ + requestKeyPattern, + `tns:${requestKeyPattern}`, + `ns1:${requestKeyPattern}`, + `p0:${requestKeyPattern}` + ]; + + for (const key of requestKeys) { + if (parsedData?.[key]) { + return parsedData[key] as SoapBodyData; + } + } + + // 키 이름 패턴 검색 + for (const key of Object.keys(parsedData)) { + if (key.includes(requestKeyPattern)) { + return parsedData[key] as SoapBodyData; + } + } + + // 메인 데이터가 직접 있는 경우 (MATL 등) + if (parsedData?.MATL && Array.isArray(parsedData.MATL)) { + return parsedData as SoapBodyData; + } + + return null; +} + +function extractFromSoapBody(soapBody: SoapBodyData, requestKeyPattern: string): SoapBodyData | null { + const requestKeys = [ + requestKeyPattern.replace('Req', ''), + requestKeyPattern, + `tns:${requestKeyPattern}`, + `ns1:${requestKeyPattern}`, + `p0:${requestKeyPattern}` + ]; + + for (const key of requestKeys) { + if (soapBody?.[key]) { + return soapBody[key] as SoapBodyData; + } + } + + // 패턴 검색 + for (const key of Object.keys(soapBody)) { + if (key.includes(requestKeyPattern)) { + return soapBody[key] as SoapBodyData; + } + } + + // 메인 데이터가 직접 있는 경우 + if (soapBody.MATL && Array.isArray(soapBody.MATL)) { + return soapBody; + } + + return null; +} + +// 범용 XML → DB 변환 함수 +export function convertXMLToDBData<T extends Record<string, unknown>>( + xmlData: Record<string, string | undefined>, + requiredFields: (keyof T)[] = [], + fkData?: Record<string, string> +): T { + const result = {} as T; + + // XML 필드를 DB 필드로 변환 (string → string|null) + for (const key in xmlData) { + if (xmlData.hasOwnProperty(key)) { + const value = xmlData[key]; + (result as Record<string, unknown>)[key] = value || null; + } + } + + // 필수 필드 처리 (FK 등) + for (const field of requiredFields) { + if (!result[field] && fkData) { + const fieldStr = String(field); + if (fkData[fieldStr]) { + (result as Record<string, unknown>)[field] = fkData[fieldStr]; + } + } + } + + return result; +} + +// 중첩 배열 처리 함수 +export function processNestedArray<T, U>( + items: T[] | undefined, + converter: (item: T, fkData?: Record<string, string>) => U, + fkData?: Record<string, string> +): U[] { + if (!items || !Array.isArray(items)) { + return []; + } + + return items.map(item => converter(item, fkData)); +} + +// 에러 응답 생성 +export function createErrorResponse(error: unknown): NextResponse { + console.error('API Error:', error); + + const errorResponse = `<?xml version="1.0" encoding="UTF-8"?> +<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> + <soap:Body> + <soap:Fault> + <faultcode>soap:Server</faultcode> + <faultstring>${error instanceof Error ? ('[from eVCP]: ' + error.message) : 'Unknown error'}</faultstring> + </soap:Fault> + </soap:Body> +</soap:Envelope>`; + + return new NextResponse(errorResponse, { + status: 500, + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + }, + }); +} + +// 성공 응답 생성 +export function createSuccessResponse(namespace: string): NextResponse { + const xmlResponse = `<?xml version="1.0" encoding="UTF-8"?> +<soap:Envelope + xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:tns="${namespace}"> + <soap:Body> + </soap:Body> +</soap:Envelope>`; + + return new NextResponse(xmlResponse, { + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + }, + }); +} + +// 하위 테이블 처리: FK 기준으로 전체 삭제 후 재삽입 +export async function replaceSubTableData<T>( + tx: any, + table: any, + data: T[], + parentField: string, + parentValue: string +) { + // 1. 기존 데이터 전체 삭제 (FK 기준) - eq() 함수 사용 + await tx.delete(table).where(eq(table[parentField], parentValue)); + + // 2. 새 데이터 삽입 + if (data.length > 0) { + await tx.insert(table).values(data); + } +} + +// ======================================== +// SOAP 로그 관련 공통 함수들 +// ======================================== + +/** + * SOAP 요청 로그를 시작하고 로그 ID를 반환 + * @param direction 수신/송신 구분 ('INBOUND' | 'OUTBOUND') + * @param system 시스템명 (예: 'S-ERP', 'MDG') + * @param interfaceName 인터페이스명 (예: 'IF_MDZ_EVCP_CUSTOMER_MASTER') + * @param requestData 요청 XML 데이터 + * @returns 생성된 로그 ID + */ +export async function startSoapLog( + direction: LogDirection, + system: string, + interfaceName: string, + requestData: string +): Promise<number> { + try { + const logData: SoapLogInsert = { + direction, + system, + interface: interfaceName, + startedAt: new Date(), + endedAt: null, + isSuccess: false, + requestData, + responseData: null, + errorMessage: null, + }; + + const [result] = await db.insert(soapLogs).values(logData).returning({ id: soapLogs.id }); + + console.log(`📝 SOAP 로그 시작 [${direction}] ${system}/${interfaceName} - ID: ${result.id}`); + return result.id; + } catch (error) { + console.error('SOAP 로그 시작 실패:', error); + throw error; + } +} + +/** + * SOAP 요청 로그를 완료 처리 + * @param logId 로그 ID + * @param isSuccess 성공 여부 + * @param responseData 응답 XML 데이터 (선택사항) + * @param errorMessage 에러 메시지 (실패시) + */ +export async function completeSoapLog( + logId: number, + isSuccess: boolean, + responseData?: string, + errorMessage?: string +): Promise<void> { + try { + await db.update(soapLogs) + .set({ + endedAt: new Date(), + isSuccess, + responseData: responseData || null, + errorMessage: errorMessage || null, + }) + .where(eq(soapLogs.id, logId)); + + console.log(`✅ SOAP 로그 완료 - ID: ${logId}, 성공: ${isSuccess}`); + } catch (error) { + console.error('SOAP 로그 완료 처리 실패:', error); + throw error; + } +} + +/** + * 환경변수 기반으로 오래된 SOAP 로그 정리 + * SOAP_LOG_MAX_RECORDS 환경변수를 확인하여 최대 개수 초과시 오래된 로그 삭제 + */ +export async function cleanupOldSoapLogs(): Promise<void> { + try { + const maxRecords = parseInt(process.env.SOAP_LOG_MAX_RECORDS || '0'); + + if (maxRecords <= 0) { + console.log('🔄 SOAP 로그 정리: 무제한 저장 설정 (SOAP_LOG_MAX_RECORDS = 0)'); + return; + } + + // 현재 총 로그 개수 확인 + const totalLogs = await db.select({ count: soapLogs.id }).from(soapLogs); + const currentCount = totalLogs.length; + + if (currentCount <= maxRecords) { + console.log(`🔄 SOAP 로그 정리: 현재 ${currentCount}개, 최대 ${maxRecords}개 - 정리 불필요`); + return; + } + + // 삭제할 개수 계산 + const deleteCount = currentCount - maxRecords; + + // 가장 오래된 로그들 조회 (ID 기준) + const oldestLogs = await db.select({ id: soapLogs.id }) + .from(soapLogs) + .orderBy(soapLogs.id) + .limit(deleteCount); + + if (oldestLogs.length === 0) { + console.log('🔄 SOAP 로그 정리: 삭제할 로그 없음'); + return; + } + + // 오래된 로그들 삭제 + const oldestIds = oldestLogs.map(log => log.id); + + // 배치 삭제 (IN 절 사용) + for (const logId of oldestIds) { + await db.delete(soapLogs).where(eq(soapLogs.id, logId)); + } + + console.log(`🗑️ SOAP 로그 정리 완료: ${deleteCount}개 삭제 (${currentCount} → ${maxRecords})`); + } catch (error) { + console.error('SOAP 로그 정리 실패:', error); + throw error; + } +} + +/** + * SOAP 로그 관련 래퍼 함수: 로그 시작부터 완료까지 자동 처리 + * @param direction 수신/송신 구분 + * @param system 시스템명 + * @param interfaceName 인터페이스명 + * @param requestData 요청 데이터 + * @param processor 실제 비즈니스 로직 함수 + * @returns 처리 결과 + */ +export async function withSoapLogging<T>( + direction: LogDirection, + system: string, + interfaceName: string, + requestData: string, + processor: () => Promise<T> +): Promise<T> { + let logId: number | null = null; + + try { + // 1. 로그 시작 + logId = await startSoapLog(direction, system, interfaceName, requestData); + + // 2. 실제 처리 실행 + const result = await processor(); + + // 3. 성공 로그 완료 + await completeSoapLog(logId, true); + + // 4. 로그 정리 (백그라운드) + cleanupOldSoapLogs().catch(error => + console.error('백그라운드 로그 정리 실패:', error) + ); + + return result; + + } catch (error) { + // 5. 실패 로그 완료 + if (logId !== null) { + await completeSoapLog( + logId, + false, + undefined, + error instanceof Error ? error.message : 'Unknown error' + ); + } + + throw error; + } +}
\ No newline at end of file |
