diff options
| -rw-r--r-- | app/[lng]/partners/(partners)/sales-force-test/AF_poc.html | 118 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/sales-force-test/page.tsx | 32 | ||||
| -rw-r--r-- | app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts | 140 | ||||
| -rw-r--r-- | app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts | 287 | ||||
| -rw-r--r-- | app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_REJECT_FOR_REVISED_PR/route.ts | 143 | ||||
| -rw-r--r-- | db/schema/ECC/ecc.ts | 622 | ||||
| -rw-r--r-- | lib/shi-api/shi-api-utils.ts | 157 |
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){ |
