summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/partners/(partners)/sales-force-test/AF_poc.html118
-rw-r--r--app/[lng]/partners/(partners)/sales-force-test/page.tsx32
-rw-r--r--app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts140
-rw-r--r--app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts287
-rw-r--r--app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_REJECT_FOR_REVISED_PR/route.ts143
5 files changed, 720 insertions, 0 deletions
diff --git a/app/[lng]/partners/(partners)/sales-force-test/AF_poc.html b/app/[lng]/partners/(partners)/sales-force-test/AF_poc.html
new file mode 100644
index 00000000..60e047ed
--- /dev/null
+++ b/app/[lng]/partners/(partners)/sales-force-test/AF_poc.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Salesforce LWC Loader</title>
+</head>
+<body>
+ <script src="https://connect-flow-8014--ps.sandbox.lightning.force.com/lightning/lightning.out.js"></script>
+ <script>
+ const myApiUrl = 'https://pyheroku-d21d18e4f257.herokuapp.com/api/getToken';
+
+ const salesforceUrl = 'https://connect-flow-8014--ps.sandbox.lightning.force.com/';
+ const appName = 'c:zzChatBot_Aura';
+ const componentName = 'c:sj_Chatbot';
+
+ const lwcAttributes = {
+ isDarkMode: false,
+ chatbot_width: '350px',
+ chatbot_height: '600px'
+ };
+
+ // 에러 메시지를 화면에 표시하는 함수
+ function displayError(message) {
+ const errorDiv = document.getElementById('errorMessage');
+ errorDiv.textContent = message;
+ errorDiv.style.display = 'block';
+ }
+
+ const salesforceApiUrl = 'https://connect-flow-8014--ps.sandbox.my.salesforce.com/services/apexrest/summarizeChat/';
+
+ const chatData = {
+ chatlog: `[
+ {
+ "sender": "SHI",
+ "message": "최근 입고된 발전기 제어반 견적서에서 납기 조건 누락을 확인했습니다.
+ 계약 전 꼭 납기 포함한 수정본 제출 부탁드립니다.",
+ },
+ {
+ "sender": "Partner",
+ "message": "네, 누락 사항 확인 후 납기 조건 명시하여 오늘 중으로 재제출 하겠습니다.",
+ },
+ {
+ "sender": "SHI",
+ "message": "감사합니다. 납기 조건은 계약 핵심 조항입니다. 반드시 반영해 주세요.",
+ }
+ ]
+ `
+ };
+
+ // API를 호출하여 토큰을 가져오고 LWC를 로드하는 로직
+ async function getToken(){
+ let accessToken = '';
+ await fetch(myApiUrl, { method: 'POST' }) // 프록시 서버에 POST 요청을 보냅니다.
+ .then(response => {
+ if (!response.ok) {
+ return response.json().then(errorData => {
+ throw new Error(`백엔드 API 에러 (Status: ${response.status}): ${JSON.stringify(errorData)}`);
+ });
+ }
+ return response.json();
+ })
+ .then(data => {
+ accessToken = data.access_token;
+ if (!accessToken) {
+ throw new Error("응답 데이터에 access_token이 없습니다.");
+ }
+
+ console.log("Heroku 프록시를 통해 안전하게 토큰을 받았습니다:", accessToken);
+ })
+ .catch(error => {
+ console.error('전체 프로세스 호출 실패:', error);
+ displayError(`오류가 발생했습니다: ${error.message}`);
+ });
+
+ return accessToken;
+ }
+
+ async function getChatSummary(accessToken){
+ console.log("토큰")
+ console.log(accessToken)
+ fetch(salesforceApiUrl, {
+ method: 'POST',
+ headers: {
+ // Bearer 뒤에 공백이 중요합니다.
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ },
+ // JavaScript 객체를 JSON 문자열로 변환합니다.
+ body: JSON.stringify(chatData)
+ })
+ .then(response => {
+ if (!response.ok) {
+ // API 호출이 실패하면 에러를 발생시킵니다.
+ throw new Error(`Salesforce API 에러: ${response.status}`);
+ }
+ return response.json(); // 응답을 JSON 형태로 파싱합니다.
+ })
+ .then(result => {
+ // 성공적인 응답을 콘솔에 출력합니다.
+ console.log('Salesforce API 응답:', result);
+ })
+ .catch(error => {
+ // API 호출 중 발생한 에러를 처리합니다.
+ console.error('Salesforce API 호출 실패:', error);
+ displayError(`Salesforce API 호출 중 오류가 발생했습니다: ${error.message}`);
+ })
+ }
+
+ async function getMain(){
+ accessToken = await getToken();
+ await getChatSummary(accessToken);
+ }
+
+ getMain();
+ </script>
+</body>
+</html> \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/sales-force-test/page.tsx b/app/[lng]/partners/(partners)/sales-force-test/page.tsx
new file mode 100644
index 00000000..8d6cbfbc
--- /dev/null
+++ b/app/[lng]/partners/(partners)/sales-force-test/page.tsx
@@ -0,0 +1,32 @@
+import path from "path";
+import { promises as fs } from "fs";
+
+type PageProps = {
+ params: { lng: string };
+};
+
+export default async function Page({ params }: PageProps) {
+ const filePath = path.join(
+ process.cwd(),
+ "app",
+ "[lng]",
+ "partners",
+ "(partners)",
+ "sales-force-test",
+ "AF_poc.html"
+ );
+
+ const html = await fs.readFile(filePath, "utf8");
+
+ return (
+ <div className="w-full h-[100vh]">
+ <iframe
+ title="Salesforce LWC Test"
+ className="w-full h-full border-0"
+ srcDoc={html}
+ />
+ </div>
+ );
+}
+
+
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts
new file mode 100644
index 00000000..97ebdb4b
--- /dev/null
+++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts
@@ -0,0 +1,140 @@
+import { NextRequest } from 'next/server';
+import db from '@/db/db';
+import { ZMM_PCR } from '@/db/schema/ECC/ecc';
+import {
+ ToXMLFields,
+ serveWsdl,
+ createXMLParser,
+ extractRequestData,
+ convertXMLToDBData,
+ createSoapResponse,
+ withSoapLogging,
+} from '@/lib/soap/utils';
+import {
+ bulkUpsert
+} from "@/lib/soap/batch-utils";
+
+type PCRData = typeof ZMM_PCR.$inferInsert;
+
+// GET 요청 처리는 ?wsdl 달고 있으면 WSDL 서비스 제공
+export async function GET(request: NextRequest) {
+ const url = new URL(request.url);
+ if (url.searchParams.has('wsdl')) {
+ return serveWsdl('IF_ECC_EVCP_PCR.wsdl');
+ }
+
+ return new Response('Method Not Allowed', { status: 405 });
+}
+
+// POST 요청이 데이터 적재 요구 (SOAP)
+export async function POST(request: NextRequest) {
+ const url = new URL(request.url);
+ if (url.searchParams.has('wsdl')) {
+ return serveWsdl('IF_ECC_EVCP_PCR.wsdl');
+ }
+
+ const body = await request.text();
+
+ // SOAP 로깅 래퍼 함수 사용
+ return withSoapLogging(
+ 'INBOUND',
+ 'ECC',
+ 'IF_ECC_EVCP_PCR',
+ body,
+ async () => {
+ console.log('🚀 PCR 수신 시작, 데이터 길이:', body.length);
+
+ // 1) XML 파싱
+ const parser = createXMLParser(['T_PCR']);
+ const parsedData = parser.parse(body);
+
+ // 2) SOAP Body 또는 루트에서 요청 데이터 추출
+ const requestData = extractRequestData(parsedData, 'IF_ECC_EVCP_PCRReq');
+ if (!requestData) {
+ console.error('유효한 요청 데이터를 찾을 수 없습니다');
+ throw new Error('Missing request data - IF_ECC_EVCP_PCRReq not found');
+ }
+
+ // 3) XML 데이터를 DB 삽입 가능한 형태로 변환
+ const processedData = transformPCRData(requestData as PCRRequestXML);
+
+ // 4) 필수 필드 검증
+ for (const pcrData of processedData) {
+ if (!pcrData.PCR_REQ || !pcrData.PCR_REQ_SEQ || !pcrData.EBELN || !pcrData.EBELP) {
+ throw new Error('Missing required fields: PCR_REQ, PCR_REQ_SEQ, EBELN, EBELP');
+ }
+ }
+
+ // 5) 데이터베이스 저장
+ await saveToDatabase(processedData);
+
+ console.log(`🎉 처리 완료: ${processedData.length}개 PCR 데이터`);
+
+ // 6) 성공 응답 반환
+ return createSoapResponse('http://60.101.108.100/', {
+ 'tns:IF_ECC_EVCP_PCRRes': {
+ EV_TYPE: 'S',
+ },
+ });
+ }
+ ).catch((error) => {
+ // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성
+ return createSoapResponse('http://60.101.108.100/', {
+ 'tns:IF_ECC_EVCP_PCRRes': {
+ EV_TYPE: 'E',
+ EV_MESSAGE:
+ error instanceof Error ? error.message.slice(0, 100) : 'Unknown error',
+ },
+ });
+ });
+}
+
+// -----------------------------------------------------------------------------
+// 데이터 변환 및 저장 관련 유틸리티
+// -----------------------------------------------------------------------------
+
+// XML 구조 타입 정의
+type PCRDataXML = ToXMLFields<Omit<PCRData, 'id' | 'createdAt' | 'updatedAt'>>;
+
+// Root XML Request 타입
+type PCRRequestXML = {
+ CHG_GB?: string;
+ T_PCR?: PCRDataXML[];
+};
+
+// XML -> DB 데이터 변환 함수
+function transformPCRData(requestData: PCRRequestXML): PCRData[] {
+ const pcrItems = requestData.T_PCR || [];
+
+ return pcrItems.map((item) => {
+ // PCR 데이터 변환 (단일 테이블이므로 간단함)
+ const pcrDataConverted = convertXMLToDBData<PCRData>(
+ item as Record<string, string | undefined>,
+ undefined // PCR은 단일 테이블이므로 FK 데이터 불필요
+ );
+
+ return pcrDataConverted;
+ });
+}
+
+// 데이터베이스 저장 함수
+async function saveToDatabase(processedPCRs: PCRData[]) {
+ console.log(`데이터베이스(배치) 저장 시작: ${processedPCRs.length}개 PCR 데이터`);
+
+ try {
+ await db.transaction(async (tx) => {
+ // 필수 키 필드가 있는 데이터만 필터링 (PCR_REQ가 unique key)
+ const validPCRRows = processedPCRs.filter((pcr): pcr is PCRData => !!pcr.PCR_REQ);
+
+ // PCR 테이블에 UPSERT (배치)
+ // PCR_REQ가 unique 키이므로 이를 기준으로 upsert
+ await bulkUpsert(tx, ZMM_PCR, validPCRRows, 'PCR_REQ');
+ });
+
+ console.log(`데이터베이스(배치) 저장 완료: ${processedPCRs.length}개 PCR`);
+ return true;
+ } catch (error) {
+ console.error('데이터베이스(배치) 저장 중 오류 발생:', error);
+ throw error;
+ }
+} \ No newline at end of file
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts
new file mode 100644
index 00000000..44ec3f36
--- /dev/null
+++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts
@@ -0,0 +1,287 @@
+import { NextRequest } from 'next/server';
+import db from '@/db/db';
+import { inArray } from 'drizzle-orm';
+import {
+ ZMM_HD,
+ ZMM_DT,
+ ZMM_PAY,
+ ZMM_KN,
+ ZMM_NOTE,
+ ZMM_NOTE2,
+} from '@/db/schema/ECC/ecc';
+import {
+ ToXMLFields,
+ serveWsdl,
+ createXMLParser,
+ extractRequestData,
+ convertXMLToDBData,
+ processNestedArray,
+ createSoapResponse,
+ withSoapLogging,
+} from '@/lib/soap/utils';
+import {
+ bulkUpsert,
+ bulkReplaceSubTableData
+} from "@/lib/soap/batch-utils";
+
+// 스키마에서 타입 추론
+type HeaderData = typeof ZMM_HD.$inferInsert;
+type DetailData = typeof ZMM_DT.$inferInsert;
+type PaymentData = typeof ZMM_PAY.$inferInsert;
+type AccountData = typeof ZMM_KN.$inferInsert;
+type NoteData = typeof ZMM_NOTE.$inferInsert;
+type Note2Data = typeof ZMM_NOTE2.$inferInsert;
+
+// XML 구조 타입 정의
+type HeaderXML = ToXMLFields<Omit<HeaderData, 'id' | 'createdAt' | 'updatedAt'>>;
+type DetailXML = ToXMLFields<Omit<DetailData, 'id' | 'createdAt' | 'updatedAt'>>;
+type PaymentXML = ToXMLFields<Omit<PaymentData, 'id' | 'createdAt' | 'updatedAt'>>;
+type AccountXML = ToXMLFields<Omit<AccountData, 'id' | 'createdAt' | 'updatedAt'>>;
+type NoteXML = ToXMLFields<Omit<NoteData, 'id' | 'createdAt' | 'updatedAt'>>;
+type Note2XML = ToXMLFields<Omit<Note2Data, 'id' | 'createdAt' | 'updatedAt'>>;
+
+// 처리된 데이터 구조
+interface ProcessedPOData {
+ header: HeaderData;
+ details: DetailData[];
+ payments: PaymentData[];
+ accounts: AccountData[];
+ notes: NoteData[];
+ notes2: Note2Data[];
+}
+
+// ZMM_DT와 연결된 데이터 구조
+interface DetailWithAccounts {
+ detail: DetailData;
+ accounts: AccountData[];
+}
+
+// GET 요청 처리는 ?wsdl 달고 있으면 WSDL 서비스 제공
+export async function GET(request: NextRequest) {
+ const url = new URL(request.url);
+ if (url.searchParams.has('wsdl')) {
+ return serveWsdl('IF_ECC_EVCP_PO_INFORMATION.wsdl');
+ }
+
+ return new Response('Method Not Allowed', { status: 405 });
+}
+
+// POST 요청이 데이터 적재 요구 (SOAP)
+export async function POST(request: NextRequest) {
+ const url = new URL(request.url);
+ if (url.searchParams.has('wsdl')) {
+ return serveWsdl('IF_ECC_EVCP_PO_INFORMATION.wsdl');
+ }
+
+ const body = await request.text();
+
+ // SOAP 로깅 래퍼 함수 사용
+ return withSoapLogging(
+ 'INBOUND',
+ 'ECC',
+ 'IF_ECC_EVCP_PO_INFORMATION',
+ body,
+ async () => {
+ console.log('🚀 PO_INFORMATION 수신 시작, 데이터 길이:', body.length);
+
+ // 1) XML 파싱
+ const parser = createXMLParser(['T_HD', 'T_DT', 'T_PAY', 'T_KN', 'T_NOTE', 'T_NOTE2']);
+ const parsedData = parser.parse(body);
+
+ // 2) SOAP Body 또는 루트에서 요청 데이터 추출
+ const requestData = extractRequestData(parsedData, 'IF_ECC_EVCP_PO_INFORMATIONReq');
+ if (!requestData) {
+ console.error('유효한 요청 데이터를 찾을 수 없습니다');
+ throw new Error('Missing request data - IF_ECC_EVCP_PO_INFORMATIONReq not found');
+ }
+
+ // 3) XML 데이터를 DB 삽입 가능한 형태로 변환
+ const processedData = transformPOData(requestData as PORequestXML);
+
+ // 4) 필수 필드 검증
+ for (const poData of processedData) {
+ if (!poData.header.EBELN) {
+ throw new Error('Missing required field: EBELN in Header');
+ }
+ }
+
+ // 5) 데이터베이스 저장
+ await saveToDatabase(processedData);
+
+ console.log(`🎉 처리 완료: ${processedData.length}개 PO 데이터`);
+
+ // 6) 성공 응답 반환
+ return createSoapResponse('http://60.101.108.100/', {
+ 'tns:IF_ECC_EVCP_PO_INFORMATIONRes': {
+ EV_TYPE: 'S',
+ },
+ });
+ }
+ ).catch((error) => {
+ // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성
+ return createSoapResponse('http://60.101.108.100/', {
+ 'tns:IF_ECC_EVCP_PO_INFORMATIONRes': {
+ EV_TYPE: 'E',
+ EV_MESSAGE:
+ error instanceof Error ? error.message.slice(0, 100) : 'Unknown error',
+ },
+ });
+ });
+}
+
+// -----------------------------------------------------------------------------
+// 데이터 변환 및 저장 관련 유틸리티
+// -----------------------------------------------------------------------------
+
+// Root XML Request 타입
+type PORequestXML = {
+ CHG_GB?: string;
+ T_HD?: HeaderXML[];
+ T_DT?: DetailXML[];
+ T_PAY?: PaymentXML[];
+ T_KN?: AccountXML[];
+ T_NOTE?: NoteXML[];
+ T_NOTE2?: Note2XML[];
+};
+
+// XML -> DB 데이터 변환 함수
+function transformPOData(requestData: PORequestXML): ProcessedPOData[] {
+ const headers = requestData.T_HD || [];
+ const details = requestData.T_DT || [];
+ const payments = requestData.T_PAY || [];
+ const accounts = requestData.T_KN || [];
+ const notes = requestData.T_NOTE || [];
+ const notes2 = requestData.T_NOTE2 || [];
+
+ return headers.map((header) => {
+ const headerKey = header.EBELN || '';
+ const fkData = { EBELN: headerKey };
+
+ // Header 변환
+ const headerConverted = convertXMLToDBData<HeaderData>(
+ header as Record<string, string | undefined>,
+ undefined // Header는 자체 필드만 사용
+ );
+
+ // 해당 Header의 Detail들 필터 후 변환 (ZMM_KN도 함께 처리)
+ const relatedDetails = details.filter((detail) => detail.EBELN === headerKey);
+ const detailsWithAccounts: DetailWithAccounts[] = relatedDetails.map((detail) => {
+ const detailKey = detail.EBELP || '';
+ const detailFkData = { EBELN: headerKey, EBELP: detailKey };
+
+ // Detail 변환
+ const detailConverted = convertXMLToDBData<DetailData>(
+ detail as Record<string, string | undefined>,
+ fkData
+ );
+
+ // 해당 Detail의 Account들 필터 후 변환 (EBELN + EBELP로 매칭)
+ const relatedAccounts = accounts.filter(
+ (account) => account.EBELN === headerKey && account.EBELP === detailKey
+ );
+ const accountsConverted = processNestedArray(
+ relatedAccounts,
+ (account) =>
+ convertXMLToDBData<AccountData>(account as Record<string, string | undefined>, detailFkData),
+ detailFkData
+ );
+
+ return {
+ detail: detailConverted,
+ accounts: accountsConverted,
+ };
+ });
+
+ // Detail들과 Account들을 분리
+ const detailsConverted = detailsWithAccounts.map(d => d.detail);
+ const allAccountsConverted = detailsWithAccounts.flatMap(d => d.accounts);
+
+ // 해당 Header의 Payment들 필터 후 변환
+ const relatedPayments = payments.filter((payment) => payment.EBELN === headerKey);
+ const paymentsConverted = processNestedArray(
+ relatedPayments,
+ (payment) =>
+ convertXMLToDBData<PaymentData>(payment as Record<string, string | undefined>, fkData),
+ fkData
+ );
+
+ // 해당 Header의 Note들 필터 후 변환
+ const relatedNotes = notes.filter((note) => note.EBELN === headerKey);
+ const notesConverted = processNestedArray(
+ relatedNotes,
+ (note) =>
+ convertXMLToDBData<NoteData>(note as Record<string, string | undefined>, fkData),
+ fkData
+ );
+
+ // 해당 Header의 Note2들 필터 후 변환
+ const relatedNotes2 = notes2.filter((note2) => note2.EBELN === headerKey);
+ const notes2Converted = processNestedArray(
+ relatedNotes2,
+ (note2) =>
+ convertXMLToDBData<Note2Data>(note2 as Record<string, string | undefined>, fkData),
+ fkData
+ );
+
+ return {
+ header: headerConverted,
+ details: detailsConverted,
+ payments: paymentsConverted,
+ notes: notesConverted,
+ notes2: notes2Converted,
+ // accounts는 이제 detail과 함께 처리되므로 별도로 저장하지 않음
+ accounts: allAccountsConverted,
+ };
+ });
+}
+
+// 데이터베이스 저장 함수
+async function saveToDatabase(processedPOs: ProcessedPOData[]) {
+ console.log(`데이터베이스(배치) 저장 시작: ${processedPOs.length}개 PO 데이터`);
+
+ try {
+ await db.transaction(async (tx) => {
+ // 1) 부모 테이블 데이터 준비 (키 없는 이상데이터 제거)
+ const headerRows = processedPOs
+ .map((po) => po.header)
+ .filter((h): h is HeaderData => !!h.EBELN);
+
+ const headerKeys = headerRows.map((h) => h.EBELN as string);
+
+ // 2) 하위 테이블 데이터 평탄화
+ const detailRows = processedPOs.flatMap((po) => po.details);
+ const paymentRows = processedPOs.flatMap((po) => po.payments);
+ const accountRows = processedPOs.flatMap((po) => po.accounts);
+ const noteRows = processedPOs.flatMap((po) => po.notes);
+ const note2Rows = processedPOs.flatMap((po) => po.notes2);
+
+ // 3) 부모 테이블 UPSERT (배치)
+ await bulkUpsert(tx, ZMM_HD, headerRows, 'EBELN');
+
+ // 4) 하위 테이블 교체 (배치)
+ await Promise.all([
+ bulkReplaceSubTableData(tx, ZMM_DT, detailRows, ZMM_DT.EBELN, headerKeys),
+ bulkReplaceSubTableData(tx, ZMM_PAY, paymentRows, ZMM_PAY.EBELN, headerKeys),
+ bulkReplaceSubTableData(tx, ZMM_NOTE, noteRows, ZMM_NOTE.EBELN, headerKeys),
+ bulkReplaceSubTableData(tx, ZMM_NOTE2, note2Rows, ZMM_NOTE2.EBELN, headerKeys),
+ ]);
+
+ // 5) ZMM_KN은 ZMM_DT의 서브테이블이므로 별도 처리 (EBELN + EBELP 조합으로 관리)
+ // 기존 ZMM_KN 데이터 삭제 (해당 EBELN의 모든 데이터)
+ await tx.delete(ZMM_KN).where(
+ inArray(ZMM_KN.EBELN, headerKeys)
+ );
+
+ // 새로운 ZMM_KN 데이터 삽입
+ if (accountRows.length > 0) {
+ await tx.insert(ZMM_KN).values(accountRows);
+ }
+ });
+
+ console.log(`데이터베이스(배치) 저장 완료: ${processedPOs.length}개 PO`);
+ return true;
+ } catch (error) {
+ console.error('데이터베이스(배치) 저장 중 오류 발생:', error);
+ throw error;
+ }
+}
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_REJECT_FOR_REVISED_PR/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_REJECT_FOR_REVISED_PR/route.ts
new file mode 100644
index 00000000..daee219a
--- /dev/null
+++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_REJECT_FOR_REVISED_PR/route.ts
@@ -0,0 +1,143 @@
+import { NextRequest } from 'next/server';
+import db from '@/db/db';
+import { T_CHANGE_PR } from '@/db/schema/ECC/ecc';
+import {
+ ToXMLFields,
+ serveWsdl,
+ createXMLParser,
+ extractRequestData,
+ convertXMLToDBData,
+ createSoapResponse,
+ withSoapLogging,
+} from '@/lib/soap/utils';
+// 단일 테이블 insert이므로 batch-utils는 불필요
+
+type ChangeData = typeof T_CHANGE_PR.$inferInsert;
+
+// GET 요청 처리는 ?wsdl 달고 있으면 WSDL 서비스 제공
+export async function GET(request: NextRequest) {
+ const url = new URL(request.url);
+ if (url.searchParams.has('wsdl')) {
+ return serveWsdl('IF_ECC_EVCP_REJECT_FOR_REVISED_PR.wsdl');
+ }
+
+ return new Response('Method Not Allowed', { status: 405 });
+}
+
+// POST 요청이 데이터 적재 요구 (SOAP)
+export async function POST(request: NextRequest) {
+ const url = new URL(request.url);
+ if (url.searchParams.has('wsdl')) {
+ return serveWsdl('IF_ECC_EVCP_REJECT_FOR_REVISED_PR.wsdl');
+ }
+
+ const body = await request.text();
+
+ // SOAP 로깅 래퍼 함수 사용
+ return withSoapLogging(
+ 'INBOUND',
+ 'ECC',
+ 'IF_ECC_EVCP_REJECT_FOR_REVISED_PR',
+ body,
+ async () => {
+ console.log('🚀 REJECT_FOR_REVISED_PR 수신 시작, 데이터 길이:', body.length);
+
+ // 1) XML 파싱
+ const parser = createXMLParser(['T_CHANGE_PR']);
+ const parsedData = parser.parse(body);
+
+ // 2) SOAP Body 또는 루트에서 요청 데이터 추출
+ const requestData = extractRequestData(parsedData, 'IF_ECC_EVCP_REJECT_FOR_REVISED_PRReq');
+ if (!requestData) {
+ console.error('유효한 요청 데이터를 찾을 수 없습니다');
+ throw new Error('Missing request data - IF_ECC_EVCP_REJECT_FOR_REVISED_PRReq not found');
+ }
+
+ // 3) XML 데이터를 DB 삽입 가능한 형태로 변환
+ const processedData = transformRejectData(requestData as RejectRequestXML);
+
+ // 4) 필수 필드 검증
+ for (const changeData of processedData) {
+ if (!changeData.BANFN || !changeData.BANPO || !changeData.ZCHG_NO) {
+ throw new Error('Missing required fields: BANFN, BANPO, ZCHG_NO');
+ }
+ }
+
+ // 5) 데이터베이스 저장
+ await saveToDatabase(processedData);
+
+ console.log(`🎉 처리 완료: ${processedData.length}개 PR 거부 데이터`);
+
+ // 6) 성공 응답 반환
+ return createSoapResponse('http://60.101.108.100/', {
+ 'tns:IF_ECC_EVCP_REJECT_FOR_REVISED_PRRes': {
+ EV_TYPE: 'S',
+ },
+ });
+ }
+ ).catch((error) => {
+ // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성
+ return createSoapResponse('http://60.101.108.100/', {
+ 'tns:IF_ECC_EVCP_REJECT_FOR_REVISED_PRRes': {
+ EV_TYPE: 'E',
+ EV_MESSAGE:
+ error instanceof Error ? error.message.slice(0, 100) : 'Unknown error',
+ },
+ });
+ });
+}
+
+// -----------------------------------------------------------------------------
+// 데이터 변환 및 저장 관련 유틸리티
+// -----------------------------------------------------------------------------
+
+// XML 구조 타입 정의
+type ChangeDataXML = ToXMLFields<Omit<ChangeData, 'id' | 'createdAt' | 'updatedAt'>>;
+
+// Root XML Request 타입
+type RejectRequestXML = {
+ IV_ERDAT?: string; // Reject Date (메타데이터, 저장하지 않음)
+ IV_ERZET?: string; // Reject Time (메타데이터, 저장하지 않음)
+ T_CHANGE_PR?: ChangeDataXML[];
+};
+
+// XML -> DB 데이터 변환 함수
+function transformRejectData(requestData: RejectRequestXML): ChangeData[] {
+ const changeItems = requestData.T_CHANGE_PR || [];
+
+ return changeItems.map((item) => {
+ // Change 데이터 변환 (단일 테이블이므로 간단함)
+ const changeDataConverted = convertXMLToDBData<ChangeData>(
+ item as Record<string, string | undefined>,
+ undefined // 단일 테이블이므로 FK 데이터 불필요
+ );
+
+ return changeDataConverted;
+ });
+}
+
+// 데이터베이스 저장 함수
+async function saveToDatabase(processedChanges: ChangeData[]) {
+ console.log(`데이터베이스(배치) 저장 시작: ${processedChanges.length}개 PR 거부 데이터`);
+
+ try {
+ await db.transaction(async (tx) => {
+ // 필수 키 필드가 있는 데이터만 필터링
+ const validChangeRows = processedChanges.filter((change): change is ChangeData =>
+ !!change.BANFN && !!change.BANPO && !!change.ZCHG_NO
+ );
+
+ // T_CHANGE_PR 테이블에 UPSERT (배치)
+ // BANFN + BANPO + ZCHG_NO 조합을 unique 키로 사용
+ if (validChangeRows.length > 0) {
+ await tx.insert(T_CHANGE_PR).values(validChangeRows);
+ }
+ });
+
+ console.log(`데이터베이스(배치) 저장 완료: ${processedChanges.length}개 PR 거부`);
+ return true;
+ } catch (error) {
+ console.error('데이터베이스(배치) 저장 중 오류 발생:', error);
+ throw error;
+ }
+}