summaryrefslogtreecommitdiff
path: root/app/api/(S-ERP)
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/(S-ERP)')
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_CUSTOMER_MASTER/route.ts100
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_DEPARTMENT_CODE/route.ts94
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_MASTER/route.ts101
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EMPLOYEE_REFERENCE_MASTER/route.ts64
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_EQUP_MASTER/route.ts173
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts121
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MODEL_MASTER/route.ts121
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_ORGANIZATION_MASTER/route.ts127
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts99
-rw-r--r--app/api/(S-ERP)/(MDG)/utils.ts88
10 files changed, 605 insertions, 483 deletions
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<BpHeaderData>(
bpHeader as Record<string, string | undefined>,
- ['BP_HEADER'],
fkData
);
- // 2단계: ADDRESS와 직속 하위들
+ // 2단계: 직속 하위 테이블들 (FK: BP_HEADER)
const addresses = processNestedArray(
bpHeader.ADDRESS,
- (addr) => convertXMLToDBData<AddressData>(addr as Record<string, string | undefined>, ['ADDRNO'], fkData),
+ (addr) => convertXMLToDBData<AddressData>(addr as Record<string, string | undefined>, fkData),
fkData
);
- // ADDRESS의 하위 테이블들 (3단계)
+ const bpTaxnums = processNestedArray(
+ bpHeader.BP_TAXNUM,
+ (item) => convertXMLToDBData<BpTaxnumData>(item as Record<string, string | undefined>, fkData),
+ fkData
+ );
+
+ const bpCusgens = processNestedArray(
+ bpHeader.BP_CUSGEN,
+ (cusgen) => convertXMLToDBData<BpCusgenData>(cusgen as Record<string, string | undefined>, fkData),
+ fkData
+ );
+
+ // 3단계: ADDRESS의 하위 테이블들 (FK: BP_HEADER)
const adEmails = bpHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_EMAIL, (item) =>
- convertXMLToDBData<AdEmailData>(item as Record<string, string | undefined>, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData)
+ convertXMLToDBData<AdEmailData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const adFaxes = bpHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_FAX, (item) =>
- convertXMLToDBData<AdFaxData>(item as Record<string, string | undefined>, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData)
+ convertXMLToDBData<AdFaxData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const adPostals = bpHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_POSTAL, (item) =>
- convertXMLToDBData<AdPostalData>(item as Record<string, string | undefined>, ['NATION'], fkData), fkData)
+ convertXMLToDBData<AdPostalData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const adTels = bpHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_TEL, (item) =>
- convertXMLToDBData<AdTelData>(item as Record<string, string | undefined>, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData)
+ convertXMLToDBData<AdTelData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const adUrls = bpHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_URL, (item) =>
- convertXMLToDBData<AdUrlData>(item as Record<string, string | undefined>, ['CONSNUMBER', 'DATE_FROM'], fkData), fkData)
+ convertXMLToDBData<AdUrlData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
- // 2단계: BP_TAXNUM
- const bpTaxnums = processNestedArray(
- bpHeader.BP_TAXNUM,
- (item) => convertXMLToDBData<BpTaxnumData>(item as Record<string, string | undefined>, ['TAXTYPE'], fkData),
- fkData
- );
-
- // 2단계: BP_CUSGEN과 하위들
- const bpCusgens = processNestedArray(
- bpHeader.BP_CUSGEN,
- (cusgen) => convertXMLToDBData<BpCusgenData>(cusgen as Record<string, string | undefined>, ['KUNNR'], fkData),
- fkData
- );
-
- // BP_CUSGEN의 하위 테이블들 (3단계)
+ // 3단계: BP_CUSGEN의 하위 테이블들 (FK: BP_HEADER)
const zvatregs = bpHeader.BP_CUSGEN?.flatMap(cusgen =>
processNestedArray(cusgen.ZVATREG, (item) =>
- convertXMLToDBData<ZvatregData>(item as Record<string, string | undefined>, ['LAND1'], fkData), fkData)
+ convertXMLToDBData<ZvatregData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const ztaxinds = bpHeader.BP_CUSGEN?.flatMap(cusgen =>
processNestedArray(cusgen.ZTAXIND, (item) =>
- convertXMLToDBData<ZtaxindData>(item as Record<string, string | undefined>, ['ALAND', 'TATYP'], fkData), fkData)
+ convertXMLToDBData<ZtaxindData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const zcompanies = bpHeader.BP_CUSGEN?.flatMap(cusgen =>
processNestedArray(cusgen.ZCOMPANY, (item) =>
- convertXMLToDBData<ZcompanyData>(item as Record<string, string | undefined>, ['BUKRS'], fkData), fkData)
+ convertXMLToDBData<ZcompanyData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const zsales = bpHeader.BP_CUSGEN?.flatMap(cusgen =>
processNestedArray(cusgen.ZSALES, (item) =>
- convertXMLToDBData<ZsalesData>(item as Record<string, string | undefined>, ['VKORG', 'VTWEG', 'SPART'], fkData), fkData)
+ convertXMLToDBData<ZsalesData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
- // ZSALES의 하위 테이블 (4단계)
+ // 4단계: 더 깊은 중첩 테이블들 (FK: BP_HEADER)
const zcpfns = bpHeader.BP_CUSGEN?.flatMap(cusgen =>
cusgen.ZSALES?.flatMap(sales =>
processNestedArray(sales.ZCPFN, (item) =>
- convertXMLToDBData<ZcpfnData>(item as Record<string, string | undefined>, ['PARVW', 'PARZA'], fkData), fkData)
+ convertXMLToDBData<ZcpfnData>(item as Record<string, string | undefined>, 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<DeptData>(
- dept as Record<string, string | undefined>,
- ['DEPTCD', 'CORPCD']
+ dept as Record<string, string | undefined>,
+ 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<DeptnmData>(deptnm, ['SPRAS'], fkData),
+ (deptnm) => convertXMLToDBData<DeptnmData>(deptnm as Record<string, string | undefined>, fkData),
fkData
);
- // COMPNM 데이터 변환
const compnms = processNestedArray(
dept.COMPNM,
- (compnm) => convertXMLToDBData<CompnmData>(compnm, ['SPRAS'], fkData),
+ (compnm) => convertXMLToDBData<CompnmData>(compnm as Record<string, string | undefined>, fkData),
fkData
);
- // CORPNM 데이터 변환
const corpnms = processNestedArray(
dept.CORPNM,
- (corpnm) => convertXMLToDBData<CorpnmData>(corpnm, ['SPRAS'], fkData),
+ (corpnm) => convertXMLToDBData<CorpnmData>(corpnm as Record<string, string | undefined>, 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<EmpMdgData>(
- emp as Record<string, string | undefined>,
- ['EMPID'],
+ emp as Record<string, string | undefined>,
fkData
);
- // 하위 테이블 데이터 변환
- const banm = processNestedArray(emp.BANM, (item) => convertXMLToDBData<EmpBanmData>(item, ['SPRAS'], fkData), fkData);
- const binm = processNestedArray(emp.BINM, (item) => convertXMLToDBData<EmpBinmData>(item, ['SPRAS'], fkData), fkData);
- const compnm = processNestedArray(emp.COMPNM, (item) => convertXMLToDBData<EmpCompnmData>(item, ['SPRAS'], fkData), fkData);
- const corpnm = processNestedArray(emp.CORPNM, (item) => convertXMLToDBData<EmpCorpnmData>(item, ['SPRAS'], fkData), fkData);
- const countrynm = processNestedArray(emp.COUNTRYNM, (item) => convertXMLToDBData<EmpCountrynmData>(item, ['SPRAS'], fkData), fkData);
- const deptcode = processNestedArray(emp.DEPTCODE, (item) => convertXMLToDBData<EmpDeptcodeData>(item, [], fkData), fkData);
- const deptcodePccdnm = processNestedArray(emp.DEPTCODE_PCCDNM, (item) => convertXMLToDBData<EmpDeptcodePccdnmData>(item, [], fkData), fkData);
- const deptnm = processNestedArray(emp.DEPTNM, (item) => convertXMLToDBData<EmpDeptnmData>(item, ['SPRAS'], fkData), fkData);
- const dhjobgdnm = processNestedArray(emp.DHJOBGDNM, (item) => convertXMLToDBData<EmpDhjobgdnmData>(item, ['SPRAS'], fkData), fkData);
- const gjobdutynm = processNestedArray(emp.GJOBDUTYNM, (item) => convertXMLToDBData<EmpGjobdutynmData>(item, ['SPRAS'], fkData), fkData);
- const gjobgrdnm = processNestedArray(emp.GJOBGRDNM, (item) => convertXMLToDBData<EmpGjobgrdnmData>(item, ['SPRAS'], fkData), fkData);
- const gjobgrdtype = processNestedArray(emp.GJOBGRDTYPE, (item) => convertXMLToDBData<EmpGjobgrdtypeData>(item, [], fkData), fkData);
- const gjobnm = processNestedArray(emp.GJOBNM, (item) => convertXMLToDBData<EmpGjobnmData>(item, ['SPRAS'], fkData), fkData);
- const gnnm = processNestedArray(emp.GNNM, (item) => convertXMLToDBData<EmpGnnmData>(item, ['SPRAS'], fkData), fkData);
- const jobdutynm = processNestedArray(emp.JOBDUTYNM, (item) => convertXMLToDBData<EmpJobdutynmData>(item, ['SPRAS'], fkData), fkData);
- const jobgrdnm = processNestedArray(emp.JOBGRDNM, (item) => convertXMLToDBData<EmpJobgrdnmData>(item, ['SPRAS'], fkData), fkData);
- const jobnm = processNestedArray(emp.JOBNM, (item) => convertXMLToDBData<EmpJobnmData>(item, ['SPRAS'], fkData), fkData);
- const ktlnm = processNestedArray(emp.KTLNM, (item) => convertXMLToDBData<EmpKtlnmData>(item, ['SPRAS'], fkData), fkData);
- const oktlnm = processNestedArray(emp.OKTLNM, (item) => convertXMLToDBData<EmpOktlnmData>(item, ['SPRAS'], fkData), fkData);
- const orgbicdnm = processNestedArray(emp.ORGBICDNM, (item) => convertXMLToDBData<EmpOrgbicdnmData>(item, ['SPRAS'], fkData), fkData);
- const orgcompnm = processNestedArray(emp.ORGCOMPNM, (item) => convertXMLToDBData<EmpOrgcompnmData>(item, ['SPRAS'], fkData), fkData);
- const orgcorpnm = processNestedArray(emp.ORGCORPNM, (item) => convertXMLToDBData<EmpOrgcorpnmData>(item, ['SPRAS'], fkData), fkData);
- const orgdeptnm = processNestedArray(emp.ORGDEPTNM, (item) => convertXMLToDBData<EmpOrgdeptnmData>(item, ['SPRAS'], fkData), fkData);
- const orgpdepnm = processNestedArray(emp.ORGPDEPNM, (item) => convertXMLToDBData<EmpOrgpdepnmData>(item, ['SPRAS'], fkData), fkData);
- const pdeptnm = processNestedArray(emp.PDEPTNM, (item) => convertXMLToDBData<EmpPdeptnmData>(item, [], fkData), fkData);
+ // 2단계: 하위 테이블들 (FK: EMPID) - 25개 테이블
+ const banm = processNestedArray(emp.BANM, (item) => convertXMLToDBData<EmpBanmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const binm = processNestedArray(emp.BINM, (item) => convertXMLToDBData<EmpBinmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const compnm = processNestedArray(emp.COMPNM, (item) => convertXMLToDBData<EmpCompnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const corpnm = processNestedArray(emp.CORPNM, (item) => convertXMLToDBData<EmpCorpnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const countrynm = processNestedArray(emp.COUNTRYNM, (item) => convertXMLToDBData<EmpCountrynmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const deptcode = processNestedArray(emp.DEPTCODE, (item) => convertXMLToDBData<EmpDeptcodeData>(item as Record<string, string | undefined>, fkData), fkData);
+ const deptcodePccdnm = processNestedArray(emp.DEPTCODE_PCCDNM, (item) => convertXMLToDBData<EmpDeptcodePccdnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const deptnm = processNestedArray(emp.DEPTNM, (item) => convertXMLToDBData<EmpDeptnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const dhjobgdnm = processNestedArray(emp.DHJOBGDNM, (item) => convertXMLToDBData<EmpDhjobgdnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const gjobdutynm = processNestedArray(emp.GJOBDUTYNM, (item) => convertXMLToDBData<EmpGjobdutynmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const gjobgrdnm = processNestedArray(emp.GJOBGRDNM, (item) => convertXMLToDBData<EmpGjobgrdnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const gjobgrdtype = processNestedArray(emp.GJOBGRDTYPE, (item) => convertXMLToDBData<EmpGjobgrdtypeData>(item as Record<string, string | undefined>, fkData), fkData);
+ const gjobnm = processNestedArray(emp.GJOBNM, (item) => convertXMLToDBData<EmpGjobnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const gnnm = processNestedArray(emp.GNNM, (item) => convertXMLToDBData<EmpGnnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const jobdutynm = processNestedArray(emp.JOBDUTYNM, (item) => convertXMLToDBData<EmpJobdutynmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const jobgrdnm = processNestedArray(emp.JOBGRDNM, (item) => convertXMLToDBData<EmpJobgrdnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const jobnm = processNestedArray(emp.JOBNM, (item) => convertXMLToDBData<EmpJobnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const ktlnm = processNestedArray(emp.KTLNM, (item) => convertXMLToDBData<EmpKtlnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const oktlnm = processNestedArray(emp.OKTLNM, (item) => convertXMLToDBData<EmpOktlnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const orgbicdnm = processNestedArray(emp.ORGBICDNM, (item) => convertXMLToDBData<EmpOrgbicdnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const orgcompnm = processNestedArray(emp.ORGCOMPNM, (item) => convertXMLToDBData<EmpOrgcompnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const orgcorpnm = processNestedArray(emp.ORGCORPNM, (item) => convertXMLToDBData<EmpOrgcorpnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const orgdeptnm = processNestedArray(emp.ORGDEPTNM, (item) => convertXMLToDBData<EmpOrgdeptnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const orgpdepnm = processNestedArray(emp.ORGPDEPNM, (item) => convertXMLToDBData<EmpOrgpdepnmData>(item as Record<string, string | undefined>, fkData), fkData);
+ const pdeptnm = processNestedArray(emp.PDEPTNM, (item) => convertXMLToDBData<EmpPdeptnmData>(item as Record<string, string | undefined>, 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<EmpRefData>(
- empRef as Record<string, string | undefined>,
- ['GRPCD', 'CORPCD', 'MAINCD']
+ empRef as Record<string, string | undefined>,
+ fkData
);
- // 필수 필드 보정
- if (!empRefRecord.GRPCD) {
- empRefRecord.GRPCD = '';
- }
-
- // FK 데이터 준비
- const fkData = { GRPCD: empRef.GRPCD || '' };
-
- // Name 데이터 변환
+ // 2단계: 하위 테이블들 (FK: GRPCD)
const names = processNestedArray(
empRef.NAME,
- (name) => convertXMLToDBData<EmpRefNameData>(name, ['SPRAS'], fkData),
+ (name) => convertXMLToDBData<EmpRefNameData>(name as Record<string, string | undefined>, 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<MatlData>(
- matl as Record<string, string | undefined>,
- ['MATNR']
+ matl as Record<string, string | undefined>,
+ fkData
);
- // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨)
- if (!material.MATNR) {
- material.MATNR = '';
- }
-
- // FK 데이터 준비
- const fkData = { MATNR: matl.MATNR || '' };
-
- // Description 데이터 변환 (자동)
+ // 2단계: 하위 테이블들 (FK: MATNR)
const descriptions = processNestedArray(
matl.DESC,
- (desc) => convertXMLToDBData<MatlDescData>(desc, ['MATNR'], fkData),
+ (desc) => convertXMLToDBData<MatlDescData>(desc as Record<string, string | undefined>, fkData),
fkData
);
- // Plant 데이터 변환 (자동)
const plants = processNestedArray(
matl.PLNT,
- (plnt) => convertXMLToDBData<MatlPlntData>(plnt, ['MATNR'], fkData),
+ (plnt) => convertXMLToDBData<MatlPlntData>(plnt as Record<string, string | undefined>, fkData),
fkData
);
- // Unit 데이터 변환 (자동)
const units = processNestedArray(
matl.UNIT,
- (unit) => convertXMLToDBData<MatlUnitData>(unit, ['MATNR'], fkData),
+ (unit) => convertXMLToDBData<MatlUnitData>(unit as Record<string, string | undefined>, fkData),
fkData
);
- // Class Assignment 데이터 변환 (자동)
const classAssignments = processNestedArray(
matl.CLASSASGN,
- (cls) => convertXMLToDBData<MatlClassAsgnData>(cls, ['MATNR'], fkData),
+ (cls) => convertXMLToDBData<MatlClassAsgnData>(cls as Record<string, string | undefined>, fkData),
fkData
);
- // Characteristic Assignment 데이터 변환 (자동)
const characteristicAssignments = processNestedArray(
matl.CHARASGN,
- (char) => convertXMLToDBData<MatlCharAsgnData>(char, ['MATNR'], fkData),
+ (char) => convertXMLToDBData<MatlCharAsgnData>(char as Record<string, string | undefined>, 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<MatlData>(
- matl as Record<string, string | undefined>,
- ['MATNR']
+ matl as Record<string, string | undefined>,
+ fkData
);
- // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨)
- if (!material.MATNR) {
- material.MATNR = '';
- }
-
- // FK 데이터 준비
- const fkData = { MATNR: matl.MATNR || '' };
-
- // Description 데이터 변환 (자동)
+ // 2단계: 하위 테이블들 (FK: MATNR)
const descriptions = processNestedArray(
matl.DESC,
- (desc) => convertXMLToDBData<MatlDescData>(desc, ['MATNR'], fkData),
+ (desc) => convertXMLToDBData<MatlDescData>(desc as Record<string, string | undefined>, fkData),
fkData
);
- // Plant 데이터 변환 (자동)
const plants = processNestedArray(
matl.PLNT,
- (plnt) => convertXMLToDBData<MatlPlntData>(plnt, ['MATNR'], fkData),
+ (plnt) => convertXMLToDBData<MatlPlntData>(plnt as Record<string, string | undefined>, fkData),
fkData
);
- // Unit 데이터 변환 (자동)
const units = processNestedArray(
matl.UNIT,
- (unit) => convertXMLToDBData<MatlUnitData>(unit, ['MATNR'], fkData),
+ (unit) => convertXMLToDBData<MatlUnitData>(unit as Record<string, string | undefined>, fkData),
fkData
);
- // Class Assignment 데이터 변환 (자동)
const classAssignments = processNestedArray(
matl.CLASSASGN,
- (cls) => convertXMLToDBData<MatlClassAsgnData>(cls, ['MATNR'], fkData),
+ (cls) => convertXMLToDBData<MatlClassAsgnData>(cls as Record<string, string | undefined>, fkData),
fkData
);
- // Characteristic Assignment 데이터 변환 (자동)
const characteristicAssignments = processNestedArray(
matl.CHARASGN,
- (char) => convertXMLToDBData<MatlCharAsgnData>(char, ['MATNR'], fkData),
+ (char) => convertXMLToDBData<MatlCharAsgnData>(char as Record<string, string | undefined>, 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<MatlData>(
- matl as Record<string, string | undefined>,
- ['MATNR']
+ matl as Record<string, string | undefined>,
+ fkData
);
- // 필수 필드 보정 (MATNR이 빈 문자열이면 안됨)
- if (!material.MATNR) {
- material.MATNR = '';
- }
-
- // FK 데이터 준비
- const fkData = { MATNR: matl.MATNR || '' };
-
- // Description 데이터 변환 (자동)
+ // 2단계: 하위 테이블들 (FK: MATNR)
const descriptions = processNestedArray(
matl.DESC,
- (desc) => convertXMLToDBData<MatlDescData>(desc, ['MATNR'], fkData),
+ (desc) => convertXMLToDBData<MatlDescData>(desc as Record<string, string | undefined>, fkData),
fkData
);
- // Plant 데이터 변환 (자동)
const plants = processNestedArray(
matl.PLNT,
- (plnt) => convertXMLToDBData<MatlPlntData>(plnt, ['MATNR'], fkData),
+ (plnt) => convertXMLToDBData<MatlPlntData>(plnt as Record<string, string | undefined>, fkData),
fkData
);
- // Unit 데이터 변환 (자동)
const units = processNestedArray(
matl.UNIT,
- (unit) => convertXMLToDBData<MatlUnitData>(unit, ['MATNR'], fkData),
+ (unit) => convertXMLToDBData<MatlUnitData>(unit as Record<string, string | undefined>, fkData),
fkData
);
- // Class Assignment 데이터 변환 (자동)
const classAssignments = processNestedArray(
matl.CLASSASGN,
- (cls) => convertXMLToDBData<MatlClassAsgnData>(cls, ['MATNR'], fkData),
+ (cls) => convertXMLToDBData<MatlClassAsgnData>(cls as Record<string, string | undefined>, fkData),
fkData
);
- // Characteristic Assignment 데이터 변환 (자동)
const characteristicAssignments = processNestedArray(
matl.CHARASGN,
- (char) => convertXMLToDBData<MatlCharAsgnData>(char, ['MATNR'], fkData),
+ (char) => convertXMLToDBData<MatlCharAsgnData>(char as Record<string, string | undefined>, 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<CctrData>(
- item as Record<string, string | undefined>,
- ['CCTR', 'KOKRS', 'DATBI']
+ item as Record<string, string | undefined>,
+ fkData
);
- const fkData = { CCTR: item.CCTR || '' };
const texts = processNestedArray(
item.TEXT,
- (text) => convertXMLToDBData<CctrTextData>(text, [], fkData),
+ (text) => convertXMLToDBData<CctrTextData>(text as Record<string, string | undefined>, 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<PctrData>(
- item as Record<string, string | undefined>,
- ['PCTR', 'KOKRS', 'DATBI']
+ item as Record<string, string | undefined>,
+ fkData
);
- const fkData = { CCTR: item.PCTR || '' }; // TEXT 테이블은 CCTR 필드를 사용
const texts = processNestedArray(
item.TEXT,
- (text) => convertXMLToDBData<CctrTextData>(text, [], fkData),
+ (text) => convertXMLToDBData<CctrTextData>(text as Record<string, string | undefined>, 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<ZbukrsData>(
- item as Record<string, string | undefined>,
- ['ZBUKRS']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZEKGRP 처리
+ // HRHMTB_ZEKGRP 처리 (unique 필드: ZEKGRP)
if (requestData.items4 && Array.isArray(requestData.items4)) {
result.zekgrpItems = requestData.items4.map((item: ZekgrpXML) =>
convertXMLToDBData<ZekgrpData>(
- item as Record<string, string | undefined>,
- ['ZEKGRP']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZEKORG 처리
+ // HRHMTB_ZEKORG 처리 (unique 필드: ZEKORG)
if (requestData.items5 && Array.isArray(requestData.items5)) {
result.zekorgItems = requestData.items5.map((item: ZekorgXML) =>
convertXMLToDBData<ZekorgData>(
- item as Record<string, string | undefined>,
- ['ZEKORG']
+ item as Record<string, string | undefined>
)
);
}
- // 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<ZgsberData>(
- item as Record<string, string | undefined>,
- ['ZGSBER']
+ item as Record<string, string | undefined>,
+ fkData
);
- const fkData = { ZGSBER: item.ZGSBER || '' };
const texts = processNestedArray(
item.TEXT,
- (text) => convertXMLToDBData<ZgsberTextData>(text, ['LANGU'], fkData),
+ (text) => convertXMLToDBData<ZgsberTextData>(text as Record<string, string | undefined>, 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<ZlgortData>(
- item as Record<string, string | undefined>,
- ['ZLGORT', 'ZWERKS']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZSPART 처리
+ // HRHMTB_ZSPART 처리 (unique 필드: ZSPART)
if (requestData.items8 && Array.isArray(requestData.items8)) {
result.zspartItems = requestData.items8.map((item: ZspartXML) =>
convertXMLToDBData<ZspartData>(
- item as Record<string, string | undefined>,
- ['ZSPART']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZVKBUR 처리
+ // HRHMTB_ZVKBUR 처리 (unique 필드: ZVKBUR)
if (requestData.items9 && Array.isArray(requestData.items9)) {
result.zvkburItems = requestData.items9.map((item: ZvkburXML) =>
convertXMLToDBData<ZvkburData>(
- item as Record<string, string | undefined>,
- ['ZVKBUR']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZVKGRP 처리
+ // HRHMTB_ZVKGRP 처리 (unique 필드: ZVKGRP)
if (requestData.items10 && Array.isArray(requestData.items10)) {
result.zvkgrpItems = requestData.items10.map((item: ZvkgrpXML) =>
convertXMLToDBData<ZvkgrpData>(
- item as Record<string, string | undefined>,
- ['ZVKGRP']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZVKORG 처리
+ // HRHMTB_ZVKORG 처리 (unique 필드: ZVKORG)
if (requestData.items11 && Array.isArray(requestData.items11)) {
result.zvkorgItems = requestData.items11.map((item: ZvkorgXML) =>
convertXMLToDBData<ZvkorgData>(
- item as Record<string, string | undefined>,
- ['ZVKORG']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZVSTEL 처리
+ // HRHMTB_ZVSTEL 처리 (unique 필드: ZVSTEL)
if (requestData.items12 && Array.isArray(requestData.items12)) {
result.zvstelItems = requestData.items12.map((item: ZvstelXML) =>
convertXMLToDBData<ZvstelData>(
- item as Record<string, string | undefined>,
- ['ZVSTEL']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZVTWEG 처리
+ // HRHMTB_ZVTWEG 처리 (unique 필드: ZVTWEG)
if (requestData.items13 && Array.isArray(requestData.items13)) {
result.zvtwegItems = requestData.items13.map((item: ZvtwegXML) =>
convertXMLToDBData<ZvtwegData>(
- item as Record<string, string | undefined>,
- ['ZVTWEG']
+ item as Record<string, string | undefined>
)
);
}
- // HRHMTB_ZWERKS 처리
+ // HRHMTB_ZWERKS 처리 (unique 필드: ZWERKS)
if (requestData.items14 && Array.isArray(requestData.items14)) {
result.zwerksItems = requestData.items14.map((item: ZwerksXML) =>
convertXMLToDBData<ZwerksData>(
- item as Record<string, string | undefined>,
- ['ZWERKS']
+ item as Record<string, string | undefined>
)
);
}
@@ -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<VendorHeaderData>(
vendorHeader as Record<string, string | undefined>,
- ['VNDRCD'],
fkData
);
- // 2단계: ADDRESS와 직속 하위들
+ // 2단계: 직속 하위 테이블들 (FK: VNDRCD)
const addresses = processNestedArray(
vendorHeader.ADDRESS,
- (addr) => convertXMLToDBData<AddressData>(addr as Record<string, string | undefined>, ['ADR_NO'], fkData),
+ (addr) => convertXMLToDBData<AddressData>(addr as Record<string, string | undefined>, fkData),
fkData
);
- // ADDRESS의 하위 테이블들 (3단계)
+ const bpTaxnums = processNestedArray(
+ vendorHeader.BP_TAXNUM,
+ (item) => convertXMLToDBData<BpTaxnumData>(item as Record<string, string | undefined>, fkData),
+ fkData
+ );
+
+ const bpVengens = processNestedArray(
+ vendorHeader.BP_VENGEN,
+ (vengen) => convertXMLToDBData<BpVengenData>(vengen as Record<string, string | undefined>, fkData),
+ fkData
+ );
+
+ // 3단계: ADDRESS의 하위 테이블들 (FK: VNDRCD)
const adEmails = vendorHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_EMAIL, (item) =>
- convertXMLToDBData<AdEmailData>(item as Record<string, string | undefined>, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData)
+ convertXMLToDBData<AdEmailData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const adFaxes = vendorHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_FAX, (item) =>
- convertXMLToDBData<AdFaxData>(item as Record<string, string | undefined>, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData)
+ convertXMLToDBData<AdFaxData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const adPostals = vendorHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_POSTAL, (item) =>
- convertXMLToDBData<AdPostalData>(item as Record<string, string | undefined>, ['INTL_ADR_VER_ID'], fkData), fkData)
+ convertXMLToDBData<AdPostalData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const adTels = vendorHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_TEL, (item) =>
- convertXMLToDBData<AdTelData>(item as Record<string, string | undefined>, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData)
+ convertXMLToDBData<AdTelData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const adUrls = vendorHeader.ADDRESS?.flatMap(addr =>
processNestedArray(addr.AD_URL, (item) =>
- convertXMLToDBData<AdUrlData>(item as Record<string, string | undefined>, ['REPR_SER', 'VLD_ST_DT'], fkData), fkData)
+ convertXMLToDBData<AdUrlData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
- // 2단계: BP_TAXNUM
- const bpTaxnums = processNestedArray(
- vendorHeader.BP_TAXNUM,
- (item) => convertXMLToDBData<BpTaxnumData>(item as Record<string, string | undefined>, ['TX_NO_CTG'], fkData),
- fkData
- );
-
- // 2단계: BP_VENGEN과 하위들
- const bpVengens = processNestedArray(
- vendorHeader.BP_VENGEN,
- (vengen) => convertXMLToDBData<BpVengenData>(vengen as Record<string, string | undefined>, ['VNDRNO'], fkData),
- fkData
- );
-
- // BP_VENGEN의 하위 테이블들 (3단계)
+ // 3단계: BP_VENGEN의 하위 테이블들 (FK: VNDRCD)
const bpCompnies = vendorHeader.BP_VENGEN?.flatMap(vengen =>
processNestedArray(vengen.BP_COMPNY, (item) =>
- convertXMLToDBData<BpCompnyData>(item as Record<string, string | undefined>, ['CO_CD'], fkData), fkData)
+ convertXMLToDBData<BpCompnyData>(item as Record<string, string | undefined>, fkData), fkData)
) || [];
const bpPorgs = vendorHeader.BP_VENGEN?.flatMap(vengen =>
processNestedArray(vengen.BP_PORG, (item) =>
- convertXMLToDBData<BpPorgData>(item as Record<string, string | undefined>, ['PUR_ORG_CD'], fkData), fkData)
+ convertXMLToDBData<BpPorgData>(item as Record<string, string | undefined>, 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<BpWhtaxData>(item as Record<string, string | undefined>, ['SRCE_TX_TP'], fkData), fkData)
+ convertXMLToDBData<BpWhtaxData>(item as Record<string, string | undefined>, fkData), fkData)
) || []
) || [];
- // BP_PORG의 하위 테이블 (4단계)
const zvpfns = vendorHeader.BP_VENGEN?.flatMap(vengen =>
vengen.BP_PORG?.flatMap(porg =>
processNestedArray(porg.ZVPFN, (item) =>
- convertXMLToDBData<ZvpfnData>(item as Record<string, string | undefined>, ['PTNR_SKL', 'PTNR_CNT'], fkData), fkData)
+ convertXMLToDBData<ZvpfnData>(item as Record<string, string | undefined>, 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<string, unknown>,
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<string, unknown> | 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<T extends Record<string, unknown>>(
xmlData: Record<string, string | undefined>,
- requiredFields: (keyof T)[] = [],
fkData?: Record<string, string>
): T {
const result = {} as T;
@@ -141,20 +162,35 @@ export function convertXMLToDBData<T extends Record<string, unknown>>(
}
}
- // 필수 필드 처리 (FK 등)
- for (const field of requiredFields) {
- if (!result[field] && fkData) {
- const fieldStr = String(field);
- if (fkData[fieldStr]) {
- (result as Record<string, unknown>)[field] = fkData[fieldStr];
+ // FK 필드 처리 (XML 우선, 없으면 상위에서 전달받은 값 사용)
+ if (fkData) {
+ for (const [key, value] of Object.entries(fkData)) {
+ // XML에 해당 FK 필드가 없거나 비어있는 경우에만 상위 값 사용
+ const existingValue = (result as Record<string, unknown>)[key];
+ if (!existingValue || existingValue === null || existingValue === '') {
+ (result as Record<string, unknown>)[key] = value;
}
+ // XML에 이미 FK 필드가 있고 값이 있는 경우는 XML 값을 그대로 사용
}
}
return result;
}
-// 중첩 배열 처리 함수
+// 중첩 배열 처리 함수 (개선된 버전)
+/**
+ * 중첩된 배열 데이터를 처리하여 DB 삽입 가능한 형태로 변환
+ *
+ * 처리 방식:
+ * - 하위 테이블 데이터는 FK만 설정하면 됨
+ * - 별도의 필수 필드 생성 로직 불필요
+ * - 전체 데이터셋 기반으로 삭제 후 재삽입 처리
+ *
+ * @param items 처리할 배열 데이터
+ * @param converter 변환 함수
+ * @param fkData FK 데이터
+ * @returns 변환된 배열 데이터
+ */
export function processNestedArray<T, U>(
items: T[] | undefined,
converter: (item: T, fkData?: Record<string, string>) => U,
@@ -207,9 +243,27 @@ export function createSuccessResponse(namespace: string): NextResponse {
}
// 하위 테이블 처리: FK 기준으로 전체 삭제 후 재삽입
-export async function replaceSubTableData<T>(
- 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<T extends Record<string, unknown>>(
+ tx: Parameters<Parameters<typeof db.transaction>[0]>[0],
+ table: any, // Drizzle 테이블 객체 - 복잡한 제네릭 타입으로 인해 any 사용
data: T[],
parentField: string,
parentValue: string