From 1092e6cbcafd69e236f9a57e2f18987be5764bd5 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 1 Jul 2025 10:12:15 +0000 Subject: (김준회) MDG SOAP 인터페이스 수신 라우트 개선 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts | 100 +++++++----- .../(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts | 94 ++++++----- .../(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts | 101 +++++++----- .../IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts | 64 +++++--- .../(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts | 173 ++++++++++----------- .../IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts | 121 +++++++------- .../(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts | 121 +++++++------- .../(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts | 127 ++++++++------- .../(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts | 99 +++++++----- app/api/(S-ERP)/(MDG)/utils.ts | 88 +++++++++-- 10 files changed, 605 insertions(+), 483 deletions(-) (limited to 'app/api/(S-ERP)') diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts index 2c9df21d..fd4afb86 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts @@ -176,6 +176,25 @@ export async function POST(request: NextRequest) { } // XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * CUSTOMER 마스터 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * 1. 최상위 테이블 (CUSTOMER_MASTER_BP_HEADER) + * - BP_HEADER가 unique 필드로 충돌 시 upsert 처리 + * + * 2. 하위 테이블들 (ADDRESS, BP_CUSGEN, BP_TAXNUM 등) + * - FK(BP_HEADER)로 연결 + * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) + * - 전체 데이터셋 기반 삭제 후 재삽입 처리 + * + * 3. 중첩 하위 테이블들 (AD_EMAIL, ZVATREG, ZSALES 등) + * - 동일하게 FK(BP_HEADER)로 연결 + * - 전체 데이터셋 기반 처리 + * + * @param bpHeaderData XML에서 파싱된 CUSTOMER 헤더 데이터 + * @returns 처리된 CUSTOMER 데이터 구조 + */ function transformCustomerData(bpHeaderData: BpHeaderXML[]): ProcessedCustomerData[] { if (!bpHeaderData || !Array.isArray(bpHeaderData)) { return []; @@ -185,86 +204,83 @@ function transformCustomerData(bpHeaderData: BpHeaderXML[]): ProcessedCustomerDa const bpHeaderKey = bpHeader.BP_HEADER || ''; const fkData = { BP_HEADER: bpHeaderKey }; - // 1단계: BP_HEADER (루트) + // 1단계: BP_HEADER (루트 - unique 필드: BP_HEADER) const bpHeaderConverted = convertXMLToDBData( bpHeader as Record, - ['BP_HEADER'], fkData ); - // 2단계: ADDRESS와 직속 하위들 + // 2단계: 직속 하위 테이블들 (FK: BP_HEADER) const addresses = processNestedArray( bpHeader.ADDRESS, - (addr) => convertXMLToDBData(addr as Record, ['ADDRNO'], fkData), + (addr) => convertXMLToDBData(addr as Record, fkData), fkData ); - // ADDRESS의 하위 테이블들 (3단계) + const bpTaxnums = processNestedArray( + bpHeader.BP_TAXNUM, + (item) => convertXMLToDBData(item as Record, fkData), + fkData + ); + + const bpCusgens = processNestedArray( + bpHeader.BP_CUSGEN, + (cusgen) => convertXMLToDBData(cusgen as Record, fkData), + fkData + ); + + // 3단계: ADDRESS의 하위 테이블들 (FK: BP_HEADER) const adEmails = bpHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_EMAIL, (item) => - convertXMLToDBData(item as Record, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const adFaxes = bpHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_FAX, (item) => - convertXMLToDBData(item as Record, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const adPostals = bpHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_POSTAL, (item) => - convertXMLToDBData(item as Record, ['NATION'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const adTels = bpHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_TEL, (item) => - convertXMLToDBData(item as Record, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const adUrls = bpHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_URL, (item) => - convertXMLToDBData(item as Record, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; - // 2단계: BP_TAXNUM - const bpTaxnums = processNestedArray( - bpHeader.BP_TAXNUM, - (item) => convertXMLToDBData(item as Record, ['TAXTYPE'], fkData), - fkData - ); - - // 2단계: BP_CUSGEN과 하위들 - const bpCusgens = processNestedArray( - bpHeader.BP_CUSGEN, - (cusgen) => convertXMLToDBData(cusgen as Record, ['KUNNR'], fkData), - fkData - ); - - // BP_CUSGEN의 하위 테이블들 (3단계) + // 3단계: BP_CUSGEN의 하위 테이블들 (FK: BP_HEADER) const zvatregs = bpHeader.BP_CUSGEN?.flatMap(cusgen => processNestedArray(cusgen.ZVATREG, (item) => - convertXMLToDBData(item as Record, ['LAND1'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const ztaxinds = bpHeader.BP_CUSGEN?.flatMap(cusgen => processNestedArray(cusgen.ZTAXIND, (item) => - convertXMLToDBData(item as Record, ['ALAND', 'TATYP'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const zcompanies = bpHeader.BP_CUSGEN?.flatMap(cusgen => processNestedArray(cusgen.ZCOMPANY, (item) => - convertXMLToDBData(item as Record, ['BUKRS'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const zsales = bpHeader.BP_CUSGEN?.flatMap(cusgen => processNestedArray(cusgen.ZSALES, (item) => - convertXMLToDBData(item as Record, ['VKORG', 'VTWEG', 'SPART'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; - // ZSALES의 하위 테이블 (4단계) + // 4단계: 더 깊은 중첩 테이블들 (FK: BP_HEADER) const zcpfns = bpHeader.BP_CUSGEN?.flatMap(cusgen => cusgen.ZSALES?.flatMap(sales => processNestedArray(sales.ZCPFN, (item) => - convertXMLToDBData(item as Record, ['PARVW', 'PARZA'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || [] ) || []; @@ -288,6 +304,17 @@ function transformCustomerData(bpHeaderData: BpHeaderXML[]): ProcessedCustomerDa } // 데이터베이스 저장 함수 +/** + * 처리된 CUSTOMER 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 최상위 테이블: BP_HEADER 기준 upsert (충돌 시 업데이트) + * 2. 하위 테이블들: FK(BP_HEADER) 기준 전체 삭제 후 재삽입 + * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 + * - 데이터 일관성과 단순성 확보 + * + * @param processedCustomers 변환된 CUSTOMER 데이터 배열 + */ async function saveToDatabase(processedCustomers: ProcessedCustomerData[]) { console.log(`데이터베이스 저장 시작: ${processedCustomers.length}개 고객 데이터`); @@ -302,7 +329,7 @@ async function saveToDatabase(processedCustomers: ProcessedCustomerData[]) { continue; } - // 1. BP_HEADER 테이블 Upsert (최상위 테이블) + // 1. BP_HEADER 테이블 Upsert (최상위 테이블 - unique 필드: BP_HEADER) await tx.insert(CUSTOMER_MASTER_BP_HEADER) .values(bpHeader) .onConflictDoUpdate({ @@ -313,14 +340,15 @@ async function saveToDatabase(processedCustomers: ProcessedCustomerData[]) { } }); - // 2. 하위 테이블들 처리 - FK 기준으로 전체 삭제 후 재삽입 + // 2. 하위 테이블들 처리 - FK(BP_HEADER) 기준 전체 삭제 후 재삽입 + // 전체 데이터셋 기반 처리로 데이터 일관성 확보 await Promise.all([ // 2단계 테이블들 replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS, addresses, 'BP_HEADER', bpHeader.BP_HEADER), replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN, bpCusgens, 'BP_HEADER', bpHeader.BP_HEADER), replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_TAXNUM, bpTaxnums, 'BP_HEADER', bpHeader.BP_HEADER), - // 3-4단계 테이블들 + // 3-4단계 테이블들 - 동일하게 FK(BP_HEADER) 기준 처리 replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, adEmails, 'BP_HEADER', bpHeader.BP_HEADER), replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_FAX, adFaxes, 'BP_HEADER', bpHeader.BP_HEADER), replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, adPostals, 'BP_HEADER', bpHeader.BP_HEADER), @@ -335,10 +363,10 @@ async function saveToDatabase(processedCustomers: ProcessedCustomerData[]) { } }); - console.log(`✅ 데이터베이스 저장 완료: ${processedCustomers.length}개 고객`); + console.log(`데이터베이스 저장 완료: ${processedCustomers.length}개 고객`); return true; } catch (error) { - console.error('❌ 데이터베이스 저장 중 오류 발생:', error); + console.error('데이터베이스 저장 중 오류 발생:', error); throw error; } } diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts index 5d407e1f..ffb39895 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts @@ -116,47 +116,52 @@ export async function POST(request: NextRequest) { } // XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * DEPARTMENT 코드 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * 1. 최상위 테이블 (DEPARTMENT_CODE_CMCTB_DEPT_MDG) + * - DEPTCD가 unique 필드로 충돌 시 upsert 처리 + * + * 2. 하위 테이블들 (DEPTNM, COMPNM, CORPNM) + * - FK(DEPTCD)로 연결 + * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) + * - 전체 데이터셋 기반 삭제 후 재삽입 처리 + * + * @param deptData XML에서 파싱된 DEPARTMENT 데이터 + * @returns 처리된 DEPARTMENT 데이터 구조 + */ function transformDepartmentData(deptData: DeptXML[]): ProcessedDepartmentData[] { if (!deptData || !Array.isArray(deptData)) { return []; } return deptData.map(dept => { - // 메인 Department 데이터 변환 + const deptcdKey = dept.DEPTCD || ''; + const fkData = { DEPTCD: deptcdKey }; + + // 1단계: DEPT (루트 - unique 필드: DEPTCD) const deptRecord = convertXMLToDBData( - dept as Record, - ['DEPTCD', 'CORPCD'] + dept as Record, + fkData ); - // 필수 필드 보정 - if (!deptRecord.DEPTCD) { - deptRecord.DEPTCD = ''; - } - if (!deptRecord.CORPCD) { - deptRecord.CORPCD = ''; - } - - // FK 데이터 준비 - const fkData = { DEPTCD: dept.DEPTCD || '' }; - - // DEPTNM 데이터 변환 + // 2단계: 하위 테이블들 (FK: DEPTCD) const deptnms = processNestedArray( dept.DEPTNM, - (deptnm) => convertXMLToDBData(deptnm, ['SPRAS'], fkData), + (deptnm) => convertXMLToDBData(deptnm as Record, fkData), fkData ); - // COMPNM 데이터 변환 const compnms = processNestedArray( dept.COMPNM, - (compnm) => convertXMLToDBData(compnm, ['SPRAS'], fkData), + (compnm) => convertXMLToDBData(compnm as Record, fkData), fkData ); - // CORPNM 데이터 변환 const corpnms = processNestedArray( dept.CORPNM, - (corpnm) => convertXMLToDBData(corpnm, ['SPRAS'], fkData), + (corpnm) => convertXMLToDBData(corpnm as Record, fkData), fkData ); @@ -170,8 +175,19 @@ function transformDepartmentData(deptData: DeptXML[]): ProcessedDepartmentData[] } // 데이터베이스 저장 함수 +/** + * 처리된 DEPARTMENT 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 최상위 테이블: DEPTCD 기준 upsert (충돌 시 업데이트) + * 2. 하위 테이블들: FK(DEPTCD) 기준 전체 삭제 후 재삽입 + * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 + * - 데이터 일관성과 단순성 확보 + * + * @param processedDepts 변환된 DEPARTMENT 데이터 배열 + */ async function saveToDatabase(processedDepts: ProcessedDepartmentData[]) { - console.log(`데이터베이스 저장 함수가 호출됨. ${processedDepts.length}개의 부서 데이터 수신.`); + console.log(`데이터베이스 저장 시작: ${processedDepts.length}개 부서 데이터`); try { await db.transaction(async (tx) => { @@ -183,7 +199,7 @@ async function saveToDatabase(processedDepts: ProcessedDepartmentData[]) { continue; } - // 1. Department 테이블 Upsert (최상위 테이블) + // 1. Department 테이블 Upsert (최상위 테이블 - unique 필드: DEPTCD) await tx.insert(DEPARTMENT_CODE_CMCTB_DEPT_MDG) .values(dept) .onConflictDoUpdate({ @@ -194,39 +210,17 @@ async function saveToDatabase(processedDepts: ProcessedDepartmentData[]) { } }); - // 2. 하위 테이블 데이터 처리 - FK 기준으로 전체 삭제 후 재삽입 + // 2. 하위 테이블들 처리 - FK(DEPTCD) 기준 전체 삭제 후 재삽입 + // 전체 데이터셋 기반 처리로 데이터 일관성 확보 await Promise.all([ - // DEPTNM 테이블 처리 - replaceSubTableData( - tx, - DEPARTMENT_CODE_CMCTB_DEPT_MDG_DEPTNM, - deptnms, - 'DEPTCD', - dept.DEPTCD - ), - - // COMPNM 테이블 처리 - replaceSubTableData( - tx, - DEPARTMENT_CODE_CMCTB_DEPT_MDG_COMPNM, - compnms, - 'DEPTCD', - dept.DEPTCD - ), - - // CORPNM 테이블 처리 - replaceSubTableData( - tx, - DEPARTMENT_CODE_CMCTB_DEPT_MDG_CORPNM, - corpnms, - 'DEPTCD', - dept.DEPTCD - ) + replaceSubTableData(tx, DEPARTMENT_CODE_CMCTB_DEPT_MDG_DEPTNM, deptnms, 'DEPTCD', dept.DEPTCD), + replaceSubTableData(tx, DEPARTMENT_CODE_CMCTB_DEPT_MDG_COMPNM, compnms, 'DEPTCD', dept.DEPTCD), + replaceSubTableData(tx, DEPARTMENT_CODE_CMCTB_DEPT_MDG_CORPNM, corpnms, 'DEPTCD', dept.DEPTCD) ]); } }); - console.log(`${processedDepts.length}개의 부서 데이터 처리 완료.`); + console.log(`데이터베이스 저장 완료: ${processedDepts.length}개 부서`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts index 39e9aa2f..b9775765 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts @@ -204,48 +204,66 @@ export async function POST(request: NextRequest) { } // XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * EMPLOYEE 마스터 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * 1. 최상위 테이블 (EMPLOYEE_MASTER_CMCTB_EMP_MDG) + * - EMPID가 unique 필드로 충돌 시 upsert 처리 + * + * 2. 하위 테이블들 (BANM, BINM, COMPNM, CORPNM, COUNTRYNM, DEPTCODE, + * DEPTCODE_PCCDNM, DEPTNM, DHJOBGDNM, GJOBDUTYNM, GJOBGRDNM, + * GJOBGRDTYPE, GJOBNM, GNNM, JOBDUTYNM, JOBGRDNM, JOBNM, + * KTLNM, OKTLNM, ORGBICDNM, ORGCOMPNM, ORGCORPNM, ORGDEPTNM, + * ORGPDEPNM, PDEPTNM) + * - FK(EMPID)로 연결 + * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) + * - 전체 데이터셋 기반 삭제 후 재삽입 처리 + * + * @param empData XML에서 파싱된 EMPLOYEE 데이터 + * @returns 처리된 EMPLOYEE 데이터 구조 + */ function transformEmployeeData(empData: EmpMdgXML[]): ProcessedEmployeeData[] { if (!empData || !Array.isArray(empData)) { return []; } return empData.map(emp => { - const empId = emp.EMPID || ''; - const fkData = { EMPID: empId }; + const empidKey = emp.EMPID || ''; + const fkData = { EMPID: empidKey }; - // 메인 Employee 데이터 변환 + // 1단계: EMP_MDG (루트 - unique 필드: EMPID) const employee = convertXMLToDBData( - emp as Record, - ['EMPID'], + emp as Record, fkData ); - // 하위 테이블 데이터 변환 - const banm = processNestedArray(emp.BANM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const binm = processNestedArray(emp.BINM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const compnm = processNestedArray(emp.COMPNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const corpnm = processNestedArray(emp.CORPNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const countrynm = processNestedArray(emp.COUNTRYNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const deptcode = processNestedArray(emp.DEPTCODE, (item) => convertXMLToDBData(item, [], fkData), fkData); - const deptcodePccdnm = processNestedArray(emp.DEPTCODE_PCCDNM, (item) => convertXMLToDBData(item, [], fkData), fkData); - const deptnm = processNestedArray(emp.DEPTNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const dhjobgdnm = processNestedArray(emp.DHJOBGDNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const gjobdutynm = processNestedArray(emp.GJOBDUTYNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const gjobgrdnm = processNestedArray(emp.GJOBGRDNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const gjobgrdtype = processNestedArray(emp.GJOBGRDTYPE, (item) => convertXMLToDBData(item, [], fkData), fkData); - const gjobnm = processNestedArray(emp.GJOBNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const gnnm = processNestedArray(emp.GNNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const jobdutynm = processNestedArray(emp.JOBDUTYNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const jobgrdnm = processNestedArray(emp.JOBGRDNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const jobnm = processNestedArray(emp.JOBNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const ktlnm = processNestedArray(emp.KTLNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const oktlnm = processNestedArray(emp.OKTLNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const orgbicdnm = processNestedArray(emp.ORGBICDNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const orgcompnm = processNestedArray(emp.ORGCOMPNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const orgcorpnm = processNestedArray(emp.ORGCORPNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const orgdeptnm = processNestedArray(emp.ORGDEPTNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const orgpdepnm = processNestedArray(emp.ORGPDEPNM, (item) => convertXMLToDBData(item, ['SPRAS'], fkData), fkData); - const pdeptnm = processNestedArray(emp.PDEPTNM, (item) => convertXMLToDBData(item, [], fkData), fkData); + // 2단계: 하위 테이블들 (FK: EMPID) - 25개 테이블 + const banm = processNestedArray(emp.BANM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const binm = processNestedArray(emp.BINM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const compnm = processNestedArray(emp.COMPNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const corpnm = processNestedArray(emp.CORPNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const countrynm = processNestedArray(emp.COUNTRYNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const deptcode = processNestedArray(emp.DEPTCODE, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const deptcodePccdnm = processNestedArray(emp.DEPTCODE_PCCDNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const deptnm = processNestedArray(emp.DEPTNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const dhjobgdnm = processNestedArray(emp.DHJOBGDNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const gjobdutynm = processNestedArray(emp.GJOBDUTYNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const gjobgrdnm = processNestedArray(emp.GJOBGRDNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const gjobgrdtype = processNestedArray(emp.GJOBGRDTYPE, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const gjobnm = processNestedArray(emp.GJOBNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const gnnm = processNestedArray(emp.GNNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const jobdutynm = processNestedArray(emp.JOBDUTYNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const jobgrdnm = processNestedArray(emp.JOBGRDNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const jobnm = processNestedArray(emp.JOBNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const ktlnm = processNestedArray(emp.KTLNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const oktlnm = processNestedArray(emp.OKTLNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const orgbicdnm = processNestedArray(emp.ORGBICDNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const orgcompnm = processNestedArray(emp.ORGCOMPNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const orgcorpnm = processNestedArray(emp.ORGCORPNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const orgdeptnm = processNestedArray(emp.ORGDEPTNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const orgpdepnm = processNestedArray(emp.ORGPDEPNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); + const pdeptnm = processNestedArray(emp.PDEPTNM, (item) => convertXMLToDBData(item as Record, fkData), fkData); return { employee, @@ -279,6 +297,18 @@ function transformEmployeeData(empData: EmpMdgXML[]): ProcessedEmployeeData[] { } // 데이터베이스 저장 함수 +/** + * 처리된 EMPLOYEE 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 최상위 테이블: EMPID 기준 upsert (충돌 시 업데이트) + * 2. 하위 테이블들: FK(EMPID) 기준 전체 삭제 후 재삽입 + * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 + * - 데이터 일관성과 단순성 확보 + * - 25개의 하위 테이블을 병렬 처리로 성능 최적화 + * + * @param processedEmployees 변환된 EMPLOYEE 데이터 배열 + */ async function saveToDatabase(processedEmployees: ProcessedEmployeeData[]) { console.log(`데이터베이스 저장 시작: ${processedEmployees.length}개 사원 데이터`); @@ -295,7 +325,7 @@ async function saveToDatabase(processedEmployees: ProcessedEmployeeData[]) { continue; } - // 1. CMCTB_EMP_MDG 테이블 Upsert (최상위 테이블) + // 1. CMCTB_EMP_MDG 테이블 Upsert (최상위 테이블 - unique 필드: EMPID) await tx.insert(EMPLOYEE_MASTER_CMCTB_EMP_MDG) .values(employee) .onConflictDoUpdate({ @@ -306,7 +336,8 @@ async function saveToDatabase(processedEmployees: ProcessedEmployeeData[]) { } }); - // 2. 하위 테이블들 처리 - FK 기준으로 전체 삭제 후 재삽입 + // 2. 하위 테이블들 처리 - FK(EMPID) 기준 전체 삭제 후 재삽입 + // 25개 테이블을 병렬 처리로 성능 최적화 await Promise.all([ replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_BANM, banm, 'EMPID', employee.EMPID), replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_BINM, binm, 'EMPID', employee.EMPID), @@ -337,10 +368,10 @@ async function saveToDatabase(processedEmployees: ProcessedEmployeeData[]) { } }); - console.log(`✅ 데이터베이스 저장 완료: ${processedEmployees.length}개 사원`); + console.log(`데이터베이스 저장 완료: ${processedEmployees.length}개 사원`); return true; } catch (error) { - console.error('❌ 데이터베이스 저장 중 오류 발생:', error); + console.error('데이터베이스 저장 중 오류 발생:', error); throw error; } } \ No newline at end of file diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts index a265fea2..3c58e3a1 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts @@ -103,30 +103,40 @@ export async function POST(request: NextRequest) { } // XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * EMPLOYEE_REFERENCE 마스터 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * 1. 최상위 테이블 (EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF) + * - GRPCD가 unique 필드로 충돌 시 upsert 처리 + * + * 2. 하위 테이블들 (NAME) + * - FK(GRPCD)로 연결 + * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) + * - 전체 데이터셋 기반 삭제 후 재삽입 처리 + * + * @param empRefData XML에서 파싱된 EMPLOYEE_REFERENCE 데이터 + * @returns 처리된 EMPLOYEE_REFERENCE 데이터 구조 + */ function transformEmpRefData(empRefData: EmpRefXML[]): ProcessedEmployeeReferenceData[] { if (!empRefData || !Array.isArray(empRefData)) { return []; } return empRefData.map(empRef => { - // 메인 Employee Reference 데이터 변환 + const grpcdKey = empRef.GRPCD || ''; + const fkData = { GRPCD: grpcdKey }; + + // 1단계: EMP_REF (루트 - unique 필드: GRPCD) const empRefRecord = convertXMLToDBData( - empRef as Record, - ['GRPCD', 'CORPCD', 'MAINCD'] + empRef as Record, + fkData ); - // 필수 필드 보정 - if (!empRefRecord.GRPCD) { - empRefRecord.GRPCD = ''; - } - - // FK 데이터 준비 - const fkData = { GRPCD: empRef.GRPCD || '' }; - - // Name 데이터 변환 + // 2단계: 하위 테이블들 (FK: GRPCD) const names = processNestedArray( empRef.NAME, - (name) => convertXMLToDBData(name, ['SPRAS'], fkData), + (name) => convertXMLToDBData(name as Record, fkData), fkData ); @@ -138,8 +148,19 @@ function transformEmpRefData(empRefData: EmpRefXML[]): ProcessedEmployeeReferenc } // 데이터베이스 저장 함수 +/** + * 처리된 EMPLOYEE_REFERENCE 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 최상위 테이블: GRPCD 기준 upsert (충돌 시 업데이트) + * 2. 하위 테이블들: FK(GRPCD) 기준 전체 삭제 후 재삽입 + * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 + * - 데이터 일관성과 단순성 확보 + * + * @param processedEmpRefs 변환된 EMPLOYEE_REFERENCE 데이터 배열 + */ async function saveToDatabase(processedEmpRefs: ProcessedEmployeeReferenceData[]) { - console.log(`데이터베이스 저장 함수가 호출됨. ${processedEmpRefs.length}개의 직원 참조 데이터 수신.`); + console.log(`데이터베이스 저장 시작: ${processedEmpRefs.length}개 직원 참조 데이터`); try { await db.transaction(async (tx) => { @@ -151,7 +172,7 @@ async function saveToDatabase(processedEmpRefs: ProcessedEmployeeReferenceData[] continue; } - // 1. Employee Reference 테이블 Upsert (최상위 테이블) + // 1. Employee Reference 테이블 Upsert (최상위 테이블 - unique 필드: GRPCD) await tx.insert(EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF) .values(empRef) .onConflictDoUpdate({ @@ -162,18 +183,13 @@ async function saveToDatabase(processedEmpRefs: ProcessedEmployeeReferenceData[] } }); - // 2. NAME 테이블 처리 - FK 기준으로 전체 삭제 후 재삽입 - await replaceSubTableData( - tx, - EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF_NAME, - names, - 'GRPCD', - empRef.GRPCD - ); + // 2. 하위 테이블 처리 - FK(GRPCD) 기준 전체 삭제 후 재삽입 + // 전체 데이터셋 기반 처리로 데이터 일관성 확보 + await replaceSubTableData(tx, EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF_NAME, names, 'GRPCD', empRef.GRPCD); } }); - console.log(`${processedEmpRefs.length}개의 직원 참조 데이터 처리 완료.`); + console.log(`데이터베이스 저장 완료: ${processedEmpRefs.length}개 직원 참조`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts index 358e9c62..97c2e636 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts @@ -123,58 +123,68 @@ export async function POST(request: NextRequest) { } // XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * EQUP 마스터 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * 1. 최상위 테이블 (EQUP_MASTER_MATL) + * - MATNR이 unique 필드로 충돌 시 upsert 처리 + * + * 2. 하위 테이블들 (DESC, PLNT, UNIT, CLASSASGN, CHARASGN) + * - FK(MATNR)로 연결 + * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) + * - 전체 데이터셋 기반 삭제 후 재삽입 처리 + * + * XML 패턴: + * - EQUP 인터페이스는 XML에 MATNR이 이미 포함된 패턴 + * - 하위 테이블에도 MATNR 필드가 있어서 XML 값 우선 사용됨 + * + * @param matlData XML에서 파싱된 EQUP 데이터 + * @returns 처리된 EQUP 데이터 구조 + */ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { if (!matlData || !Array.isArray(matlData)) { return []; } return matlData.map(matl => { - // 메인 Material 데이터 변환 (자동) + const matnrKey = matl.MATNR || ''; + const fkData = { MATNR: matnrKey }; + + // 1단계: MATL (루트 - unique 필드: MATNR) const material = convertXMLToDBData( - matl as Record, - ['MATNR'] + matl as Record, + fkData ); - // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨) - if (!material.MATNR) { - material.MATNR = ''; - } - - // FK 데이터 준비 - const fkData = { MATNR: matl.MATNR || '' }; - - // Description 데이터 변환 (자동) + // 2단계: 하위 테이블들 (FK: MATNR) const descriptions = processNestedArray( matl.DESC, - (desc) => convertXMLToDBData(desc, ['MATNR'], fkData), + (desc) => convertXMLToDBData(desc as Record, fkData), fkData ); - // Plant 데이터 변환 (자동) const plants = processNestedArray( matl.PLNT, - (plnt) => convertXMLToDBData(plnt, ['MATNR'], fkData), + (plnt) => convertXMLToDBData(plnt as Record, fkData), fkData ); - // Unit 데이터 변환 (자동) const units = processNestedArray( matl.UNIT, - (unit) => convertXMLToDBData(unit, ['MATNR'], fkData), + (unit) => convertXMLToDBData(unit as Record, fkData), fkData ); - // Class Assignment 데이터 변환 (자동) const classAssignments = processNestedArray( matl.CLASSASGN, - (cls) => convertXMLToDBData(cls, ['MATNR'], fkData), + (cls) => convertXMLToDBData(cls as Record, fkData), fkData ); - // Characteristic Assignment 데이터 변환 (자동) const characteristicAssignments = processNestedArray( matl.CHARASGN, - (char) => convertXMLToDBData(char, ['MATNR'], fkData), + (char) => convertXMLToDBData(char as Record, fkData), fkData ); @@ -190,82 +200,57 @@ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { } // 데이터베이스 저장 함수 +/** + * 처리된 EQUP 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 최상위 테이블: MATNR 기준 upsert (충돌 시 업데이트) + * 2. 하위 테이블들: FK(MATNR) 기준 전체 삭제 후 재삽입 + * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 + * - 데이터 일관성과 단순성 확보 + * + * @param processedMaterials 변환된 EQUP 데이터 배열 + */ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { - console.log(`Starting database save for ${processedMaterials.length} equipment materials`); + console.log(`데이터베이스 저장 시작: ${processedMaterials.length}개 장비 데이터`); - await db.transaction(async (tx) => { - for (const materialData of processedMaterials) { - const { material, descriptions, plants, units, classAssignments, characteristicAssignments } = materialData; - - if (!material.MATNR) { - console.warn('Skipping material without MATNR'); - continue; - } - - console.log(`Processing MATNR: ${material.MATNR}`); - - // 1. MATL 테이블 Upsert (최상위 테이블) - await tx.insert(EQUP_MASTER_MATL) - .values(material) - .onConflictDoUpdate({ - target: EQUP_MASTER_MATL.MATNR, - set: { - ...material, - updatedAt: new Date(), - } - }); - - // 2. 하위 테이블 데이터 처리 - FK 기준으로 전체 삭제 후 재삽입 (병렬 처리) - await Promise.all([ - // DESC 테이블 처리 - replaceSubTableData( - tx, - EQUP_MASTER_MATL_DESC, - descriptions, - 'MATNR', - material.MATNR - ), - - // PLNT 테이블 처리 - replaceSubTableData( - tx, - EQUP_MASTER_MATL_PLNT, - plants, - 'MATNR', - material.MATNR - ), - - // UNIT 테이블 처리 - replaceSubTableData( - tx, - EQUP_MASTER_MATL_UNIT, - units, - 'MATNR', - material.MATNR - ), - - // CLASSASGN 테이블 처리 - replaceSubTableData( - tx, - EQUP_MASTER_MATL_CLASSASGN, - classAssignments, - 'MATNR', - material.MATNR - ), + try { + await db.transaction(async (tx) => { + for (const materialData of processedMaterials) { + const { material, descriptions, plants, units, classAssignments, characteristicAssignments } = materialData; - // CHARASGN 테이블 처리 - replaceSubTableData( - tx, - EQUP_MASTER_MATL_CHARASGN, - characteristicAssignments, - 'MATNR', - material.MATNR - ) - ]); + if (!material.MATNR) { + console.warn('자재번호(MATNR)가 없는 항목 발견, 건너뜁니다.'); + continue; + } - console.log(`Successfully processed MATNR: ${material.MATNR}`); - } - }); + // 1. MATL 테이블 Upsert (최상위 테이블 - unique 필드: MATNR) + await tx.insert(EQUP_MASTER_MATL) + .values(material) + .onConflictDoUpdate({ + target: EQUP_MASTER_MATL.MATNR, + set: { + ...material, + updatedAt: new Date(), + } + }); - console.log(`Database save completed for ${processedMaterials.length} equipment materials`); + // 2. 하위 테이블들 처리 - FK(MATNR) 기준 전체 삭제 후 재삽입 + // 전체 데이터셋 기반 처리로 데이터 일관성 확보 + await Promise.all([ + replaceSubTableData(tx, EQUP_MASTER_MATL_DESC, descriptions, 'MATNR', material.MATNR), + replaceSubTableData(tx, EQUP_MASTER_MATL_PLNT, plants, 'MATNR', material.MATNR), + replaceSubTableData(tx, EQUP_MASTER_MATL_UNIT, units, 'MATNR', material.MATNR), + replaceSubTableData(tx, EQUP_MASTER_MATL_CLASSASGN, classAssignments, 'MATNR', material.MATNR), + replaceSubTableData(tx, EQUP_MASTER_MATL_CHARASGN, characteristicAssignments, 'MATNR', material.MATNR) + ]); + } + }); + + console.log(`데이터베이스 저장 완료: ${processedMaterials.length}개 장비`); + return true; + } catch (error) { + console.error('데이터베이스 저장 중 오류 발생:', error); + throw error; + } } \ No newline at end of file diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts index 3992d788..19836c36 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts @@ -124,58 +124,68 @@ export async function POST(request: NextRequest) { } // XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * MATERIAL 마스터 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * 1. 최상위 테이블 (MATERIAL_MASTER_PART_MATL) + * - MATNR이 unique 필드로 충돌 시 upsert 처리 + * + * 2. 하위 테이블들 (DESC, PLNT, UNIT, CLASSASGN, CHARASGN) + * - FK(MATNR)로 연결 + * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) + * - 전체 데이터셋 기반 삭제 후 재삽입 처리 + * + * XML 패턴: + * - MATERIAL 인터페이스는 XML에 MATNR이 이미 포함된 패턴 + * - 하위 테이블에도 MATNR 필드가 있어서 XML 값 우선 사용됨 + * + * @param matlData XML에서 파싱된 MATERIAL 데이터 + * @returns 처리된 MATERIAL 데이터 구조 + */ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { if (!matlData || !Array.isArray(matlData)) { return []; } return matlData.map(matl => { - // 메인 Material 데이터 변환 (자동) + const matnrKey = matl.MATNR || ''; + const fkData = { MATNR: matnrKey }; + + // 1단계: MATL (루트 - unique 필드: MATNR) const material = convertXMLToDBData( - matl as Record, - ['MATNR'] + matl as Record, + fkData ); - // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨) - if (!material.MATNR) { - material.MATNR = ''; - } - - // FK 데이터 준비 - const fkData = { MATNR: matl.MATNR || '' }; - - // Description 데이터 변환 (자동) + // 2단계: 하위 테이블들 (FK: MATNR) const descriptions = processNestedArray( matl.DESC, - (desc) => convertXMLToDBData(desc, ['MATNR'], fkData), + (desc) => convertXMLToDBData(desc as Record, fkData), fkData ); - // Plant 데이터 변환 (자동) const plants = processNestedArray( matl.PLNT, - (plnt) => convertXMLToDBData(plnt, ['MATNR'], fkData), + (plnt) => convertXMLToDBData(plnt as Record, fkData), fkData ); - // Unit 데이터 변환 (자동) const units = processNestedArray( matl.UNIT, - (unit) => convertXMLToDBData(unit, ['MATNR'], fkData), + (unit) => convertXMLToDBData(unit as Record, fkData), fkData ); - // Class Assignment 데이터 변환 (자동) const classAssignments = processNestedArray( matl.CLASSASGN, - (cls) => convertXMLToDBData(cls, ['MATNR'], fkData), + (cls) => convertXMLToDBData(cls as Record, fkData), fkData ); - // Characteristic Assignment 데이터 변환 (자동) const characteristicAssignments = processNestedArray( matl.CHARASGN, - (char) => convertXMLToDBData(char, ['MATNR'], fkData), + (char) => convertXMLToDBData(char as Record, fkData), fkData ); @@ -191,8 +201,19 @@ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { } // 데이터베이스 저장 함수 +/** + * 처리된 MATERIAL 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 최상위 테이블: MATNR 기준 upsert (충돌 시 업데이트) + * 2. 하위 테이블들: FK(MATNR) 기준 전체 삭제 후 재삽입 + * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 + * - 데이터 일관성과 단순성 확보 + * + * @param processedMaterials 변환된 MATERIAL 데이터 배열 + */ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { - console.log(`데이터베이스 저장 함수가 호출됨. ${processedMaterials.length}개의 자재 데이터 수신.`); + console.log(`데이터베이스 저장 시작: ${processedMaterials.length}개 자재 데이터`); try { await db.transaction(async (tx) => { @@ -204,7 +225,7 @@ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { continue; } - // 1. MATL 테이블 Upsert (최상위 테이블) + // 1. MATL 테이블 Upsert (최상위 테이블 - unique 필드: MATNR) await tx.insert(MATERIAL_MASTER_PART_MATL) .values(material) .onConflictDoUpdate({ @@ -215,57 +236,19 @@ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { } }); - // 2. 하위 테이블 데이터 처리 - FK 기준으로 전체 삭제 후 재삽입 + // 2. 하위 테이블들 처리 - FK(MATNR) 기준 전체 삭제 후 재삽입 + // 전체 데이터셋 기반 처리로 데이터 일관성 확보 await Promise.all([ - // DESC 테이블 처리 - replaceSubTableData( - tx, - MATERIAL_MASTER_PART_MATL_DESC, - descriptions, - 'MATNR', - material.MATNR - ), - - // PLNT 테이블 처리 - replaceSubTableData( - tx, - MATERIAL_MASTER_PART_MATL_PLNT, - plants, - 'MATNR', - material.MATNR - ), - - // UNIT 테이블 처리 - replaceSubTableData( - tx, - MATERIAL_MASTER_PART_MATL_UNIT, - units, - 'MATNR', - material.MATNR - ), - - // CLASSASGN 테이블 처리 - replaceSubTableData( - tx, - MATERIAL_MASTER_PART_MATL_CLASSASGN, - classAssignments, - 'MATNR', - material.MATNR - ), - - // CHARASGN 테이블 처리 - replaceSubTableData( - tx, - MATERIAL_MASTER_PART_MATL_CHARASGN, - characteristicAssignments, - 'MATNR', - material.MATNR - ) + replaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_DESC, descriptions, 'MATNR', material.MATNR), + replaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_PLNT, plants, 'MATNR', material.MATNR), + replaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_UNIT, units, 'MATNR', material.MATNR), + replaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_CLASSASGN, classAssignments, 'MATNR', material.MATNR), + replaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_CHARASGN, characteristicAssignments, 'MATNR', material.MATNR) ]); } }); - console.log(`${processedMaterials.length}개의 자재 데이터 처리 완료.`); + console.log(`데이터베이스 저장 완료: ${processedMaterials.length}개 자재`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts index cb8de491..4e7cdf35 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts @@ -122,58 +122,68 @@ export async function POST(request: NextRequest) { } // XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * MODEL 마스터 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * 1. 최상위 테이블 (MODEL_MASTER_MATL) + * - MATNR이 unique 필드로 충돌 시 upsert 처리 + * + * 2. 하위 테이블들 (DESC, PLNT, UNIT, CLASSASGN, CHARASGN) + * - FK(MATNR)로 연결 + * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) + * - 전체 데이터셋 기반 삭제 후 재삽입 처리 + * + * XML 패턴: + * - MODEL 인터페이스는 XML에 MATNR이 이미 포함된 패턴 + * - 하위 테이블에도 MATNR 필드가 있어서 XML 값 우선 사용됨 + * + * @param matlData XML에서 파싱된 MODEL 데이터 + * @returns 처리된 MODEL 데이터 구조 + */ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { if (!matlData || !Array.isArray(matlData)) { return []; } return matlData.map(matl => { - // 메인 Material 데이터 변환 (자동) + const matnrKey = matl.MATNR || ''; + const fkData = { MATNR: matnrKey }; + + // 1단계: MATL (루트 - unique 필드: MATNR) const material = convertXMLToDBData( - matl as Record, - ['MATNR'] + matl as Record, + fkData ); - // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨) - if (!material.MATNR) { - material.MATNR = ''; - } - - // FK 데이터 준비 - const fkData = { MATNR: matl.MATNR || '' }; - - // Description 데이터 변환 (자동) + // 2단계: 하위 테이블들 (FK: MATNR) const descriptions = processNestedArray( matl.DESC, - (desc) => convertXMLToDBData(desc, ['MATNR'], fkData), + (desc) => convertXMLToDBData(desc as Record, fkData), fkData ); - // Plant 데이터 변환 (자동) const plants = processNestedArray( matl.PLNT, - (plnt) => convertXMLToDBData(plnt, ['MATNR'], fkData), + (plnt) => convertXMLToDBData(plnt as Record, fkData), fkData ); - // Unit 데이터 변환 (자동) const units = processNestedArray( matl.UNIT, - (unit) => convertXMLToDBData(unit, ['MATNR'], fkData), + (unit) => convertXMLToDBData(unit as Record, fkData), fkData ); - // Class Assignment 데이터 변환 (자동) const classAssignments = processNestedArray( matl.CLASSASGN, - (cls) => convertXMLToDBData(cls, ['MATNR'], fkData), + (cls) => convertXMLToDBData(cls as Record, fkData), fkData ); - // Characteristic Assignment 데이터 변환 (자동) const characteristicAssignments = processNestedArray( matl.CHARASGN, - (char) => convertXMLToDBData(char, ['MATNR'], fkData), + (char) => convertXMLToDBData(char as Record, fkData), fkData ); @@ -189,8 +199,19 @@ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { } // 데이터베이스 저장 함수 +/** + * 처리된 MODEL 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 최상위 테이블: MATNR 기준 upsert (충돌 시 업데이트) + * 2. 하위 테이블들: FK(MATNR) 기준 전체 삭제 후 재삽입 + * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 + * - 데이터 일관성과 단순성 확보 + * + * @param processedMaterials 변환된 MODEL 데이터 배열 + */ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { - console.log(`데이터베이스 저장 함수가 호출됨. ${processedMaterials.length}개의 자재 데이터 수신.`); + console.log(`데이터베이스 저장 시작: ${processedMaterials.length}개 모델 데이터`); try { await db.transaction(async (tx) => { @@ -202,7 +223,7 @@ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { continue; } - // 1. MATL 테이블 Upsert (최상위 테이블) + // 1. MATL 테이블 Upsert (최상위 테이블 - unique 필드: MATNR) await tx.insert(MODEL_MASTER_MATL) .values(material) .onConflictDoUpdate({ @@ -213,57 +234,19 @@ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { } }); - // 2. 하위 테이블 데이터 처리 - FK 기준으로 전체 삭제 후 재삽입 + // 2. 하위 테이블들 처리 - FK(MATNR) 기준 전체 삭제 후 재삽입 + // 전체 데이터셋 기반 처리로 데이터 일관성 확보 await Promise.all([ - // DESC 테이블 처리 - replaceSubTableData( - tx, - MODEL_MASTER_MATL_DESC, - descriptions, - 'MATNR', - material.MATNR - ), - - // PLNT 테이블 처리 - replaceSubTableData( - tx, - MODEL_MASTER_MATL_PLNT, - plants, - 'MATNR', - material.MATNR - ), - - // UNIT 테이블 처리 - replaceSubTableData( - tx, - MODEL_MASTER_MATL_UNIT, - units, - 'MATNR', - material.MATNR - ), - - // CLASSASGN 테이블 처리 - replaceSubTableData( - tx, - MODEL_MASTER_MATL_CLASSASGN, - classAssignments, - 'MATNR', - material.MATNR - ), - - // CHARASGN 테이블 처리 - replaceSubTableData( - tx, - MODEL_MASTER_MATL_CHARASGN, - characteristicAssignments, - 'MATNR', - material.MATNR - ) + replaceSubTableData(tx, MODEL_MASTER_MATL_DESC, descriptions, 'MATNR', material.MATNR), + replaceSubTableData(tx, MODEL_MASTER_MATL_PLNT, plants, 'MATNR', material.MATNR), + replaceSubTableData(tx, MODEL_MASTER_MATL_UNIT, units, 'MATNR', material.MATNR), + replaceSubTableData(tx, MODEL_MASTER_MATL_CLASSASGN, classAssignments, 'MATNR', material.MATNR), + replaceSubTableData(tx, MODEL_MASTER_MATL_CHARASGN, characteristicAssignments, 'MATNR', material.MATNR) ]); } }); - console.log(`${processedMaterials.length}개의 자재 데이터 처리 완료.`); + console.log(`데이터베이스 저장 완료: ${processedMaterials.length}개 모델`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts index c3f214e6..886e4851 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts @@ -133,7 +133,11 @@ export async function POST(request: NextRequest) { const parsedData = parser.parse(body); console.log('XML root keys:', Object.keys(parsedData)); - const requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_ORGANIZATION_MASTERReq'); + // IF_MDZ_EVCP_ORGANIZATION_MASTER 또는 IF_MDZ_EVCP_ORGANIZATION_MASTERReq 패턴 시도 + let requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_ORGANIZATION_MASTERReq'); + if (!requestData) { + requestData = extractRequestData(parsedData, 'IF_MDZ_EVCP_ORGANIZATION_MASTER'); + } if (!requestData) { console.error('Could not find valid request data in the received payload'); @@ -160,7 +164,18 @@ export async function POST(request: NextRequest) { }); } -// XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * ORGANIZATION 마스터 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * - 독립적인 여러 조직 테이블들을 처리 (CCTR, PCTR, ZBUKRS, ZEKGRP, ZEKORG, ZGSBER 등) + * - 각 테이블은 고유한 unique 필드를 가짐 + * - 일부 테이블은 하위 TEXT 테이블을 가짐 (CCTR, PCTR, ZGSBER) + * - 전체 데이터셋 기반 upsert 처리 + * + * @param requestData XML에서 파싱된 ORGANIZATION 데이터 + * @returns 처리된 ORGANIZATION 데이터 구조 + */ function transformOrganizationData(requestData: any): ProcessedOrganizationData { const result: ProcessedOrganizationData = { cctrItems: [], @@ -179,18 +194,20 @@ function transformOrganizationData(requestData: any): ProcessedOrganizationData zwerksItems: [] }; - // HRHMTB_CCTR 처리 + // HRHMTB_CCTR 처리 (unique 필드: CCTR, 하위 테이블: TEXT) if (requestData.items1 && Array.isArray(requestData.items1)) { result.cctrItems = requestData.items1.map((item: CctrXML) => { + const cctrKey = item.CCTR || ''; + const fkData = { CCTR: cctrKey }; + const cctr = convertXMLToDBData( - item as Record, - ['CCTR', 'KOKRS', 'DATBI'] + item as Record, + fkData ); - const fkData = { CCTR: item.CCTR || '' }; const texts = processNestedArray( item.TEXT, - (text) => convertXMLToDBData(text, [], fkData), + (text) => convertXMLToDBData(text as Record, fkData), fkData ); @@ -198,18 +215,20 @@ function transformOrganizationData(requestData: any): ProcessedOrganizationData }); } - // HRHMTB_PCTR 처리 + // HRHMTB_PCTR 처리 (unique 필드: PCTR, 하위 테이블: TEXT) if (requestData.items2 && Array.isArray(requestData.items2)) { result.pctrItems = requestData.items2.map((item: PctrXML) => { + const pctrKey = item.PCTR || ''; + const fkData = { CCTR: pctrKey }; // TEXT 테이블은 CCTR 필드를 사용 + const pctr = convertXMLToDBData( - item as Record, - ['PCTR', 'KOKRS', 'DATBI'] + item as Record, + fkData ); - const fkData = { CCTR: item.PCTR || '' }; // TEXT 테이블은 CCTR 필드를 사용 const texts = processNestedArray( item.TEXT, - (text) => convertXMLToDBData(text, [], fkData), + (text) => convertXMLToDBData(text as Record, fkData), fkData ); @@ -217,48 +236,47 @@ function transformOrganizationData(requestData: any): ProcessedOrganizationData }); } - // HRHMTB_ZBUKRS 처리 + // HRHMTB_ZBUKRS 처리 (unique 필드: ZBUKRS) if (requestData.items3 && Array.isArray(requestData.items3)) { result.zbukrsItems = requestData.items3.map((item: ZbukrsXML) => convertXMLToDBData( - item as Record, - ['ZBUKRS'] + item as Record ) ); } - // HRHMTB_ZEKGRP 처리 + // HRHMTB_ZEKGRP 처리 (unique 필드: ZEKGRP) if (requestData.items4 && Array.isArray(requestData.items4)) { result.zekgrpItems = requestData.items4.map((item: ZekgrpXML) => convertXMLToDBData( - item as Record, - ['ZEKGRP'] + item as Record ) ); } - // HRHMTB_ZEKORG 처리 + // HRHMTB_ZEKORG 처리 (unique 필드: ZEKORG) if (requestData.items5 && Array.isArray(requestData.items5)) { result.zekorgItems = requestData.items5.map((item: ZekorgXML) => convertXMLToDBData( - item as Record, - ['ZEKORG'] + item as Record ) ); } - // HRHMTB_ZGSBER 처리 + // HRHMTB_ZGSBER 처리 (unique 필드: ZGSBER, 하위 테이블: TEXT) if (requestData.items6 && Array.isArray(requestData.items6)) { result.zgsberItems = requestData.items6.map((item: ZgsberXML) => { + const zgsberKey = item.ZGSBER || ''; + const fkData = { ZGSBER: zgsberKey }; + const zgsber = convertXMLToDBData( - item as Record, - ['ZGSBER'] + item as Record, + fkData ); - const fkData = { ZGSBER: item.ZGSBER || '' }; const texts = processNestedArray( item.TEXT, - (text) => convertXMLToDBData(text, ['LANGU'], fkData), + (text) => convertXMLToDBData(text as Record, fkData), fkData ); @@ -266,82 +284,74 @@ function transformOrganizationData(requestData: any): ProcessedOrganizationData }); } - // HRHMTB_ZLGORT 처리 + // HRHMTB_ZLGORT 처리 (unique 필드: ZLGORT, ZWERKS) if (requestData.items7 && Array.isArray(requestData.items7)) { result.zlgortItems = requestData.items7.map((item: ZlgortXML) => convertXMLToDBData( - item as Record, - ['ZLGORT', 'ZWERKS'] + item as Record ) ); } - // HRHMTB_ZSPART 처리 + // HRHMTB_ZSPART 처리 (unique 필드: ZSPART) if (requestData.items8 && Array.isArray(requestData.items8)) { result.zspartItems = requestData.items8.map((item: ZspartXML) => convertXMLToDBData( - item as Record, - ['ZSPART'] + item as Record ) ); } - // HRHMTB_ZVKBUR 처리 + // HRHMTB_ZVKBUR 처리 (unique 필드: ZVKBUR) if (requestData.items9 && Array.isArray(requestData.items9)) { result.zvkburItems = requestData.items9.map((item: ZvkburXML) => convertXMLToDBData( - item as Record, - ['ZVKBUR'] + item as Record ) ); } - // HRHMTB_ZVKGRP 처리 + // HRHMTB_ZVKGRP 처리 (unique 필드: ZVKGRP) if (requestData.items10 && Array.isArray(requestData.items10)) { result.zvkgrpItems = requestData.items10.map((item: ZvkgrpXML) => convertXMLToDBData( - item as Record, - ['ZVKGRP'] + item as Record ) ); } - // HRHMTB_ZVKORG 처리 + // HRHMTB_ZVKORG 처리 (unique 필드: ZVKORG) if (requestData.items11 && Array.isArray(requestData.items11)) { result.zvkorgItems = requestData.items11.map((item: ZvkorgXML) => convertXMLToDBData( - item as Record, - ['ZVKORG'] + item as Record ) ); } - // HRHMTB_ZVSTEL 처리 + // HRHMTB_ZVSTEL 처리 (unique 필드: ZVSTEL) if (requestData.items12 && Array.isArray(requestData.items12)) { result.zvstelItems = requestData.items12.map((item: ZvstelXML) => convertXMLToDBData( - item as Record, - ['ZVSTEL'] + item as Record ) ); } - // HRHMTB_ZVTWEG 처리 + // HRHMTB_ZVTWEG 처리 (unique 필드: ZVTWEG) if (requestData.items13 && Array.isArray(requestData.items13)) { result.zvtwegItems = requestData.items13.map((item: ZvtwegXML) => convertXMLToDBData( - item as Record, - ['ZVTWEG'] + item as Record ) ); } - // HRHMTB_ZWERKS 처리 + // HRHMTB_ZWERKS 처리 (unique 필드: ZWERKS) if (requestData.items14 && Array.isArray(requestData.items14)) { result.zwerksItems = requestData.items14.map((item: ZwerksXML) => convertXMLToDBData( - item as Record, - ['ZWERKS'] + item as Record ) ); } @@ -350,12 +360,23 @@ function transformOrganizationData(requestData: any): ProcessedOrganizationData } // 데이터베이스 저장 함수 +/** + * 처리된 ORGANIZATION 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 각 조직 테이블별로 고유 필드 기준 upsert 처리 + * 2. 하위 TEXT 테이블들: FK 기준 전체 삭제 후 재삽입 + * - CCTR_TEXT, ZGSBER_TEXT 포함 + * 3. 14개의 독립적인 조직 테이블 처리 + * + * @param processedOrganizations 변환된 ORGANIZATION 데이터 + */ async function saveToDatabase(processedOrganizations: ProcessedOrganizationData) { - console.log('데이터베이스 저장 함수가 호출됨. 조직 마스터 데이터 수신.'); + console.log('데이터베이스 저장 시작: 조직 마스터 데이터'); try { await db.transaction(async (tx) => { - // CCTR 테이블 처리 + // CCTR 테이블 처리 (unique 필드: CCTR) for (const { cctr, texts } of processedOrganizations.cctrItems) { if (!cctr.CCTR) continue; @@ -369,7 +390,7 @@ async function saveToDatabase(processedOrganizations: ProcessedOrganizationData) await replaceSubTableData(tx, ORGANIZATION_MASTER_HRHMTB_CCTR_TEXT, texts, 'CCTR', cctr.CCTR); } - // PCTR 테이블 처리 + // PCTR 테이블 처리 (unique 필드: PCTR) for (const { pctr, texts } of processedOrganizations.pctrItems) { if (!pctr.PCTR) continue; diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts index e257a28a..d59246c2 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts @@ -172,6 +172,25 @@ export async function POST(request: NextRequest) { } // XML 데이터를 DB 삽입 가능한 형태로 변환 +/** + * VENDOR 마스터 데이터 변환 함수 + * + * 데이터 처리 아키텍처: + * 1. 최상위 테이블 (VENDOR_MASTER_BP_HEADER) + * - VNDRCD가 unique 필드로 충돌 시 upsert 처리 + * + * 2. 하위 테이블들 (ADDRESS, BP_TAXNUM, BP_VENGEN 등) + * - FK(VNDRCD)로 연결 + * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) + * - 전체 데이터셋 기반 삭제 후 재삽입 처리 + * + * 3. 중첩 하위 테이블들 (AD_EMAIL, AD_FAX, BP_COMPNY 등) + * - 동일하게 FK(VNDRCD)로 연결 + * - 전체 데이터셋 기반 처리 + * + * @param vendorHeaderData XML에서 파싱된 VENDOR 헤더 데이터 + * @returns 처리된 VENDOR 데이터 구조 + */ function transformVendorData(vendorHeaderData: VendorHeaderXML[]): ProcessedVendorData[] { if (!vendorHeaderData || !Array.isArray(vendorHeaderData)) { return []; @@ -181,84 +200,80 @@ function transformVendorData(vendorHeaderData: VendorHeaderXML[]): ProcessedVend const vndrcdKey = vendorHeader.VNDRCD || ''; const fkData = { VNDRCD: vndrcdKey }; - // 1단계: VENDOR_HEADER (루트) + // 1단계: VENDOR_HEADER (루트 - unique 필드: VNDRCD) const vendorHeaderConverted = convertXMLToDBData( vendorHeader as Record, - ['VNDRCD'], fkData ); - // 2단계: ADDRESS와 직속 하위들 + // 2단계: 직속 하위 테이블들 (FK: VNDRCD) const addresses = processNestedArray( vendorHeader.ADDRESS, - (addr) => convertXMLToDBData(addr as Record, ['ADR_NO'], fkData), + (addr) => convertXMLToDBData(addr as Record, fkData), fkData ); - // ADDRESS의 하위 테이블들 (3단계) + const bpTaxnums = processNestedArray( + vendorHeader.BP_TAXNUM, + (item) => convertXMLToDBData(item as Record, fkData), + fkData + ); + + const bpVengens = processNestedArray( + vendorHeader.BP_VENGEN, + (vengen) => convertXMLToDBData(vengen as Record, fkData), + fkData + ); + + // 3단계: ADDRESS의 하위 테이블들 (FK: VNDRCD) const adEmails = vendorHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_EMAIL, (item) => - convertXMLToDBData(item as Record, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const adFaxes = vendorHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_FAX, (item) => - convertXMLToDBData(item as Record, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const adPostals = vendorHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_POSTAL, (item) => - convertXMLToDBData(item as Record, ['INTL_ADR_VER_ID'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const adTels = vendorHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_TEL, (item) => - convertXMLToDBData(item as Record, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const adUrls = vendorHeader.ADDRESS?.flatMap(addr => processNestedArray(addr.AD_URL, (item) => - convertXMLToDBData(item as Record, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; - // 2단계: BP_TAXNUM - const bpTaxnums = processNestedArray( - vendorHeader.BP_TAXNUM, - (item) => convertXMLToDBData(item as Record, ['TX_NO_CTG'], fkData), - fkData - ); - - // 2단계: BP_VENGEN과 하위들 - const bpVengens = processNestedArray( - vendorHeader.BP_VENGEN, - (vengen) => convertXMLToDBData(vengen as Record, ['VNDRNO'], fkData), - fkData - ); - - // BP_VENGEN의 하위 테이블들 (3단계) + // 3단계: BP_VENGEN의 하위 테이블들 (FK: VNDRCD) const bpCompnies = vendorHeader.BP_VENGEN?.flatMap(vengen => processNestedArray(vengen.BP_COMPNY, (item) => - convertXMLToDBData(item as Record, ['CO_CD'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; const bpPorgs = vendorHeader.BP_VENGEN?.flatMap(vengen => processNestedArray(vengen.BP_PORG, (item) => - convertXMLToDBData(item as Record, ['PUR_ORG_CD'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || []; - // BP_COMPNY의 하위 테이블 (4단계) + // 4단계: 더 깊은 중첩 테이블들 (FK: VNDRCD) const bpWhtaxes = vendorHeader.BP_VENGEN?.flatMap(vengen => vengen.BP_COMPNY?.flatMap(compny => processNestedArray(compny.BP_WHTAX, (item) => - convertXMLToDBData(item as Record, ['SRCE_TX_TP'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || [] ) || []; - // BP_PORG의 하위 테이블 (4단계) const zvpfns = vendorHeader.BP_VENGEN?.flatMap(vengen => vengen.BP_PORG?.flatMap(porg => processNestedArray(porg.ZVPFN, (item) => - convertXMLToDBData(item as Record, ['PTNR_SKL', 'PTNR_CNT'], fkData), fkData) + convertXMLToDBData(item as Record, fkData), fkData) ) || [] ) || []; @@ -281,6 +296,17 @@ function transformVendorData(vendorHeaderData: VendorHeaderXML[]): ProcessedVend } // 데이터베이스 저장 함수 +/** + * 처리된 VENDOR 데이터를 데이터베이스에 저장 + * + * 저장 전략: + * 1. 최상위 테이블: VNDRCD 기준 upsert (충돌 시 업데이트) + * 2. 하위 테이블들: FK(VNDRCD) 기준 전체 삭제 후 재삽입 + * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 + * - 데이터 일관성과 단순성 확보 + * + * @param processedVendors 변환된 VENDOR 데이터 배열 + */ async function saveToDatabase(processedVendors: ProcessedVendorData[]) { console.log(`데이터베이스 저장 시작: ${processedVendors.length}개 벤더 데이터`); @@ -295,7 +321,7 @@ async function saveToDatabase(processedVendors: ProcessedVendorData[]) { continue; } - // 1. BP_HEADER 테이블 Upsert (최상위 테이블) + // 1. BP_HEADER 테이블 Upsert (최상위 테이블 - unique 필드: VNDRCD) await tx.insert(VENDOR_MASTER_BP_HEADER) .values(vendorHeader) .onConflictDoUpdate({ @@ -306,14 +332,15 @@ async function saveToDatabase(processedVendors: ProcessedVendorData[]) { } }); - // 2. 하위 테이블들 처리 - FK 기준으로 전체 삭제 후 재삽입 + // 2. 하위 테이블들 처리 - FK(VNDRCD) 기준 전체 삭제 후 재삽입 + // 전체 데이터셋 기반 처리로 데이터 일관성 확보 await Promise.all([ // 2단계 테이블들 replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS, addresses, 'VNDRCD', vendorHeader.VNDRCD), replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_TAXNUM, bpTaxnums, 'VNDRCD', vendorHeader.VNDRCD), replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN, bpVengens, 'VNDRCD', vendorHeader.VNDRCD), - // 3-4단계 테이블들 + // 3-4단계 테이블들 - 동일하게 FK(VNDRCD) 기준 처리 replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, adEmails, 'VNDRCD', vendorHeader.VNDRCD), replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_FAX, adFaxes, 'VNDRCD', vendorHeader.VNDRCD), replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, adPostals, 'VNDRCD', vendorHeader.VNDRCD), @@ -327,10 +354,10 @@ async function saveToDatabase(processedVendors: ProcessedVendorData[]) { } }); - console.log(`✅ 데이터베이스 저장 완료: ${processedVendors.length}개 벤더`); + console.log(`데이터베이스 저장 완료: ${processedVendors.length}개 벤더`); return true; } catch (error) { - console.error('❌ 데이터베이스 저장 중 오류 발생:', error); + console.error('데이터베이스 저장 중 오류 발생:', error); throw error; } } diff --git a/app/api/(S-ERP)/(MDG)/utils.ts b/app/api/(S-ERP)/(MDG)/utils.ts index bcb1dd45..437988dc 100644 --- a/app/api/(S-ERP)/(MDG)/utils.ts +++ b/app/api/(S-ERP)/(MDG)/utils.ts @@ -1,8 +1,8 @@ import { XMLParser } from "fast-xml-parser"; import { readFileSync } from "fs"; -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { join } from "path"; -import { eq, desc } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import db from "@/db/db"; import { soapLogs, type LogDirection, type SoapLogInsert } from "@/db/schema/SOAP/soap"; @@ -19,6 +19,8 @@ export interface SoapBodyData { // WSDL 파일 제공 함수 export function serveWsdl(wsdlFileName: string) { try { + // public/wsdl 에서 WSDL 제공함을 가정 + // 이게 WSDL 구현 표준인데, 보안 감사에서 반대한다면 제거 const wsdlPath = join(process.cwd(), 'public', 'wsdl', wsdlFileName); const wsdlContent = readFileSync(wsdlPath, 'utf-8'); @@ -33,7 +35,8 @@ export function serveWsdl(wsdlFileName: string) { } } -// XML 파서 생성 (기본 설정) +// XML 파서 생성 +// SAP XI 가 자동생성해 보내는 XML을 처리할 수 있도록 설정함 export function createXMLParser(arrayTags: string[] = []) { return new XMLParser({ ignoreAttributes: false, @@ -51,7 +54,7 @@ export function extractRequestData( parsedData: Record, requestKeyPattern: string ): SoapBodyData | null { - // SOAP 구조 체크 + // SOAP 구조 체크 (방어적) const soapPaths = [ ['soap:Envelope', 'soap:Body'], ['SOAP:Envelope', 'SOAP:Body'], @@ -60,8 +63,9 @@ export function extractRequestData( ]; for (const [envelope, body] of soapPaths) { - if (parsedData?.[envelope]?.[body]) { - const result = extractFromSoapBody(parsedData[envelope][body] as SoapBodyData, requestKeyPattern); + const envelopeData = parsedData?.[envelope] as Record | undefined; + if (envelopeData?.[body]) { + const result = extractFromSoapBody(envelopeData[body] as SoapBodyData, requestKeyPattern); if (result) return result; } } @@ -126,9 +130,26 @@ function extractFromSoapBody(soapBody: SoapBodyData, requestKeyPattern: string): } // 범용 XML → DB 변환 함수 +/** + * XML 데이터를 DB 삽입 가능한 형태로 변환 + * + * 아키텍처 설계: + * - 하위 테이블들은 별도의 필수 필드가 없다고 가정 (스키마에서 notNull() 제거 예정) + * - FK는 항상 최상위 테이블의 unique 필드를 참조 + * - 송신된 XML은 항상 전체 데이터셋을 포함 + * - 최상위 테이블의 unique 필드가 충돌하면 전체 삭제 후 재삽입 처리 + * + * FK 처리 방식: + * - XML에 FK 필드가 이미 포함된 경우: XML 값 우선 사용 (예: MATL 인터페이스) + * - XML에 FK 필드가 없는 경우: 상위에서 전달받은 FK 값 사용 (예: VENDOR 인터페이스) + * - 이를 통해 다양한 SAP 인터페이스 패턴에 대응 + * + * @param xmlData XML에서 파싱된 데이터 + * @param fkData 상위 테이블에서 전달받은 FK 데이터 + * @returns DB 삽입 가능한 형태로 변환된 데이터 + */ export function convertXMLToDBData>( xmlData: Record, - requiredFields: (keyof T)[] = [], fkData?: Record ): T { const result = {} as T; @@ -141,20 +162,35 @@ export function convertXMLToDBData>( } } - // 필수 필드 처리 (FK 등) - for (const field of requiredFields) { - if (!result[field] && fkData) { - const fieldStr = String(field); - if (fkData[fieldStr]) { - (result as Record)[field] = fkData[fieldStr]; + // FK 필드 처리 (XML 우선, 없으면 상위에서 전달받은 값 사용) + if (fkData) { + for (const [key, value] of Object.entries(fkData)) { + // XML에 해당 FK 필드가 없거나 비어있는 경우에만 상위 값 사용 + const existingValue = (result as Record)[key]; + if (!existingValue || existingValue === null || existingValue === '') { + (result as Record)[key] = value; } + // XML에 이미 FK 필드가 있고 값이 있는 경우는 XML 값을 그대로 사용 } } return result; } -// 중첩 배열 처리 함수 +// 중첩 배열 처리 함수 (개선된 버전) +/** + * 중첩된 배열 데이터를 처리하여 DB 삽입 가능한 형태로 변환 + * + * 처리 방식: + * - 하위 테이블 데이터는 FK만 설정하면 됨 + * - 별도의 필수 필드 생성 로직 불필요 + * - 전체 데이터셋 기반으로 삭제 후 재삽입 처리 + * + * @param items 처리할 배열 데이터 + * @param converter 변환 함수 + * @param fkData FK 데이터 + * @returns 변환된 배열 데이터 + */ export function processNestedArray( items: T[] | undefined, converter: (item: T, fkData?: Record) => U, @@ -207,9 +243,27 @@ export function createSuccessResponse(namespace: string): NextResponse { } // 하위 테이블 처리: FK 기준으로 전체 삭제 후 재삽입 -export async function replaceSubTableData( - tx: any, - table: any, +/** + * 하위 테이블 데이터를 전체 삭제 후 재삽입하는 함수 + * + * 처리 전략: + * - 송신 XML이 전체 데이터셋을 포함한다는 가정하에 설계 + * - 부분 업데이트보다 전체 교체를 통해 데이터 일관성 확보 + * - FK 기준으로 해당 부모 레코드의 모든 하위 데이터 교체 + * + * 처리 순서: + * 1. FK 기준으로 기존 데이터 전체 삭제 + * 2. 새로운 데이터 전체 삽입 + * + * @param tx 트랜잭션 객체 + * @param table 대상 테이블 스키마 + * @param data 삽입할 데이터 배열 + * @param parentField FK 필드명 (일반적으로 'VNDRCD') + * @param parentValue FK 값 (상위 테이블의 unique 필드 값) + */ +export async function replaceSubTableData>( + tx: Parameters[0]>[0], + table: any, // Drizzle 테이블 객체 - 복잡한 제네릭 타입으로 인해 any 사용 data: T[], parentField: string, parentValue: string -- cgit v1.2.3