summaryrefslogtreecommitdiff
path: root/app/api/(S-ERP)/(MDG)/utils.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-06-27 01:25:48 +0000
committerjoonhoekim <26rote@gmail.com>2025-06-27 01:25:48 +0000
commit15b2d4ff61d0339385edd8cc67bf7579fcc2af08 (patch)
treef0c36724855abccf705a9cdcae6fa3efd54d996d /app/api/(S-ERP)/(MDG)/utils.ts
parente9897d416b3e7327bbd4d4aef887eee37751ae82 (diff)
(김준회) MDG SOAP 수신 유틸리티 및 API 엔드포인트, 스키마
Diffstat (limited to 'app/api/(S-ERP)/(MDG)/utils.ts')
-rw-r--r--app/api/(S-ERP)/(MDG)/utils.ts396
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