summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
-rw-r--r--db/schema/ECC/ecc.ts622
-rw-r--r--lib/shi-api/shi-api-utils.ts157
7 files changed, 1439 insertions, 60 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;
+ }
+}
diff --git a/db/schema/ECC/ecc.ts b/db/schema/ECC/ecc.ts
index a1dfe15b..af726678 100644
--- a/db/schema/ECC/ecc.ts
+++ b/db/schema/ECC/ecc.ts
@@ -6,14 +6,13 @@ import {
pgSchema,
} from 'drizzle-orm/pg-core';
-// 적용된 WSDL 파일: IF_ECC_EVCP_PR_INFORMATION.wsdl
-
// WSDL 정의에서 CHG_GB, 응답할 메시지 타입(성공/실패) 및 내용은 메타데이터이다.
// 따라서 로깅 테이블에 저장하고, 별도 스키마를 생성하지는 않는다.
// S-ERP에서 받는 ECC 데이터를 MDG와 구분하기 위해 스키마를 새로 생성한다.
export const ECCSchema = pgSchema('ecc');
+// *** === START === WSDL FILE: IF_ECC_EVCP_PR_INFORMATION.wsdl === ***
// Table: PR_INFORMATION_T_BID_HEADER
export const PR_INFORMATION_T_BID_HEADER = ECCSchema.table(
'PR_INFORMATION_T_BID_HEADER',
@@ -37,7 +36,7 @@ export const PR_INFORMATION_T_BID_ITEM = ECCSchema.table(
'PR_INFORMATION_T_BID_ITEM',
{
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
- ANFNR: varchar({ length: 10 }).notNull(), // Bidding/RFQ Number // From: IF_ECC_EVCP_PR_INFORMATION.wsdl // Required
+ ANFNR: varchar({ length: 10 }).notNull().references(() => PR_INFORMATION_T_BID_HEADER.ANFNR), // Bidding/RFQ Number // From: IF_ECC_EVCP_PR_INFORMATION.wsdl // Required
ANFPS: varchar({ length: 10 }).notNull(), // Item Number of Bidding // From: IF_ECC_EVCP_PR_INFORMATION.wsdl // Required
AUFNR: varchar({ length: 12 }), // Order Number // From: IF_ECC_EVCP_PR_INFORMATION.wsdl
BANFN: varchar({ length: 10 }).notNull(), // Purchase Requisition Number // From: IF_ECC_EVCP_PR_INFORMATION.wsdl // Required
@@ -72,3 +71,620 @@ export const PR_INFORMATION_T_BID_ITEM = ECCSchema.table(
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}
);
+// *** === END === WSDL FILE: IF_ECC_EVCP_PR_INFORMATION.wsdl === ***
+
+
+
+
+// *** === START === WSDL FILE: IF_ECC_EVCP_PCR.wsdl === ***
+// PCR (Purchase Change Request) 테이블
+export const ZMM_PCR = ECCSchema.table('ZMM_PCR', {
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
+
+ // SEQ,Table,Field,M/O,Type,Size,Description
+ // 1,ZMM_PCR,PCR_REQ,M,CHAR,10,PCR 요청번호
+ PCR_REQ: varchar({ length: 255 }).notNull().unique(),
+ // 2,ZMM_PCR,PCR_REQ_SEQ,M,NUMC,5,PCR 요청순번
+ PCR_REQ_SEQ: varchar({ length: 255 }).notNull(),
+ // 3,ZMM_PCR,PCR_REQ_DATE,M,DATS,8,PCR 요청일자
+ PCR_REQ_DATE: varchar({ length: 255 }).notNull(),
+ // 4,ZMM_PCR,EBELN,M,CHAR,10,구매오더
+ EBELN: varchar({ length: 255 }).notNull(),
+ // 5,ZMM_PCR,EBELP,M,NUMC,5,구매오더 품번
+ EBELP: varchar({ length: 255 }).notNull(),
+ // 6,ZMM_PCR,PCR_TYPE,M,CHAR,2,"물량/Spec 변경 Type : Q, W, S, QW"
+ PCR_TYPE: varchar({ length: 255 }).notNull(),
+ // 7,ZMM_PCR,PSPID,,CHAR,24,프로젝트
+ PSPID: varchar({ length: 255 }),
+ // 8,ZMM_PCR,BANFN,M,CHAR,10,구매요청
+ BANFN: varchar({ length: 255 }).notNull(),
+ // 9,ZMM_PCR,BNFPO,M,NUMC,5,구매요청 품번
+ BNFPO: varchar({ length: 255 }).notNull(),
+ // 10,ZMM_PCR,MATNR,,CHAR,18,자재번호
+ MATNR: varchar({ length: 255 }),
+ // 11,ZMM_PCR,MAKTX,,CHAR,40,자재명
+ MAKTX: varchar({ length: 255 }),
+ // 12,ZMM_PCR,ZZSPEC,,CHAR,255,Specification
+ ZZSPEC: varchar({ length: 255 }),
+ // 13,ZMM_PCR,ZSPEC_NUM,,CHAR,25,POS
+ ZSPEC_NUM: varchar({ length: 255 }),
+ // 14,ZMM_PCR,QTY_B,,QUAN,"13,3",변경 전 수량
+ QTY_B: varchar({ length: 255 }),
+ // 15,ZMM_PCR,QTY_A,,QUAN,"13,3",변경 후 수량
+ QTY_A: varchar({ length: 255 }),
+ // 16,ZMM_PCR,MEINS,,UNIT,3,단위
+ MEINS: varchar({ length: 255 }),
+ // 17,ZMM_PCR,T_WEIGHT_B,,QUAN,"13,3",변경 전 Total 중량
+ T_WEIGHT_B: varchar({ length: 255 }),
+ // 18,ZMM_PCR,T_WEIGHT_A,,QUAN,"13,3",변경 후 Total 중량
+ T_WEIGHT_A: varchar({ length: 255 }),
+ // 19,ZMM_PCR,MEINS_W,,UNIT,3,중량 단위
+ MEINS_W: varchar({ length: 255 }),
+ // 20,ZMM_PCR,S_WEIGHT_B,,QUAN,"13,3",변경 전 사급 중량
+ S_WEIGHT_B: varchar({ length: 255 }),
+ // 21,ZMM_PCR,S_WEIGHT_A,,QUAN,"13,3",변경 후 사급 중량
+ S_WEIGHT_A: varchar({ length: 255 }),
+ // 22,ZMM_PCR,C_WEIGHT_B,,QUAN,"13,3",변경 전 도급 중량
+ C_WEIGHT_B: varchar({ length: 255 }),
+ // 23,ZMM_PCR,C_WEIGHT_A,,QUAN,"13,3",변경 후 도급 중량
+ C_WEIGHT_A: varchar({ length: 255 }),
+ // 24,ZMM_PCR,ZACC_DT,,DATS,8,구매담당자 PR 접수일
+ ZACC_DT: varchar({ length: 255 }),
+ // 25,ZMM_PCR,ERDAT,,DATS,8,물량 변경일
+ ERDAT: varchar({ length: 255 }),
+ // 26,ZMM_PCR,DEPTCD,,CHAR,10,설계부서
+ DEPTCD: varchar({ length: 255 }),
+ // 27,ZMM_PCR,DEPTNM,,CHAR,60,설계부서명
+ DEPTNM: varchar({ length: 255 }),
+ // 28,ZMM_PCR,EMPID,,CHAR,20,설계담당자ID
+ EMPID: varchar({ length: 255 }),
+ // 29,ZMM_PCR,NAME,,CHAR,70,설계담당명
+ NAME: varchar({ length: 255 }),
+ // 30,ZMM_PCR,LIFNR,M,CHAR,10,공급업체
+ LIFNR: varchar({ length: 255 }),
+ // 31,ZMM_PCR,NAME1,,CHAR,35,공급업체명
+ NAME1: varchar({ length: 255 }),
+ // 32,ZMM_PCR,ZPROC_IND,,CHAR,1,PR 상태
+ ZPROC_IND: varchar({ length: 255 }),
+ // 33,ZMM_PCR,LFDAT,,DATS,8,PR 납품일
+ LFDAT: varchar({ length: 255 }),
+ // 34,ZMM_PCR,WAERS,M,CUKY,5,PO 통화
+ WAERS: varchar({ length: 255 }),
+ // 35,ZMM_PCR,NETPR,M,CURR,"13,2",PO 단가
+ NETPR: varchar({ length: 255 }),
+ // 36,ZMM_PCR,PEINH,,DEC,5,"Price Unit, 수량에 대한 PER 당 단가"
+ PEINH: varchar({ length: 255 }),
+ // 37,ZMM_PCR,NETWR,M,CURR,"13,2",PO 금액
+ NETWR: varchar({ length: 255 }),
+ // 38,ZMM_PCR,POSID,,CHAR,24,WBS
+ POSID: varchar({ length: 255 }),
+ // 39,ZMM_PCR,EKGRP,,CHAR,3,구매그룹
+ EKGRP: varchar({ length: 255 }),
+ // 40,ZMM_PCR,EKNAM,,CHAR,18,구매그룹명
+ EKNAM: varchar({ length: 255 }),
+ // 41,ZMM_PCR,ZCHG_NO,,CHAR,10,(ECC 내부목적) PR 수정번호
+ ZCHG_NO: varchar({ length: 255 }),
+ // 42,ZMM_PCR,DOKNR,,CHAR,25,(ECC 내부목적) 도면번호
+ DOKNR: varchar({ length: 255 }),
+ // 43,ZMM_PCR,DOKAR,,CHAR,3,(ECC 내부목적) 도면문서Type
+ DOKAR: varchar({ length: 255 }),
+ // 44,ZMM_PCR,DOKTL,,CHAR,3,(ECC 내부목적) 도면문서Part
+ DOKTL: varchar({ length: 255 }),
+ // 45,ZMM_PCR,DOKVR,,CHAR,2,(ECC 내부목적) 도면문서버젼
+ DOKVR: varchar({ length: 255 }),
+ // 46,ZMM_PCR,ZAEDAT,,DATS,8,(ECC 내부목적) 도면변경일
+ ZAEDAT: varchar({ length: 255 }),
+ // 47,ZMM_PCR,WERKS,,CHAR,4,PLANT
+ WERKS: varchar({ length: 255 }),
+ // 48,ZMM_PCR,REQUEST_CD,,CHAR,10,Request 코드
+ REQUEST_CD: varchar({ length: 255 }),
+ // 49,ZMM_PCR,REQUEST_RSN,,CHAR,50,Request 사유
+ REQUEST_RSN: varchar({ length: 255 }),
+
+ // 수신 처리 완료하고, 응답해줄 형식은 다음과 같다.
+ // 하나의 요청이 여러 데이터셋을 가지고 있는데, 응답은 하나의 요청에 대해 한번만 하며, 에 저장하지 않음
+ // 50,ZMM_RT (수신측 응답),PCR_REQ,M,CHAR,10,PCR 요청번호
+ // 51,ZMM_RT (수신측 응답),PCR_REQ_SEQ,M,NUMC,5,PCR 요청순번
+ // 52,ZMM_RT (수신측 응답),EBELN,M,CHAR,10,구매오더
+ // 53,ZMM_RT (수신측 응답),EBELP,M,NUMC,5,구매오더 품번
+ // 54,ZMM_RT (수신측 응답),MSGTY,,CHAR,1,Message Type
+ // 55,ZMM_RT (수신측 응답),MSGTXT,,CHAR,100,Message Text
+
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+// *** === END === WSDL FILE: IF_ECC_EVCP_PCR.wsdl === ***
+
+
+
+
+// *** === START === WSDL FILE: IF_ECC_EVCP_PO_INFORMATION.wsdl === ***
+export const ZMM_HD = ECCSchema.table('ZMM_HD', {
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
+
+ // SEQ,Table,Field,M/O,Type,Size,Description
+ // 1,ZMM_HD,EBELN,M,CHAR,10,구매오더번호
+ EBELN: varchar({ length: 255 }).notNull().unique(),
+ // 2,ZMM_HD,BUKRS,M,CHAR,4,회사코드
+ BUKRS: varchar({ length: 255 }),
+ // 3,ZMM_HD,BSTYP,M,CHAR,1,구매문서범주
+ BSTYP: varchar({ length: 255 }),
+ // 4,ZMM_HD,BSART,M,CHAR,4,구매문서유형
+ BSART: varchar({ length: 255 }),
+ // 5,ZMM_HD,LOEKZ,M,CHAR,1,구매문서삭제지시자
+ LOEKZ: varchar({ length: 255 }),
+ // 6,ZMM_HD,AEDAT,M,DATS,8,생성일자
+ AEDAT: varchar({ length: 255 }),
+ // 7,ZMM_HD,ERNAM,M,CHAR,13,생성자ID
+ ERNAM: varchar({ length: 255 }),
+ // 8,ZMM_HD,LIFNR,M,CHAR,10,VENDOR코드
+ LIFNR: varchar({ length: 255 }),
+ // 9,ZMM_HD,ZTERM,M,CHAR,4,지급조건코드
+ ZTERM: varchar({ length: 255 }),
+ // 10,ZMM_HD,EKORG,M,CHAR,4,구매조직코드
+ EKORG: varchar({ length: 255 }),
+ // 11,ZMM_HD,EKGRP,M,CHAR,3,구매그룹코드
+ EKGRP: varchar({ length: 255 }),
+ // 12,ZMM_HD,WKURS,M,DEC,"9,5",환율
+ WKURS: varchar({ length: 255 }),
+ // 13,ZMM_HD,BEDAT,M,DATS,8,구매증빙일자
+ BEDAT: varchar({ length: 255 }),
+ // 14,ZMM_HD,INCO1,M,CHAR,3,인도조건코드
+ INCO1: varchar({ length: 255 }),
+ // 15,ZMM_HD,ZSHIPMT_PLC_CD,M,CHAR,5,선적지코드
+ ZSHIPMT_PLC_CD: varchar({ length: 255 }),
+ // 16,ZMM_HD,ZUNLD_PLC_CD,M,CHAR,5,하역지코드
+ ZUNLD_PLC_CD: varchar({ length: 255 }),
+ // 17,ZMM_HD,ZIND_CD,M,CHAR,2,증감코드
+ ZIND_CD: varchar({ length: 255 }),
+ // 18,ZMM_HD,ZDAMT_DD_SUBRT,M,DEC,"6,2",지체상금일일공제율
+ ZDAMT_DD_SUBRT: varchar({ length: 255 }),
+ // 19,ZMM_HD,ZMAX_SUBRT,M,DEC,"6,2",최대공제율
+ ZMAX_SUBRT: varchar({ length: 255 }),
+ // 20,ZMM_HD,ZCNRT_GRNT_CD,M,CHAR,1,계약보증코드
+ ZCNRT_GRNT_CD: varchar({ length: 255 }),
+ // 21,ZMM_HD,ZDFCT_GRNT_CD,M,CHAR,1,하자보증코드
+ ZDFCT_GRNT_CD: varchar({ length: 255 }),
+ // 22,ZMM_HD,ZGRNT_PRD_CD,M,CHAR,1,보증기간코드
+ ZGRNT_PRD_CD: varchar({ length: 255 }),
+ // 23,ZMM_HD,ZPAMT_YN,M,CHAR,1,선급금여부
+ ZPAMT_YN: varchar({ length: 255 }),
+ // 24,ZMM_HD,ZBGT_AMT,M,CURR,"17,2","예산금액, ZBTG_CURR"
+ ZBGT_AMT: varchar({ length: 255 }),
+ // 25,ZMM_HD,ZBGT_CURR,M,CUKY,3,예산금액 통화키
+ ZBGT_CURR: varchar({ length: 255 }),
+ // 26,ZMM_HD,ZPO_AMT,M,CURR,"17,2",발주금액
+ ZPO_AMT: varchar({ length: 255 }),
+ // 27,ZMM_HD,ZPO_AMT_KRW,M,CURR,"17,2",발주금액 KRW
+ ZPO_AMT_KRW: varchar({ length: 255 }),
+ // 28,ZMM_HD,ZPO_CURR,M,CUKY,5,통화키
+ ZPO_CURR: varchar({ length: 255 }),
+ // 29,ZMM_HD,ZCHG_PO_DT,M,DATS,8,변경발주일자
+ ZCHG_PO_DT: varchar({ length: 255 }),
+ // 30,ZMM_HD,ZPO_CNFM_STAT,M,CHAR,1,구매오더확인상태
+ ZPO_CNFM_STAT: varchar({ length: 255 }),
+ // 31,ZMM_HD,ZOWN_AGR_IND_YN,M,CHAR,1,선주승인필요여부
+ ZOWN_AGR_IND_YN: varchar({ length: 255 }),
+ // 32,ZMM_HD,ZELC_AGR_DT,M,DATS,8,전자승인일자
+ ZELC_AGR_DT: varchar({ length: 255 }),
+ // 33,ZMM_HD,ZELC_AGR_TM,M,TIMS,6,전자승인시간
+ ZELC_AGR_TM: varchar({ length: 255 }),
+ // 34,ZMM_HD,ZELC_CNRT_ND_YN,M,CHAR,1,전자계약필요여부
+ ZELC_CNRT_ND_YN: varchar({ length: 255 }),
+ // 35,ZMM_HD,ZPO_DT,M,DATS,8,발주일자
+ ZPO_DT: varchar({ length: 255 }),
+ // 36,ZMM_HD,ZPLN_INO_GB,M,CHAR,1,계획내외구분
+ ZPLN_INO_GB: varchar({ length: 255 }),
+ // 37,ZMM_HD,ZECAL_BSE,M,CHAR,1,정산기준
+ ZECAL_BSE: varchar({ length: 255 }),
+ // 38,ZMM_HD,ZWGT_ECAL_GB,M,CHAR,1,중량정산구분
+ ZWGT_ECAL_GB: varchar({ length: 255 }),
+ // 39,ZMM_HD,ZPO_TRANS_DT,M,DATS,8,발주전송일자
+ ZPO_TRANS_DT: varchar({ length: 255 }),
+ // 40,ZMM_HD,ZPO_TRANS_CANC,M,DATS,1,전송여부지시자
+ ZPO_TRANS_CANC: varchar({ length: 255 }),
+ // 41,ZMM_HD,ZVST_TMS,M,NUMC,9,방문횟수
+ ZVST_TMS: varchar({ length: 255 }),
+ // 42,ZMM_HD,ZSVC_WK_PRD,M,NUMC,9,SE작업일수
+ ZSVC_WK_PRD: varchar({ length: 255 }),
+ // 43,ZMM_HD,ZDT_EXCS_AMT,M,CURR,"17,2",일초과금액1
+ ZDT_EXCS_AMT: varchar({ length: 255 }),
+ // 44,ZMM_HD,ZDT_EXCS_AMT2,M,CURR,"17,2",일초과금액2
+ ZDT_EXCS_AMT2: varchar({ length: 255 }),
+ // 45,ZMM_HD,ZDT_EXCS_AMT3,M,CURR,"17,2",일초과금액3
+ ZDT_EXCS_AMT3: varchar({ length: 255 }),
+ // 46,ZMM_HD,ZSVC_CNRT_CUR,M,CUKY,5,SE계약통화
+ ZSVC_CNRT_CUR: varchar({ length: 255 }),
+ // 47,ZMM_HD,ZPAY_GB,M,CHAR,1,기타비용처리구분
+ ZPAY_GB: varchar({ length: 255 }),
+ // 48,ZMM_HD,ZFULL_COMM,M,CHAR,1,수수료전체부담여부
+ ZFULL_COMM: varchar({ length: 255 }),
+ // 49,ZMM_HD,PSPID,M,CHAR,24,프로젝트 번호
+ PSPID: varchar({ length: 255 }),
+ // 50,ZMM_HD,ZCON_NO,M,CHAR,10,구매통합번호
+ ZCON_NO: varchar({ length: 255 }),
+ // 51,ZMM_HD,ZTITLE,M,CHAR,90,발주제목
+ ZTITLE: varchar({ length: 255 }),
+ // 52,ZMM_HD,ZPO_VER,M,NUMC,2,발주버전
+ ZPO_VER: varchar({ length: 255 }),
+ // 53,ZMM_HD,ITEM_CATEGORY,M,CHAR,2,선물환 Item Category
+ ITEM_CATEGORY: varchar({ length: 255 }),
+ // 54,ZMM_HD,LTEXT,M,CHAR,60,선물환 Item Category 명
+ LTEXT: varchar({ length: 255 }),
+ // 55,ZMM_HD,ITEM_NO,M,CHAR,3,PO의 ITEM 수 (인터페이스검증)
+ ITEM_NO: varchar({ length: 255 }),
+ // 56,ZMM_HD,USECD,M,CHAR,20,사용코드
+ USECD: varchar({ length: 255 }),
+ // 57,ZMM_HD,ETC_2,M,CHAR,100,확장2
+ ETC_2: varchar({ length: 255 }),
+ // 58,ZMM_HD,ETC_3,M,CHAR,100,확장3
+ ETC_3: varchar({ length: 255 }),
+ // 59,ZMM_HD,ETC_4,M,CHAR,100,확장4
+ ETC_4: varchar({ length: 255 }),
+ // 60,ZMM_HD,ETC_5,M,CHAR,100,확장5
+ ETC_5: varchar({ length: 255 }),
+ // 61,ZMM_HD,ETC_6,M,CHAR,100,확장6
+ ETC_6: varchar({ length: 255 }),
+ // 62,ZMM_HD,ETC_7,M,CHAR,100,확장7
+ ETC_7: varchar({ length: 255 }),
+ // 63,ZMM_HD,ETC_8,M,CHAR,100,확장8
+ ETC_8: varchar({ length: 255 }),
+ // 64,ZMM_HD,ETC_9,M,CHAR,100,확장9
+ ETC_9: varchar({ length: 255 }),
+ // 65,ZMM_HD,ETC_10,M,CHAR,100,확장10
+ ETC_10: varchar({ length: 255 }),
+ // 66,ZMM_HD,ZDLV_PRICE_T,,CHAR,1,"납품대금연동제대상여부 (Y:대상, N:미대상, 공백:미해당)"
+ ZDLV_PRICE_T: varchar({ length: 255 }),
+ // 67,ZMM_HD,ZWEBELN,,CHAR,10,서면계약번호
+ ZWEBELN: varchar({ length: 255 }),
+ // 68,ZMM_HD,ZVER_NO,,NUMC,3,서면계약차수
+ ZVER_NO: varchar({ length: 255 }),
+
+ // 응답은 전체 요청에 대해 하나의 메시지만 전송하며, 데이터베이스에 저장하지 않음
+ // 191,ZMM_RT,EBELN,M,CHAR,10,구매오더번호
+ // 192,ZMM_RT,RT_CODE,M,CHAR,1,IF상태
+ // 193,ZMM_RT,RT_TEXT,M,CHAR,100,IF메세지
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+})
+
+export const ZMM_PAY = ECCSchema.table('ZMM_PAY', {
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
+
+ // FK
+ // 1,ZMM_HD,EBELN,M,CHAR,10,구매오더번호
+ EBELN: varchar({ length: 255 }).notNull().references(() => ZMM_HD.EBELN),
+
+ // 69,ZMM_HD/ZMM_PAY,ZPAYSEQ,M,CHAR,2,선급금차수
+ ZPAYSEQ: varchar({ length: 255 }).notNull(),
+ // 70,ZMM_HD/ZMM_PAY,ZADVTYP,M,CHAR,1,선급금타입
+ ZADVTYP: varchar({ length: 255 }),
+ // 71,ZMM_HD/ZMM_PAY,ZDWPRT,M,NUMC,3,선급금비율
+ ZDWPRT: varchar({ length: 255 }),
+ // 72,ZMM_HD/ZMM_PAY,ZDWPAMT,M,CURR,"17,2",선급금
+ ZDWPAMT: varchar({ length: 255 }),
+ // 73,ZMM_HD/ZMM_PAY,ZDWPDAT,M,DATS,8,지불계획일자
+ ZDWPDAT: varchar({ length: 255 }),
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+export const ZMM_DT = ECCSchema.table('ZMM_DT', {
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
+
+ // FK
+ // 1,ZMM_HD,EBELN,M,CHAR,10,구매오더번호
+ EBELN: varchar({ length: 255 }).notNull().references(() => ZMM_HD.EBELN),
+
+ // 74,ZMM_HD/ZMM_DT,EBELP,M,NUMC,5,구매오더품목번호
+ EBELP: varchar({ length: 255 }),
+ // 75,ZMM_HD/ZMM_DT,LOEKZ,M,CHAR,1,구매문서삭제지시자
+ LOEKZ: varchar({ length: 255 }),
+ // 76,ZMM_HD/ZMM_DT,AEDAT,M,DATS,8,변경일자
+ AEDAT: varchar({ length: 255 }),
+ // 77,ZMM_HD/ZMM_DT,MAKTX,M,CHAR,120,자재내역
+ MAKTX: varchar({ length: 255 }),
+ // 78,ZMM_HD/ZMM_DT,MATKL,M,CHAR,9,자재그룹
+ MATKL: varchar({ length: 255 }),
+ // 79,ZMM_HD/ZMM_DT,MATNR,M,CHAR,18,자재코드
+ MATNR: varchar({ length: 255 }),
+ // 80,ZMM_HD/ZMM_DT,WERKS,M,CHAR,4,플랜트코드
+ WERKS: varchar({ length: 255 }),
+ // 81,ZMM_HD/ZMM_DT,LGORT,M,CHAR,10,저장위치
+ LGORT: varchar({ length: 255 }),
+ // 82,ZMM_HD/ZMM_DT,BEDNR,M,CHAR,10,요청추적번호
+ BEDNR: varchar({ length: 255 }),
+ // 83,ZMM_HD/ZMM_DT,MENGE,M,QUAN,"13,3",구매오더수량
+ MENGE: varchar({ length: 255 }),
+ // 84,ZMM_HD/ZMM_DT,NETPR,M,CURR,"17,2",구매단가
+ NETPR: varchar({ length: 255 }),
+ // 85,ZMM_HD/ZMM_DT,PEINH,M,DEC,5,가격단위값
+ PEINH: varchar({ length: 255 }),
+ // 86,ZMM_HD/ZMM_DT,NETWR,M,CURR,"17,2",오더정가
+ NETWR: varchar({ length: 255 }),
+ // 87,ZMM_HD/ZMM_DT,BRTWR,M,CURR,"17,2",오더총액
+ BRTWR: varchar({ length: 255 }),
+ // 88,ZMM_HD/ZMM_DT,WEBAZ,M,DEC,3,입고소요일수
+ WEBAZ: varchar({ length: 255 }),
+ // 89,ZMM_HD/ZMM_DT,MWSKZ,M,CHAR,2,매출부가가치세코드
+ MWSKZ: varchar({ length: 255 }),
+ // 90,ZMM_HD/ZMM_DT,INSMK,M,CHAR,1,재고유형
+ INSMK: varchar({ length: 255 }),
+ // 91,ZMM_HD/ZMM_DT,BWTAR,M,CHAR,10,평가유형
+ BWTAR: varchar({ length: 255 }),
+ // 92,ZMM_HD/ZMM_DT,BWTTY,M,CHAR,1,평가범주
+ BWTTY: varchar({ length: 255 }),
+ // 93,ZMM_HD/ZMM_DT,ELIKZ,M,CHAR,1,납품완료지시자
+ ELIKZ: varchar({ length: 255 }),
+ // 94,ZMM_HD/ZMM_DT,EREKZ,M,CHAR,1,최종송장지시자
+ EREKZ: varchar({ length: 255 }),
+ // 95,ZMM_HD/ZMM_DT,TWRKZ,M,CHAR,1,분할송장지시자
+ TWRKZ: varchar({ length: 255 }),
+ // 96,ZMM_HD/ZMM_DT,REPOS,M,CHAR,1,송장수령지시자
+ REPOS: varchar({ length: 255 }),
+ // 97,ZMM_HD/ZMM_DT,WEBRE,M,CHAR,1,GR송장검증지시자
+ WEBRE: varchar({ length: 255 }),
+ // 98,ZMM_HD/ZMM_DT,KNTTP,M,CHAR,1,계정지정범주
+ KNTTP: varchar({ length: 255 }),
+ // 99,ZMM_HD/ZMM_DT,NTGEW,M,QUAN,"13,3",순중량
+ NTGEW: varchar({ length: 255 }),
+ // 100,ZMM_HD/ZMM_DT,GEWEI,M,UNIT,3,중량단위
+ GEWEI: varchar({ length: 255 }),
+ // 101,ZMM_HD/ZMM_DT,BRGEW,M,QUAN,"15,3",총중량
+ BRGEW: varchar({ length: 255 }),
+ // 102,ZMM_HD/ZMM_DT,VOLUM,M,QUAN,"15,3",볼륨
+ VOLUM: varchar({ length: 255 }),
+ // 103,ZMM_HD/ZMM_DT,VOLEH,M,UNIT,3,볼륨단위
+ VOLEH: varchar({ length: 255 }),
+ // 104,ZMM_HD/ZMM_DT,BANFN,M,CHAR,10,구매요청번호
+ BANFN: varchar({ length: 255 }),
+ // 105,ZMM_HD/ZMM_DT,BNFPO,M,NUMC,5,구매요청품목번호
+ BNFPO: varchar({ length: 255 }),
+ // 106,ZMM_HD/ZMM_DT,UPTYP,M,CHAR,1,하위품목범주
+ UPTYP: varchar({ length: 255 }),
+ // 107,ZMM_HD/ZMM_DT,UPVOR,M,CHAR,1,하위품목존재여부
+ UPVOR: varchar({ length: 255 }),
+ // 108,ZMM_HD/ZMM_DT,ZPO_DLV_DT,M,DATS,8,PO납기일자
+ ZPO_DLV_DT: varchar({ length: 255 }),
+ // 109,ZMM_HD/ZMM_DT,ZSHIP_DT,M,DATS,8,생산소요선적일자
+ ZSHIP_DT: varchar({ length: 255 }),
+ // 110,ZMM_HD/ZMM_DT,ZDST_CD,M,CHAR,4,강재도착지코드
+ ZDST_CD: varchar({ length: 255 }),
+ // 111,ZMM_HD/ZMM_DT,ZRCV_DT,M,DATS,8,구매접수일자
+ ZRCV_DT: varchar({ length: 255 }),
+ // 112,ZMM_HD/ZMM_DT,ZCON_NO,M,CHAR,10,구매통합번호
+ ZCON_NO: varchar({ length: 255 }),
+ // 113,ZMM_HD/ZMM_DT,ZCON_IND,M,CHAR,1,시리즈구분
+ ZCON_IND: varchar({ length: 255 }),
+ // 114,ZMM_HD/ZMM_DT,ZCHAR_CD,M,CHAR,1,"물성코드,풍력 일련번호 처리여부"
+ ZCHAR_CD: varchar({ length: 255 }),
+ // 115,ZMM_HD/ZMM_DT,ZMAT_AREA,M,QUAN,"13,3",자재면적
+ ZMAT_AREA: varchar({ length: 255 }),
+ // 116,ZMM_HD/ZMM_DT,ZSZ,M,CHAR,50,품목사이즈
+ ZSZ: varchar({ length: 255 }),
+ // 117,ZMM_HD/ZMM_DT,ZAF_ECAL_AMT,M,CURR,"17,2","사후정산금액(참고: NETWR), ZPO_CURR"
+ ZAF_ECAL_AMT: varchar({ length: 255 }),
+ // 118,ZMM_HD/ZMM_DT,ZPLN_ST_DT,M,DATS,8,예정시작일자
+ ZPLN_ST_DT: varchar({ length: 255 }),
+ // 119,ZMM_HD/ZMM_DT,ZPLN_ED_DT,M,DATS,8,예정종료일자
+ ZPLN_ED_DT: varchar({ length: 255 }),
+ // 120,ZMM_HD/ZMM_DT,PSPID,M,CHAR,24,프로젝트번호
+ PSPID: varchar({ length: 255 }),
+ // 121,ZMM_HD/ZMM_DT,ZUSD_BGT,M,CURR,"17,2",미화예산
+ ZUSD_BGT: varchar({ length: 255 }),
+ // 122,ZMM_HD/ZMM_DT,ZKRW_BGT,M,CURR,"17,2",원화예산
+ ZKRW_BGT: varchar({ length: 255 }),
+ // 123,ZMM_HD/ZMM_DT,ZDLV_CNTLR,M,CHAR,3,조달담당자코드
+ ZDLV_CNTLR: varchar({ length: 255 }),
+ // 124,ZMM_HD/ZMM_DT,ANFNR,M,CHAR,10,RFQ번호
+ ANFNR: varchar({ length: 255 }),
+ // 125,ZMM_HD/ZMM_DT,ANFPS,M,NUMC,5,RFQ품목번호
+ ANFPS: varchar({ length: 255 }),
+ // 126,ZMM_HD/ZMM_DT,KONNR,M,CHAR,10,계약번호
+ KONNR: varchar({ length: 255 }),
+ // 127,ZMM_HD/ZMM_DT,KTPNR,M,NUMC,5,계약항목번호
+ KTPNR: varchar({ length: 255 }),
+ // 128,ZMM_HD/ZMM_DT,ZCR_NO,M,CHAR,40,CR번호
+ ZCR_NO: varchar({ length: 255 }),
+ // 129,ZMM_HD/ZMM_DT,ZCR_AMT,M,CURR,"17,2",EXTRA CREDIT 금액
+ ZCR_AMT: varchar({ length: 255 }),
+ // 130,ZMM_HD/ZMM_DT,ZRT_CUR,M,CUKY,3,실적통화
+ ZRT_CUR: varchar({ length: 255 }),
+ // 131,ZMM_HD/ZMM_DT,ZRT_AMT,M,CURR,"17,2","실적금액, ZRT_CURR"
+ ZRT_AMT: varchar({ length: 255 }),
+ // 132,ZMM_HD/ZMM_DT,ZPO_UNIT,M,UNIT,3,구매오더수량단위
+ ZPO_UNIT: varchar({ length: 255 }),
+ // 133,ZMM_HD/ZMM_DT,ZREF_NETPR,M,CURR,"17,2","참조단가, ZPO_CURR"
+ ZREF_NETPR: varchar({ length: 255 }),
+ // 134,ZMM_HD/ZMM_DT,ZNETPR,M,CURR,"17,2","발주단가, ZPO_CURR"
+ ZNETPR: varchar({ length: 255 }),
+ // 135,ZMM_HD/ZMM_DT,BPRME,M,UNIT,3,구매단가단위
+ BPRME: varchar({ length: 255 }),
+ // 136,ZMM_HD/ZMM_DT,ZDISPLN,M,CHAR,1,설계기능
+ ZDISPLN: varchar({ length: 255 }),
+ // 137,ZMM_HD/ZMM_DT,ZORCT_CNRT_KRW,M,CURR,"17,2",외주비계약KRW
+ ZORCT_CNRT_KRW: varchar({ length: 255 }),
+ // 138,ZMM_HD/ZMM_DT,ZORCT_CNRT_USD,M,CURR,"17,2",외주비계약USD
+ ZORCT_CNRT_USD: varchar({ length: 255 }),
+ // 139,ZMM_HD/ZMM_DT,ZETC_CNRT_KRW,M,CURR,"17,2",기타계약KRW
+ ZETC_CNRT_KRW: varchar({ length: 255 }),
+ // 140,ZMM_HD/ZMM_DT,ZETC_CNRT_USD,M,CURR,"17,2",기타계약USD
+ ZETC_CNRT_USD: varchar({ length: 255 }),
+ // 141,ZMM_HD/ZMM_DT,ZEXTRA_AMT,M,CURR,"17,2","EXTRA금액, ZPO_CURR"
+ ZEXTRA_AMT: varchar({ length: 255 }),
+ // 142,ZMM_HD/ZMM_DT,ZCRDT_AMT,M,CURR,"17,2","CREDIT금액, ZPO_CURR"
+ ZCRDT_AMT: varchar({ length: 255 }),
+ // 143,ZMM_HD/ZMM_DT,ZART,M,CHAR,2,검사코드
+ ZART: varchar({ length: 255 }),
+ // 144,ZMM_HD/ZMM_DT,ART,M,CHAR,8,검사유형(QMAT)
+ ART: varchar({ length: 255 }),
+ // 145,ZMM_HD/ZMM_DT,ZPDT_BSE_UPR,M,CURR,"17,2","BASE금액, ZPO_CURR"
+ ZPDT_BSE_UPR: varchar({ length: 255 }),
+ // 146,ZMM_HD/ZMM_DT,ZPDT_EXTRA_UPR,M,CURR,"17,2","EXTRA금액, ZPO_CURR"
+ ZPDT_EXTRA_UPR: varchar({ length: 255 }),
+ // 147,ZMM_HD/ZMM_DT,ZPDT_EXDS_AMT,M,CURR,"17,2","할인/할증금액, ZPO_CURR"
+ ZPDT_EXDS_AMT: varchar({ length: 255 }),
+ // 148,ZMM_HD/ZMM_DT,ZTRNS_UPR,M,CURR,"17,2","운송단가, ZPO_CURR"
+ ZTRNS_UPR: varchar({ length: 255 }),
+ // 149,ZMM_HD/ZMM_DT,ZFST_DST_CD,M,CHAR,4,발주초기착지코드
+ ZFST_DST_CD: varchar({ length: 255 }),
+ // 150,ZMM_HD/ZMM_DT,ZCHG_CHK,M,CHAR,1,물량수정승인여부
+ ZCHG_CHK: varchar({ length: 255 }),
+ // 151,ZMM_HD/ZMM_DT,ZITP_CHK,M,CHAR,1,ITP체크
+ ZITP_CHK: varchar({ length: 255 }),
+ // 152,ZMM_HD/ZMM_DT,ZPO_RMK,M,CHAR,90,발주비고
+ ZPO_RMK: varchar({ length: 255 }),
+ // 153,ZMM_HD/ZMM_DT,ZPO_LOT_NO,M,CHAR,50,Steel Material Marking No
+ ZPO_LOT_NO: varchar({ length: 255 }),
+ // 154,ZMM_HD/ZMM_DT,ZCLM_NO,M,CHAR,10,SR번호
+ ZCLM_NO: varchar({ length: 255 }),
+ // 155,ZMM_HD/ZMM_DT,ZWH_CNTLR,M,CHAR,3,현물담당자코드
+ ZWH_CNTLR: varchar({ length: 255 }),
+ // 156,ZMM_HD/ZMM_DT,LFDAT,M,DATS,8,PR Delivery Date
+ LFDAT: varchar({ length: 255 }),
+ // 157,ZMM_HD/ZMM_DT,ETC_2,M,CHAR,100,확장2
+ ETC_2: varchar({ length: 255 }),
+ // 158,ZMM_HD/ZMM_DT,ETC_3,M,CHAR,100,확장3
+ ETC_3: varchar({ length: 255 }),
+ // 159,ZMM_HD/ZMM_DT,ETC_4,M,CHAR,100,확장4
+ ETC_4: varchar({ length: 255 }),
+ // 160,ZMM_HD/ZMM_DT,ETC_5,M,CHAR,100,확장5
+ ETC_5: varchar({ length: 255 }),
+ // 161,ZMM_HD/ZMM_DT,ETC_6,M,CHAR,100,확장6
+ ETC_6: varchar({ length: 255 }),
+ // 162,ZMM_HD/ZMM_DT,ETC_7,M,CHAR,100,확장7
+ ETC_7: varchar({ length: 255 }),
+ // 163,ZMM_HD/ZMM_DT,ETC_8,M,CHAR,100,확장8
+ ETC_8: varchar({ length: 255 }),
+ // 164,ZMM_HD/ZMM_DT,ETC_9,M,CHAR,100,확장9
+ ETC_9: varchar({ length: 255 }),
+ // 165,ZMM_HD/ZMM_DT,ETC_10,M,CHAR,100,확장10
+ ETC_10: varchar({ length: 255 }),
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+export const ZMM_KN = ECCSchema.table('ZMM_KN', {
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
+
+ // FK
+ // 1,ZMM_HD,EBELN,M,CHAR,10,구매오더번호
+ EBELN: varchar({ length: 255 }).notNull().references(() => ZMM_HD.EBELN),
+ // ZMM_DT의 서브테이블이므로 EBELP도 필요
+ EBELP: varchar({ length: 255 }).notNull(),
+
+ // 166,ZMM_HD/ZMM_DT/ZMM_KN,ZEKKN,M,NUMC,2,계정지정순번
+ ZEKKN: varchar({ length: 255 }),
+ // 167,ZMM_HD/ZMM_DT/ZMM_KN,SAKTO,M,CHAR,10,G/L계정번호
+ SAKTO: varchar({ length: 255 }),
+ // 168,ZMM_HD/ZMM_DT/ZMM_KN,GSBER,M,CHAR,4,사업영역코드
+ GSBER: varchar({ length: 255 }),
+ // 169,ZMM_HD/ZMM_DT/ZMM_KN,KOSTL,M,CHAR,10,코스트센터
+ KOSTL: varchar({ length: 255 }),
+ // 170,ZMM_HD/ZMM_DT/ZMM_KN,VBELN,M,CHAR,10,판매오더번호
+ VBELN: varchar({ length: 255 }),
+ // 171,ZMM_HD/ZMM_DT/ZMM_KN,VBELP,M,NUMC,6,판매오더품목번호
+ VBELP: varchar({ length: 255 }),
+ // 172,ZMM_HD/ZMM_DT/ZMM_KN,ANLN1,M,CHAR,12,주요자산번호
+ ANLN1: varchar({ length: 255 }),
+ // 173,ZMM_HD/ZMM_DT/ZMM_KN,ANLN2,M,CHAR,4,자산하위번호
+ ANLN2: varchar({ length: 255 }),
+ // 174,ZMM_HD/ZMM_DT/ZMM_KN,AUFNR,M,CHAR,12,생산오더번호
+ AUFNR: varchar({ length: 255 }),
+ // 175,ZMM_HD/ZMM_DT/ZMM_KN,WEMPF,M,CHAR,20,자재수령인명
+ WEMPF: varchar({ length: 255 }),
+ // 176,ZMM_HD/ZMM_DT/ZMM_KN,ABLAD,M,CHAR,75,하역지점
+ ABLAD: varchar({ length: 255 }),
+ // 177,ZMM_HD/ZMM_DT/ZMM_KN,KOKRS,M,CHAR,4,관리회계영역
+ KOKRS: varchar({ length: 255 }),
+ // 178,ZMM_HD/ZMM_DT/ZMM_KN,PRCTR,M,CHAR,10,손익센터
+ PRCTR: varchar({ length: 255 }),
+ // 179,ZMM_HD/ZMM_DT/ZMM_KN,NPLNR,M,CHAR,12,네트워크오더번호
+ NPLNR: varchar({ length: 255 }),
+ // 180,ZMM_HD/ZMM_DT/ZMM_KN,AUFPL,M,NUMC,10,오더라우팅번호
+ AUFPL: varchar({ length: 255 }),
+ // 181,ZMM_HD/ZMM_DT/ZMM_KN,APLZL,M,NUMC,8,오더내부카운터
+ APLZL: varchar({ length: 255 }),
+ // 182,ZMM_HD/ZMM_DT/ZMM_KN,FIPOS,M,CHAR,14,약정항목
+ FIPOS: varchar({ length: 255 }),
+ // 183,ZMM_HD/ZMM_DT/ZMM_KN,FISTL,M,CHAR,16,자금관리센터
+ FISTL: varchar({ length: 255 }),
+ // 184,ZMM_HD/ZMM_DT/ZMM_KN,GEBER,M,CHAR,10,자금코드
+ GEBER: varchar({ length: 255 }),
+ // 185,ZMM_HD/ZMM_DT/ZMM_KN,VORNR,M,CHAR,4,네트워크작업번호
+ VORNR: varchar({ length: 255 }),
+ // 186,ZMM_HD/ZMM_DT/ZMM_KN,POSID,M,CHAR,24,WBS요소
+ POSID: varchar({ length: 255 }),
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+export const ZMM_NOTE = ECCSchema.table('ZMM_NOTE', {
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
+
+ // FK
+ // 1,ZMM_HD,EBELN,M,CHAR,10,구매오더번호
+ EBELN: varchar({ length: 255 }).notNull().references(() => ZMM_HD.EBELN),
+
+ // 187,ZMM_HD/ZMM_NOTE,ZNOTE_SER,M,NUMC,4,발주 Note 순번
+ ZNOTE_SER: varchar({ length: 255 }).notNull(),
+ // 188,ZMM_HD/ZMM_NOTE,ZNOTE_TXT,M,CHAR,4000,발주 Note Text
+ ZNOTE_TXT: varchar({ length: 10000 }).notNull(),
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+
+
+export const ZMM_NOTE2 = ECCSchema.table('ZMM_NOTE2', {
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
+
+ // FK
+ // 1,ZMM_HD,EBELN,M,CHAR,10,구매오더번호
+ EBELN: varchar({ length: 255 }).notNull().references(() => ZMM_HD.EBELN),
+
+ // 189,ZMM_HD/ZMM_NOTE2,ZDLV_PRICE_SER,,NUMC,4,연동제 Note 순번
+ ZDLV_PRICE_SER: varchar({ length: 255 }).notNull(),
+ // 190,ZMM_HD/ZMM_NOTE2,ZDLV_PRICE_NOTE,,CHAR,4000,연동제 Note Text
+ ZDLV_PRICE_NOTE: varchar({ length: 10000 }).notNull(),
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+// *** === END === WSDL FILE: IF_ECC_EVCP_PO_INFORMATION.wsdl === ***
+
+
+
+
+// *** === START === WSDL FILE: IF_ECC_EVCP_REJECT_FOR_REVISED_PR.wsdl === ***
+export const T_CHANGE_PR = ECCSchema.table('T_CHANGE_PR', {
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
+
+ // SEQ,Table,Field,M/O,,Type,Size,,Description
+ // 1,메타데이터(저장하지 않음),IV_ERDAT,M,,DATS,8,,Reject Date
+ // 2,메타데이터(저장하지 않음),IV_ERZET,M,,TIMS,6,,Reject Time
+ // 3,T_CHANGE_PR,BANFN,M,,CHAR,10,,Purchase Requisition Number
+ BANFN: varchar({ length: 255 }).notNull(),
+ // 4,T_CHANGE_PR,BANPO,M,,NUMC,5,,Item Number of Purchase Requisition
+ BANPO: varchar({ length: 255 }).notNull(),
+ // 5,T_CHANGE_PR,ZCHG_NO,M,,CHAR,10,,Change Number
+ ZCHG_NO: varchar({ length: 255 }).notNull(),
+ // 6,T_CHANGE_PR,ZACC_IND,,,CHAR,1,,P/R Accept Indicator
+ ZACC_IND: varchar({ length: 255 }),
+ // 7,T_CHANGE_PR,PCR_REQ,,,CHAR,10,,PCR Request No.
+ PCR_REQ: varchar({ length: 255 }),
+ // 8,T_CHANGE_PR,PCR_REQ_SEQ,,,NUMC,5,,PCR Request Sequence No.
+ PCR_REQ_SEQ: varchar({ length: 255 }),
+ // 응답은 전체 요청에 대해 하나의 메시지만 전송하며, 데이터베이스에 저장하지 않음
+ // 9,수신측 응답,EV_TYPE,,,CHAR,1,,Message Type
+ // 10,수신측 응답,EV_MESSAGE,,,CHAR,100,,Message Text
+
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+});
+// *** === END === WSDL FILE: IF_ECC_EVCP_REJECT_FOR_REVISED_PR.wsdl === *** \ No newline at end of file
diff --git a/lib/shi-api/shi-api-utils.ts b/lib/shi-api/shi-api-utils.ts
index b8eeff29..7ea582f6 100644
--- a/lib/shi-api/shi-api-utils.ts
+++ b/lib/shi-api/shi-api-utils.ts
@@ -41,55 +41,97 @@ export const getAllNonsapUser = async () => {
// ** 2. 받은 데이터를 DELETE & INSERT 방식으로 수신 테이블 (nonsap-user) 에 저장 **
await db.delete(nonsapUser); // 전체 정리
if (Array.isArray(data) && data.length > 0) {
- await db.insert(nonsapUser).values(data as unknown as NonsapUserInsert[]); // 데이터 저장 (스키마 컬럼 그대로)
- debugSuccess(`[STAGE] nonsap_user refreshed with ${data.length} records`);
+ // 대용량 데이터를 청크 단위로 분할하여 저장 (스택 오버플로 방지)
+ const STAGING_CHUNK_SIZE = 1000; // 스테이징 테이블용 청크 크기
+ let totalStaged = 0;
+
+ for (let i = 0; i < data.length; i += STAGING_CHUNK_SIZE) {
+ const chunk = data.slice(i, i + STAGING_CHUNK_SIZE);
+ debugLog(`[STAGING CHUNK ${Math.floor(i/STAGING_CHUNK_SIZE) + 1}] Inserting ${chunk.length} records (${i + 1}-${Math.min(i + chunk.length, data.length)}/${data.length})`);
+
+ try {
+ await db.insert(nonsapUser).values(chunk as unknown as NonsapUserInsert[]);
+ totalStaged += chunk.length;
+
+ // 청크 간 잠시 대기하여 시스템 부하 방지
+ if (i + STAGING_CHUNK_SIZE < data.length) {
+ await new Promise(resolve => setTimeout(resolve, 50));
+ }
+ } catch (chunkError) {
+ debugError(`[STAGING CHUNK ${Math.floor(i/STAGING_CHUNK_SIZE) + 1}] Failed to insert chunk ${i}-${i+chunk.length}`, chunkError);
+ throw chunkError;
+ }
+ }
+
+ debugSuccess(`[STAGE] nonsap_user refreshed with ${totalStaged} records (processed in ${Math.ceil(data.length/STAGING_CHUNK_SIZE)} chunks)`);
}
// ** 3. 데이터 저장 이후, 비즈니스 테이블인 "public"."users" 에 동기화 시킴 (매핑 필요) **
const now = new Date();
- const mappedRaw: Partial<InsertUser>[] = (Array.isArray(data) ? data : [])
- .map((u: NonsapUser): Partial<InsertUser> => {
- const isDeleted = ynToBool(u.DEL_YN); // nonsap user 테이블에서 삭제여부
- const isAbsent = ynToBool(u.LOFF_GB); // nonsap user 테이블에서 휴직여부
- const notApproved = (u.AGR_YN || '').toUpperCase() === 'N'; // nonsap user 테이블에서 승인여부
- const isActive = !(isDeleted || isAbsent || notApproved); // eVCP 내에서 활성화 여부
- // S = 정직원
- const isRegularEmployee = (u.REGL_ORORD_GB || '').toUpperCase() === 'S';
-
- return {
- // mapped fields
- nonsapUserId: u.USR_ID || undefined,
- employeeNumber: u.EMPNO || undefined,
- knoxId: u.MYSNG_ID || undefined,
- name: u.USR_NM || undefined,
- email: u.EMAIL_ADR || undefined,
- epId: u.MYSNG_ID || undefined,
- deptCode: u.DEPTCD || undefined,
- deptName: u.DEPTNM || undefined,
- phone: u.HP_NO || undefined,
- isAbsent,
- isDeletedOnNonSap: isDeleted,
- isActive,
- isRegularEmployee,
- };
- });
- // users 테이블 제약조건 대응: email, name 은 not null + nonsapUserId 존재
- //.filter((u) => typeof u.email === 'string' && !!u.email && typeof u.name === 'string' && !!u.name && typeof u.nonsapUserId === 'string' && u.nonsapUserId.length > 0);
-
- const mappedUsers = mappedRaw as InsertUser[];
-
- if (mappedUsers.length > 0) {
- // 배치 처리: 500개씩 분할하여 처리 (콜스택 크기 문제 대응)
- const BATCH_SIZE = 500;
- let totalUpserted = 0;
+ // 매핑과 DB 처리를 청크 단위로 통합 처리하여 메모리 효율성 향상
+ const sourceData = Array.isArray(data) ? data : [];
+ if (sourceData.length === 0) {
+ debugWarn('[UPSERT] No source data to process');
+ return {
+ fetched: 0,
+ staged: 0,
+ upserted: 0,
+ skippedDueToMissingRequiredFields: 0,
+ ranAt: now.toISOString(),
+ };
+ }
+
+ const CHUNK_SIZE = 500; // 매핑과 DB 처리 청크 크기
+ let totalUpserted = 0;
+ let totalSkipped = 0;
+
+ // 청크 단위로 매핑과 DB 처리를 동시에 수행
+ for (let i = 0; i < sourceData.length; i += CHUNK_SIZE) {
+ const chunk = sourceData.slice(i, i + CHUNK_SIZE);
+ debugLog(`[CHUNK ${Math.floor(i/CHUNK_SIZE) + 1}] Processing ${chunk.length} users (${i + 1}-${Math.min(i + chunk.length, sourceData.length)}/${sourceData.length})`);
- for (let i = 0; i < mappedUsers.length; i += BATCH_SIZE) {
- const batch = mappedUsers.slice(i, i + BATCH_SIZE);
+ try {
+ // 청크 단위로 매핑 수행
+ const mappedChunk: InsertUser[] = [];
- try {
+ for (const u of chunk) {
+ const isDeleted = ynToBool(u.DEL_YN); // nonsap user 테이블에서 삭제여부
+ const isAbsent = ynToBool(u.LOFF_GB); // nonsap user 테이블에서 휴직여부
+ const notApproved = (u.AGR_YN || '').toUpperCase() === 'N'; // nonsap user 테이블에서 승인여부
+ const isActive = !(isDeleted || isAbsent || notApproved); // eVCP 내에서 활성화 여부
+ // S = 정직원
+ const isRegularEmployee = (u.REGL_ORORD_GB || '').toUpperCase() === 'S';
+
+ const mappedUser: Partial<InsertUser> = {
+ // mapped fields
+ nonsapUserId: u.USR_ID || undefined,
+ employeeNumber: u.EMPNO || undefined,
+ knoxId: u.MYSNG_ID || undefined,
+ name: u.USR_NM || undefined,
+ email: u.EMAIL_ADR || undefined,
+ epId: u.MYSNG_ID || undefined,
+ deptCode: u.DEPTCD || undefined,
+ deptName: u.DEPTNM || undefined,
+ phone: u.HP_NO || undefined,
+ isAbsent,
+ isDeletedOnNonSap: isDeleted,
+ isActive,
+ isRegularEmployee,
+ };
+
+ // 필수 필드 검증 (기본적인 검증만 수행)
+ if (mappedUser.nonsapUserId) {
+ mappedChunk.push(mappedUser as InsertUser);
+ } else {
+ totalSkipped++;
+ }
+ }
+
+ // 매핑된 청크가 있을 경우에만 DB 처리
+ if (mappedChunk.length > 0) {
await db.insert(users)
- .values(batch)
+ .values(mappedChunk)
.onConflictDoUpdate({
target: users.nonsapUserId,
set: {
@@ -109,31 +151,32 @@ export const getAllNonsapUser = async () => {
},
});
- totalUpserted += batch.length;
- debugLog(`[BATCH ${Math.floor(i/BATCH_SIZE) + 1}] Processed ${batch.length} users (${totalUpserted}/${mappedUsers.length})`);
-
- // 배치 간 잠시 대기하여 시스템 부하 방지
- if (i + BATCH_SIZE < mappedUsers.length) {
- await new Promise(resolve => setTimeout(resolve, 100));
- }
- } catch (batchError) {
- debugError(`[BATCH ${Math.floor(i/BATCH_SIZE) + 1}] Failed to process batch ${i}-${i+batch.length}`, batchError);
- throw batchError;
+ totalUpserted += mappedChunk.length;
+ debugLog(`[CHUNK ${Math.floor(i/CHUNK_SIZE) + 1}] Successfully processed ${mappedChunk.length} users`);
}
+
+ // 청크 간 잠시 대기하여 시스템 부하 방지
+ if (i + CHUNK_SIZE < sourceData.length) {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ } catch (chunkError) {
+ debugError(`[CHUNK ${Math.floor(i/CHUNK_SIZE) + 1}] Failed to process chunk ${i}-${i+chunk.length}`, chunkError);
+ throw chunkError;
}
-
- debugSuccess(`[UPSERT] users upserted=${totalUpserted} using key=nonsapUserId (processed in ${Math.ceil(mappedUsers.length/BATCH_SIZE)} batches)`);
- } else {
- debugWarn('[UPSERT] No users mapped for upsert (missing name/email or invalid USR_ID)');
}
+
+ debugSuccess(`[UPSERT] users upserted=${totalUpserted}, skipped=${totalSkipped} (processed in ${Math.ceil(sourceData.length/CHUNK_SIZE)} chunks)`);
+
+ // 처리 완료
// 휴직 사용자도 API에서 수신하므로, 기존 사용자와의 비교를 통한 휴직 처리 로직은 더 이상 필요하지 않음
return {
fetched: Array.isArray(data) ? data.length : 0,
staged: Array.isArray(data) ? data.length : 0,
- upserted: mappedUsers.length,
- skippedDueToMissingRequiredFields: (Array.isArray(data) ? data.length : 0) - mappedUsers.length,
+ upserted: totalUpserted,
+ skippedDueToMissingRequiredFields: totalSkipped,
ranAt: now.toISOString(),
};
} catch(error){