summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-07-02 10:00:07 +0000
committerjoonhoekim <26rote@gmail.com>2025-07-02 10:00:07 +0000
commitdeb2d31dba913a3b831523f41b9bf2e286c53af1 (patch)
tree26b7c440445ef0bb32a54450018b449e0d62d7c9 /app
parentc0c80aa0e43fd70cee6ccb94c66354eb4c25873c (diff)
(김준회) MDG 수신 구조 개선 및 MDG SOAP 송신 액션 & 테스트 페이지 구성
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/admin/mdg/page.tsx248
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts2
-rw-r--r--app/api/(S-ERP)/(MDG)/utils.ts450
13 files changed, 259 insertions, 461 deletions
diff --git a/app/[lng]/admin/mdg/page.tsx b/app/[lng]/admin/mdg/page.tsx
new file mode 100644
index 00000000..27416f25
--- /dev/null
+++ b/app/[lng]/admin/mdg/page.tsx
@@ -0,0 +1,248 @@
+'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'
+
+// 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',
+};
+
+// XML escape helper
+const escapeXml = (unsafe: string) => unsafe.replace(/[<>&'"']/g, (c) => {
+ switch (c) {
+ case '<': return '&lt;';
+ case '>': return '&gt;';
+ case '&': return '&amp;';
+ case '"': return '&quot;';
+ case "'": return '&apos;';
+ default: return c;
+ }
+});
+
+export default function MDGTestPage() {
+ const [formData, setFormData] = useState<Record<string, string>>({});
+ const [fieldDefs, setFieldDefs] = useState<VendorFieldDef[]>([]);
+ const [resultXml, setResultXml] = useState<string>('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ // 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);
+ toast.success('폼이 기본값으로 리셋되었습니다.');
+ };
+
+ // 테스트 송신 실행
+ const handleTestSend = async () => {
+ try {
+ setIsLoading(true);
+
+ // 필수 필드 검증
+ 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(', ')}`);
+ return;
+ }
+
+ // XML 생성
+ const bodyContent = fieldDefs.map(f => {
+ const val = formData[f.name] ?? '';
+ return `<${f.name}>${escapeXml(val)}</${f.name}>`;
+ }).join('\n ');
+
+ const supplierXml = `<SUPPLIER_MASTER>\n ${bodyContent}\n </SUPPLIER_MASTER>`;
+
+ const envelope = `<?xml version="1.0" encoding="UTF-8"?>\n<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:p1=\"http://shi.samsung.co.kr/P2_MD/MDZ\">\n <soap:Header/>\n <soap:Body>\n <p1:MT_P2MD3007_S>\n <P2MD3007_S>\n ${supplierXml}\n </P2MD3007_S>\n </p1:MT_P2MD3007_S>\n </soap:Body>\n</soap:Envelope>`;
+
+ setResultXml(envelope);
+ toast.success('요청 XML이 생성되었습니다. 하단 영역을 확인하세요.');
+
+ } catch (error) {
+ console.error('테스트 송신 실패:', error);
+ toast.error('테스트 송신 중 오류가 발생했습니다.');
+ } 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 시스템으로 테스트 송신합니다
+ </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" />
+ )}
+ 테스트 송신
+ </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 시스템으로의 송신 결과가 여기에 표시됩니다
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ {resultXml ? (
+ <pre className="p-4 bg-muted max-h-96 overflow-auto text-xs whitespace-pre-wrap">
+ {resultXml}
+ </pre>
+ ) : (
+ <div className="p-4 bg-muted rounded-lg">
+ <p className="text-sm text-muted-foreground">
+ 테스트 송신 버튼을 클릭하면 결과가 표시됩니다.
+ </p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
+
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts
index fd4afb86..9d08527b 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts
@@ -27,7 +27,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론
type BpHeaderData = typeof CUSTOMER_MASTER_BP_HEADER.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts
index ffb39895..28757fb5 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts
@@ -18,7 +18,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론
type DeptData = typeof DEPARTMENT_CODE_CMCTB_DEPT_MDG.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts
index b9775765..fc6bc71f 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts
@@ -41,7 +41,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론
type EmpMdgData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts
index 3c58e3a1..22f151b3 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts
@@ -16,7 +16,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론
type EmpRefData = typeof EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts
index 97c2e636..cd1005e7 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts
@@ -20,7 +20,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론 (Insert와 XML을 통합)
type MatlData = typeof EQUP_MASTER_MATL.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts
index 19836c36..21063ff7 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts
@@ -21,7 +21,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론 (Insert와 XML을 통합)
type MatlData = typeof MATERIAL_MASTER_PART_MATL.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts
index ecbc23bc..428cd298 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts
@@ -10,7 +10,7 @@ import {
createSuccessResponse,
ToXMLFields,
withSoapLogging,
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론
type CMCTBMatBseData = typeof MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts
index 4e7cdf35..204dffa3 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts
@@ -19,7 +19,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론 (Insert와 XML을 통합)
type MatlData = typeof MODEL_MASTER_MATL.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts
index 886e4851..987d4002 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts
@@ -30,7 +30,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론
type CctrData = typeof ORGANIZATION_MASTER_HRHMTB_CCTR.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts
index 167c5c5d..93071c69 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts
@@ -13,7 +13,7 @@ import {
createErrorResponse,
createSuccessResponse,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론
type ProjectData = typeof PROJECT_MASTER_CMCTB_PROJ_MAST.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts
index d59246c2..75f8cd62 100644
--- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts
+++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts
@@ -26,7 +26,7 @@ import {
createSuccessResponse,
replaceSubTableData,
withSoapLogging
-} from "../utils";
+} from "@/lib/soap/mdg/utils";
// 스키마에서 직접 타입 추론
type VendorHeaderData = typeof VENDOR_MASTER_BP_HEADER.$inferInsert;
diff --git a/app/api/(S-ERP)/(MDG)/utils.ts b/app/api/(S-ERP)/(MDG)/utils.ts
deleted file mode 100644
index 437988dc..00000000
--- a/app/api/(S-ERP)/(MDG)/utils.ts
+++ /dev/null
@@ -1,450 +0,0 @@
-import { XMLParser } from "fast-xml-parser";
-import { readFileSync } from "fs";
-import { NextResponse } from "next/server";
-import { join } from "path";
-import { eq } 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 {
- // public/wsdl 에서 WSDL 제공함을 가정
- // 이게 WSDL 구현 표준인데, 보안 감사에서 반대한다면 제거
- 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 파서 생성
-// SAP XI 가 자동생성해 보내는 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) {
- const envelopeData = parsedData?.[envelope] as Record<string, unknown> | undefined;
- if (envelopeData?.[body]) {
- const result = extractFromSoapBody(envelopeData[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 변환 함수
-/**
- * XML 데이터를 DB 삽입 가능한 형태로 변환
- *
- * 아키텍처 설계:
- * - 하위 테이블들은 별도의 필수 필드가 없다고 가정 (스키마에서 notNull() 제거 예정)
- * - FK는 항상 최상위 테이블의 unique 필드를 참조
- * - 송신된 XML은 항상 전체 데이터셋을 포함
- * - 최상위 테이블의 unique 필드가 충돌하면 전체 삭제 후 재삽입 처리
- *
- * FK 처리 방식:
- * - XML에 FK 필드가 이미 포함된 경우: XML 값 우선 사용 (예: MATL 인터페이스)
- * - XML에 FK 필드가 없는 경우: 상위에서 전달받은 FK 값 사용 (예: VENDOR 인터페이스)
- * - 이를 통해 다양한 SAP 인터페이스 패턴에 대응
- *
- * @param xmlData XML에서 파싱된 데이터
- * @param fkData 상위 테이블에서 전달받은 FK 데이터
- * @returns DB 삽입 가능한 형태로 변환된 데이터
- */
-export function convertXMLToDBData<T extends Record<string, unknown>>(
- xmlData: Record<string, string | undefined>,
- 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 필드 처리 (XML 우선, 없으면 상위에서 전달받은 값 사용)
- if (fkData) {
- for (const [key, value] of Object.entries(fkData)) {
- // XML에 해당 FK 필드가 없거나 비어있는 경우에만 상위 값 사용
- const existingValue = (result as Record<string, unknown>)[key];
- if (!existingValue || existingValue === null || existingValue === '') {
- (result as Record<string, unknown>)[key] = value;
- }
- // XML에 이미 FK 필드가 있고 값이 있는 경우는 XML 값을 그대로 사용
- }
- }
-
- return result;
-}
-
-// 중첩 배열 처리 함수 (개선된 버전)
-/**
- * 중첩된 배열 데이터를 처리하여 DB 삽입 가능한 형태로 변환
- *
- * 처리 방식:
- * - 하위 테이블 데이터는 FK만 설정하면 됨
- * - 별도의 필수 필드 생성 로직 불필요
- * - 전체 데이터셋 기반으로 삭제 후 재삽입 처리
- *
- * @param items 처리할 배열 데이터
- * @param converter 변환 함수
- * @param fkData FK 데이터
- * @returns 변환된 배열 데이터
- */
-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 기준으로 전체 삭제 후 재삽입
-/**
- * 하위 테이블 데이터를 전체 삭제 후 재삽입하는 함수
- *
- * 처리 전략:
- * - 송신 XML이 전체 데이터셋을 포함한다는 가정하에 설계
- * - 부분 업데이트보다 전체 교체를 통해 데이터 일관성 확보
- * - FK 기준으로 해당 부모 레코드의 모든 하위 데이터 교체
- *
- * 처리 순서:
- * 1. FK 기준으로 기존 데이터 전체 삭제
- * 2. 새로운 데이터 전체 삽입
- *
- * @param tx 트랜잭션 객체
- * @param table 대상 테이블 스키마
- * @param data 삽입할 데이터 배열
- * @param parentField FK 필드명 (일반적으로 'VNDRCD')
- * @param parentValue FK 값 (상위 테이블의 unique 필드 값)
- */
-export async function replaceSubTableData<T extends Record<string, unknown>>(
- tx: Parameters<Parameters<typeof db.transaction>[0]>[0],
- table: any, // Drizzle 테이블 객체 - 복잡한 제네릭 타입으로 인해 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