diff options
| author | joonhoekim <26rote@gmail.com> | 2025-07-31 09:34:29 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-07-31 09:34:29 +0000 |
| commit | 0728ce2e0c085b8f1e8699bcdbe3d2000208bc74 (patch) | |
| tree | 3b6299d082314d55065e16a1cf09a0abf2118088 /app/api/(S-ERP) | |
| parent | 10f90dc68dec42e9a64e081cc0dce6a484447290 (diff) | |
(김준회) MDG 쿼리 배치처리 도입 (네트워크 왕복 오버헤드 해결 목적), 마이그레이션간 DELETE 제거, 환경변수 정리
Diffstat (limited to 'app/api/(S-ERP)')
12 files changed, 375 insertions, 404 deletions
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts index 3b7636f9..31b61ffc 100644 --- a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts +++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts @@ -13,7 +13,7 @@ import { createSoapResponse, replaceSubTableData, withSoapLogging, -} from '@/lib/soap/mdg/utils'; +} from '@/lib/soap/utils'; import { PR_INFORMATION_T_BID_HEADER, 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 9d08527b..0cedcade 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 @@ -25,9 +25,12 @@ import { processNestedArray, createErrorResponse, createSuccessResponse, - replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 type BpHeaderData = typeof CUSTOMER_MASTER_BP_HEADER.$inferInsert; @@ -316,54 +319,53 @@ function transformCustomerData(bpHeaderData: BpHeaderXML[]): ProcessedCustomerDa * @param processedCustomers 변환된 CUSTOMER 데이터 배열 */ async function saveToDatabase(processedCustomers: ProcessedCustomerData[]) { - console.log(`데이터베이스 저장 시작: ${processedCustomers.length}개 고객 데이터`); - + console.log(`데이터베이스(배치) 저장 시작: ${processedCustomers.length}개 고객 데이터`); try { await db.transaction(async (tx) => { - for (const customerData of processedCustomers) { - const { bpHeader, addresses, adEmails, adFaxes, adPostals, adTels, adUrls, - bpCusgens, zvatregs, ztaxinds, zcompanies, zsales, zcpfns, bpTaxnums } = customerData; - - if (!bpHeader.BP_HEADER) { - console.warn('BP_HEADER가 없는 항목 발견, 건너뜁니다.'); - continue; - } + // 1) 부모 테이블 데이터 준비 + const bpHeaderRows = processedCustomers + .map((c) => c.bpHeader) + .filter((h): h is BpHeaderData => !!h.BP_HEADER); - // 1. BP_HEADER 테이블 Upsert (최상위 테이블 - unique 필드: BP_HEADER) - await tx.insert(CUSTOMER_MASTER_BP_HEADER) - .values(bpHeader) - .onConflictDoUpdate({ - target: CUSTOMER_MASTER_BP_HEADER.BP_HEADER, - set: { - ...bpHeader, - updatedAt: new Date(), - } - }); + const bpHeaderKeys = bpHeaderRows.map((h) => h.BP_HEADER as string); - // 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단계 테이블들 - 동일하게 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), - replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_TEL, adTels, 'BP_HEADER', bpHeader.BP_HEADER), - replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_URL, adUrls, 'BP_HEADER', bpHeader.BP_HEADER), - replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZVATREG, zvatregs, 'BP_HEADER', bpHeader.BP_HEADER), - replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZTAXIND, ztaxinds, 'BP_HEADER', bpHeader.BP_HEADER), - replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZCOMPANY, zcompanies, 'BP_HEADER', bpHeader.BP_HEADER), - replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES, zsales, 'BP_HEADER', bpHeader.BP_HEADER), - replaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES_ZCPFN, zcpfns, 'BP_HEADER', bpHeader.BP_HEADER), - ]); - } + // 2) 하위 테이블 데이터 평탄화 + const addresses = processedCustomers.flatMap((c) => c.addresses); + const adEmails = processedCustomers.flatMap((c) => c.adEmails); + const adFaxes = processedCustomers.flatMap((c) => c.adFaxes); + const adPostals = processedCustomers.flatMap((c) => c.adPostals); + const adTels = processedCustomers.flatMap((c) => c.adTels); + const adUrls = processedCustomers.flatMap((c) => c.adUrls); + const bpCusgens = processedCustomers.flatMap((c) => c.bpCusgens); + const zvatregs = processedCustomers.flatMap((c) => c.zvatregs); + const ztaxinds = processedCustomers.flatMap((c) => c.ztaxinds); + const zcompanies = processedCustomers.flatMap((c) => c.zcompanies); + const zsales = processedCustomers.flatMap((c) => c.zsales); + const zcpfns = processedCustomers.flatMap((c) => c.zcpfns); + const bpTaxnums = processedCustomers.flatMap((c) => c.bpTaxnums); + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, CUSTOMER_MASTER_BP_HEADER, bpHeaderRows, 'BP_HEADER'); + + // 4) 하위 테이블 교체 (배치) + await Promise.all([ + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS, addresses, CUSTOMER_MASTER_BP_HEADER_ADDRESS.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, adEmails, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_EMAIL.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_FAX, adFaxes, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_FAX.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, adPostals, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_POSTAL.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_TEL, adTels, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_TEL.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_URL, adUrls, CUSTOMER_MASTER_BP_HEADER_ADDRESS_AD_URL.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN, bpCusgens, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZVATREG, zvatregs, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZVATREG.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZTAXIND, ztaxinds, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZTAXIND.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZCOMPANY, zcompanies, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZCOMPANY.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES, zsales, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES_ZCPFN, zcpfns, CUSTOMER_MASTER_BP_HEADER_BP_CUSGEN_ZSALES_ZCPFN.BP_HEADER, bpHeaderKeys), + bulkReplaceSubTableData(tx, CUSTOMER_MASTER_BP_HEADER_BP_TAXNUM, bpTaxnums, CUSTOMER_MASTER_BP_HEADER_BP_TAXNUM.BP_HEADER, bpHeaderKeys), + ]); }); - console.log(`데이터베이스 저장 완료: ${processedCustomers.length}개 고객`); + console.log(`데이터베이스(배치) 저장 완료: ${processedCustomers.length}개 고객`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', 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 28757fb5..fb54dff3 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 @@ -16,9 +16,12 @@ import { processNestedArray, createErrorResponse, createSuccessResponse, - replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 type DeptData = typeof DEPARTMENT_CODE_CMCTB_DEPT_MDG.$inferInsert; @@ -187,40 +190,29 @@ function transformDepartmentData(deptData: DeptXML[]): ProcessedDepartmentData[] * @param processedDepts 변환된 DEPARTMENT 데이터 배열 */ async function saveToDatabase(processedDepts: ProcessedDepartmentData[]) { - console.log(`데이터베이스 저장 시작: ${processedDepts.length}개 부서 데이터`); - + console.log(`데이터베이스(배치) 저장 시작: ${processedDepts.length}개 부서 데이터`); try { await db.transaction(async (tx) => { - for (const deptData of processedDepts) { - const { dept, deptnms, compnms, corpnms } = deptData; - - if (!dept.DEPTCD) { - console.warn('부서코드(DEPTCD)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + const deptRows = processedDepts + .map((d) => d.dept) + .filter((d): d is DeptData => !!d.DEPTCD); - // 1. Department 테이블 Upsert (최상위 테이블 - unique 필드: DEPTCD) - await tx.insert(DEPARTMENT_CODE_CMCTB_DEPT_MDG) - .values(dept) - .onConflictDoUpdate({ - target: DEPARTMENT_CODE_CMCTB_DEPT_MDG.DEPTCD, - set: { - ...dept, - updatedAt: new Date(), - } - }); - - // 2. 하위 테이블들 처리 - FK(DEPTCD) 기준 전체 삭제 후 재삽입 - // 전체 데이터셋 기반 처리로 데이터 일관성 확보 - await Promise.all([ - 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) - ]); - } + const deptcds = deptRows.map((d) => d.DEPTCD as string); + + const deptnms = processedDepts.flatMap((d) => d.deptnms); + const compnms = processedDepts.flatMap((d) => d.compnms); + const corpnms = processedDepts.flatMap((d) => d.corpnms); + + await bulkUpsert(tx, DEPARTMENT_CODE_CMCTB_DEPT_MDG, deptRows, 'DEPTCD'); + + await Promise.all([ + bulkReplaceSubTableData(tx, DEPARTMENT_CODE_CMCTB_DEPT_MDG_DEPTNM, deptnms, DEPARTMENT_CODE_CMCTB_DEPT_MDG_DEPTNM.DEPTCD, deptcds), + bulkReplaceSubTableData(tx, DEPARTMENT_CODE_CMCTB_DEPT_MDG_COMPNM, compnms, DEPARTMENT_CODE_CMCTB_DEPT_MDG_COMPNM.DEPTCD, deptcds), + bulkReplaceSubTableData(tx, DEPARTMENT_CODE_CMCTB_DEPT_MDG_CORPNM, corpnms, DEPARTMENT_CODE_CMCTB_DEPT_MDG_CORPNM.DEPTCD, deptcds), + ]); }); - 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 fc6bc71f..388c4dc4 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 @@ -41,7 +41,12 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; + // 스키마에서 직접 타입 추론 type EmpMdgData = typeof EMPLOYEE_MASTER_CMCTB_EMP_MDG.$inferInsert; @@ -310,65 +315,76 @@ function transformEmployeeData(empData: EmpMdgXML[]): ProcessedEmployeeData[] { * @param processedEmployees 변환된 EMPLOYEE 데이터 배열 */ async function saveToDatabase(processedEmployees: ProcessedEmployeeData[]) { - console.log(`데이터베이스 저장 시작: ${processedEmployees.length}개 사원 데이터`); + console.log(`데이터베이스(배치) 저장 시작: ${processedEmployees.length}개 사원 데이터`); try { await db.transaction(async (tx) => { - for (const employeeData of processedEmployees) { - const { employee, banm, binm, compnm, corpnm, countrynm, deptcode, deptcodePccdnm, - deptnm, dhjobgdnm, gjobdutynm, gjobgrdnm, gjobgrdtype, gjobnm, gnnm, - jobdutynm, jobgrdnm, jobnm, ktlnm, oktlnm, orgbicdnm, orgcompnm, - orgcorpnm, orgdeptnm, orgpdepnm, pdeptnm } = employeeData; - - if (!employee.EMPID) { - console.warn('사원번호(EMPID)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + // 1) 부모 테이블 데이터 준비 + const employeeRows = processedEmployees + .map((e) => e.employee) + .filter((e): e is EmpMdgData => !!e.EMPID); - // 1. CMCTB_EMP_MDG 테이블 Upsert (최상위 테이블 - unique 필드: EMPID) - await tx.insert(EMPLOYEE_MASTER_CMCTB_EMP_MDG) - .values(employee) - .onConflictDoUpdate({ - target: EMPLOYEE_MASTER_CMCTB_EMP_MDG.EMPID, - set: { - ...employee, - updatedAt: new Date(), - } - }); + const empids = employeeRows.map((e) => e.EMPID as string); - // 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), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_COMPNM, compnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_CORPNM, corpnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_COUNTRYNM, countrynm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE, deptcode, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE_PCCDNM, deptcodePccdnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTNM, deptnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DHJOBGDNM, dhjobgdnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBDUTYNM, gjobdutynm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDNM, gjobgrdnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDTYPE, gjobgrdtype, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBNM, gjobnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GNNM, gnnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBDUTYNM, jobdutynm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBGRDNM, jobgrdnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBNM, jobnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_KTLNM, ktlnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_OKTLNM, oktlnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGBICDNM, orgbicdnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCOMPNM, orgcompnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCORPNM, orgcorpnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGDEPTNM, orgdeptnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGPDEPNM, orgpdepnm, 'EMPID', employee.EMPID), - replaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_PDEPTNM, pdeptnm, 'EMPID', employee.EMPID) - ]); - } + // 2) 하위 테이블 데이터 평탄화 + const banm = processedEmployees.flatMap((e) => e.banm); + const binm = processedEmployees.flatMap((e) => e.binm); + const compnm = processedEmployees.flatMap((e) => e.compnm); + const corpnm = processedEmployees.flatMap((e) => e.corpnm); + const countrynm = processedEmployees.flatMap((e) => e.countrynm); + const deptcode = processedEmployees.flatMap((e) => e.deptcode); + const deptcodePccdnm = processedEmployees.flatMap((e) => e.deptcodePccdnm); + const deptnm = processedEmployees.flatMap((e) => e.deptnm); + const dhjobgdnm = processedEmployees.flatMap((e) => e.dhjobgdnm); + const gjobdutynm = processedEmployees.flatMap((e) => e.gjobdutynm); + const gjobgrdnm = processedEmployees.flatMap((e) => e.gjobgrdnm); + const gjobgrdtype = processedEmployees.flatMap((e) => e.gjobgrdtype); + const gjobnm = processedEmployees.flatMap((e) => e.gjobnm); + const gnnm = processedEmployees.flatMap((e) => e.gnnm); + const jobdutynm = processedEmployees.flatMap((e) => e.jobdutynm); + const jobgrdnm = processedEmployees.flatMap((e) => e.jobgrdnm); + const jobnm = processedEmployees.flatMap((e) => e.jobnm); + const ktlnm = processedEmployees.flatMap((e) => e.ktlnm); + const oktlnm = processedEmployees.flatMap((e) => e.oktlnm); + const orgbicdnm = processedEmployees.flatMap((e) => e.orgbicdnm); + const orgcompnm = processedEmployees.flatMap((e) => e.orgcompnm); + const orgcorpnm = processedEmployees.flatMap((e) => e.orgcorpnm); + const orgdeptnm = processedEmployees.flatMap((e) => e.orgdeptnm); + const orgpdepnm = processedEmployees.flatMap((e) => e.orgpdepnm); + const pdeptnm = processedEmployees.flatMap((e) => e.pdeptnm); + + // 3) 데이터베이스 저장 + await bulkUpsert(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG, employeeRows, 'EMPID'); + + // 4) 하위 테이블 데이터 저장 + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_BANM, banm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_BINM, binm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_COMPNM, compnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_CORPNM, corpnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_COUNTRYNM, countrynm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE, deptcode, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTCODE_PCCDNM, deptcodePccdnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DEPTNM, deptnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_DHJOBGDNM, dhjobgdnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBDUTYNM, gjobdutynm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDNM, gjobgrdnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBGRDTYPE, gjobgrdtype, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GJOBNM, gjobnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_GNNM, gnnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBDUTYNM, jobdutynm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBGRDNM, jobgrdnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_JOBNM, jobnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_KTLNM, ktlnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_OKTLNM, oktlnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGBICDNM, orgbicdnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCOMPNM, orgcompnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGCORPNM, orgcorpnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGDEPTNM, orgdeptnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_ORGPDEPNM, orgpdepnm, 'EMPID', empids); + await bulkReplaceSubTableData(tx, EMPLOYEE_MASTER_CMCTB_EMP_MDG_PDEPTNM, pdeptnm, 'EMPID', empids); }); - console.log(`데이터베이스 저장 완료: ${processedEmployees.length}개 사원`); + console.log(`데이터베이스(배치) 저장 완료: ${processedEmployees.length}개 사원`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); 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 22f151b3..563696d3 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 @@ -16,7 +16,11 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 type EmpRefData = typeof EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF.$inferInsert; @@ -160,33 +164,25 @@ function transformEmpRefData(empRefData: EmpRefXML[]): ProcessedEmployeeReferenc * @param processedEmpRefs 변환된 EMPLOYEE_REFERENCE 데이터 배열 */ async function saveToDatabase(processedEmpRefs: ProcessedEmployeeReferenceData[]) { - console.log(`데이터베이스 저장 시작: ${processedEmpRefs.length}개 직원 참조 데이터`); + console.log(`데이터베이스(배치) 저장 시작: ${processedEmpRefs.length}개 직원 참조 데이터`); try { await db.transaction(async (tx) => { - for (const empRefData of processedEmpRefs) { - const { empRef, names } = empRefData; - - if (!empRef.GRPCD) { - console.warn('그룹코드(GRPCD)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + // 1) 부모 테이블 데이터 준비 + const empRefRows = processedEmpRefs + .map((e) => e.empRef) + .filter((e): e is EmpRefData => !!e.GRPCD); - // 1. Employee Reference 테이블 Upsert (최상위 테이블 - unique 필드: GRPCD) - await tx.insert(EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF) - .values(empRef) - .onConflictDoUpdate({ - target: EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF.GRPCD, - set: { - ...empRef, - updatedAt: new Date(), - } - }); - - // 2. 하위 테이블 처리 - FK(GRPCD) 기준 전체 삭제 후 재삽입 - // 전체 데이터셋 기반 처리로 데이터 일관성 확보 - await replaceSubTableData(tx, EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF_NAME, names, 'GRPCD', empRef.GRPCD); - } + const grpcds = empRefRows.map((e) => e.GRPCD as string); + + // 2) 하위 테이블 데이터 평탄화 + const names = processedEmpRefs.flatMap((e) => e.names); + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF, empRefRows, 'GRPCD'); + + // 4) 하위 테이블 교체 (배치) + await bulkReplaceSubTableData(tx, EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF_NAME, names, EMPLOYEE_REFERENCE_MASTER_CMCTB_EMP_REF_MDG_IF_NAME.GRPCD, grpcds); }); console.log(`데이터베이스 저장 완료: ${processedEmpRefs.length}개 직원 참조`); 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 cd1005e7..5544dfdb 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 @@ -18,9 +18,12 @@ import { processNestedArray, createErrorResponse, createSuccessResponse, - replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 (Insert와 XML을 통합) type MatlData = typeof EQUP_MASTER_MATL.$inferInsert; @@ -212,42 +215,37 @@ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { * @param processedMaterials 변환된 EQUP 데이터 배열 */ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { - console.log(`데이터베이스 저장 시작: ${processedMaterials.length}개 장비 데이터`); - + console.log(`데이터베이스(배치) 저장 시작: ${processedMaterials.length}개 장비 데이터`); try { await db.transaction(async (tx) => { - for (const materialData of processedMaterials) { - const { material, descriptions, plants, units, classAssignments, characteristicAssignments } = materialData; - - if (!material.MATNR) { - console.warn('자재번호(MATNR)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + // 1) 부모 테이블 데이터 준비 + const materialRows = processedMaterials + .map((m) => m.material) + .filter((m): m is MatlData => !!m.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(), - } - }); + const matnrs = materialRows.map((m) => m.MATNR as string); - // 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) - ]); - } + // 2) 하위 테이블 데이터 평탄화 + const descriptions = processedMaterials.flatMap((m) => m.descriptions); + const plants = processedMaterials.flatMap((m) => m.plants); + const units = processedMaterials.flatMap((m) => m.units); + const classAssignments = processedMaterials.flatMap((m) => m.classAssignments); + const characteristicAssignments = processedMaterials.flatMap((m) => m.characteristicAssignments); + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, EQUP_MASTER_MATL, materialRows, 'MATNR'); + + // 4) 하위 테이블 교체 (배치) + await Promise.all([ + bulkReplaceSubTableData(tx, EQUP_MASTER_MATL_DESC, descriptions, EQUP_MASTER_MATL_DESC.MATNR, matnrs), + bulkReplaceSubTableData(tx, EQUP_MASTER_MATL_PLNT, plants, EQUP_MASTER_MATL_PLNT.MATNR, matnrs), + bulkReplaceSubTableData(tx, EQUP_MASTER_MATL_UNIT, units, EQUP_MASTER_MATL_UNIT.MATNR, matnrs), + bulkReplaceSubTableData(tx, EQUP_MASTER_MATL_CLASSASGN, classAssignments, EQUP_MASTER_MATL_CLASSASGN.MATNR, matnrs), + bulkReplaceSubTableData(tx, EQUP_MASTER_MATL_CHARASGN, characteristicAssignments, EQUP_MASTER_MATL_CHARASGN.MATNR, matnrs), + ]); }); - 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_MATERIAL_MASTER_PART/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts index 21063ff7..49aff036 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 @@ -11,7 +11,6 @@ import { import { ToXMLFields, - SoapBodyData, serveWsdl, createXMLParser, extractRequestData, @@ -19,9 +18,16 @@ import { processNestedArray, createErrorResponse, createSuccessResponse, - replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 (Insert와 XML을 통합) type MatlData = typeof MATERIAL_MASTER_PART_MATL.$inferInsert; @@ -213,42 +219,37 @@ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { * @param processedMaterials 변환된 MATERIAL 데이터 배열 */ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { - console.log(`데이터베이스 저장 시작: ${processedMaterials.length}개 자재 데이터`); - + console.log(`데이터베이스(배치) 저장 시작: ${processedMaterials.length}개 자재`); try { await db.transaction(async (tx) => { - for (const materialData of processedMaterials) { - const { material, descriptions, plants, units, classAssignments, characteristicAssignments } = materialData; - - if (!material.MATNR) { - console.warn('자재번호(MATNR)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + // 1) 부모 테이블 데이터 준비 + const materialRows = processedMaterials + .map((m) => m.material) + .filter((m): m is MatlData => !!m.MATNR); - // 1. MATL 테이블 Upsert (최상위 테이블 - unique 필드: MATNR) - await tx.insert(MATERIAL_MASTER_PART_MATL) - .values(material) - .onConflictDoUpdate({ - target: MATERIAL_MASTER_PART_MATL.MATNR, - set: { - ...material, - updatedAt: new Date(), - } - }); + const matnrs = materialRows.map((m) => m.MATNR as string); - // 2. 하위 테이블들 처리 - FK(MATNR) 기준 전체 삭제 후 재삽입 - // 전체 데이터셋 기반 처리로 데이터 일관성 확보 - await Promise.all([ - 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) - ]); - } + // 2) 하위 테이블 데이터 평탄화 + const descriptions = processedMaterials.flatMap((m) => m.descriptions); + const plants = processedMaterials.flatMap((m) => m.plants); + const units = processedMaterials.flatMap((m) => m.units); + const classAssignments = processedMaterials.flatMap((m) => m.classAssignments); + const characteristicAssignments = processedMaterials.flatMap((m) => m.characteristicAssignments); + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, MATERIAL_MASTER_PART_MATL, materialRows, 'MATNR'); + + // 4) 하위 테이블 교체 (배치) + await Promise.all([ + bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_DESC, descriptions, MATERIAL_MASTER_PART_MATL_DESC.MATNR, matnrs), + bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_PLNT, plants, MATERIAL_MASTER_PART_MATL_PLNT.MATNR, matnrs), + bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_UNIT, units, MATERIAL_MASTER_PART_MATL_UNIT.MATNR, matnrs), + bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_CLASSASGN, classAssignments, MATERIAL_MASTER_PART_MATL_CLASSASGN.MATNR, matnrs), + bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_CHARASGN, characteristicAssignments, MATERIAL_MASTER_PART_MATL_CHARASGN.MATNR, matnrs), + ]); }); - 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_MATERIAL_MASTER_PART_RETURN/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts index 428cd298..e2c97b2e 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN/route.ts @@ -10,7 +10,11 @@ import { createSuccessResponse, ToXMLFields, withSoapLogging, -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 type CMCTBMatBseData = typeof MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE.$inferInsert; @@ -116,29 +120,21 @@ function transformMaterialData(materialData: CMCTBMatBseXML[]): ProcessedMateria // 데이터베이스 저장 함수 async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { - console.log(`데이터베이스 저장 시작: ${processedMaterials.length}개 자재 데이터`); + console.log(`데이터베이스(배치) 저장 시작: ${processedMaterials.length}개 자재 데이터`); try { await db.transaction(async (tx) => { - for (const materialData of processedMaterials) { - const { materialData: material } = materialData; - - if (!material.MAT_CD) { - console.warn('자재코드(MAT_CD)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + // 1) 부모 테이블 데이터 준비 + const materialRows = processedMaterials + .map((m) => m.materialData) + .filter((m): m is CMCTBMatBseData => !!m.MAT_CD); - // MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE 테이블 Upsert - await tx.insert(MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE) - .values(material) - .onConflictDoUpdate({ - target: MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE.MAT_CD, - set: { - ...material, - updatedAt: new Date(), - } - }); - } + const matcds = materialRows.map((m) => m.MAT_CD as string); + + // 2) 하위 테이블 데이터 평탄화 (하위 테이블 없음) + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE, materialRows, 'MAT_CD'); }); console.log(`✅ 데이터베이스 저장 완료: ${processedMaterials.length}개 자료`); 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 204dffa3..9d76adbb 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 @@ -17,9 +17,12 @@ import { processNestedArray, createErrorResponse, createSuccessResponse, - replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 (Insert와 XML을 통합) type MatlData = typeof MODEL_MASTER_MATL.$inferInsert; @@ -211,39 +214,35 @@ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { * @param processedMaterials 변환된 MODEL 데이터 배열 */ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { - console.log(`데이터베이스 저장 시작: ${processedMaterials.length}개 모델 데이터`); + console.log(`데이터베이스(배치) 저장 시작: ${processedMaterials.length}개 모델 데이터`); try { await db.transaction(async (tx) => { - for (const materialData of processedMaterials) { - const { material, descriptions, plants, units, classAssignments, characteristicAssignments } = materialData; - - if (!material.MATNR) { - console.warn('자재번호(MATNR)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + // 1) 부모 테이블 데이터 준비 + const materialRows = processedMaterials + .map((m) => m.material) + .filter((m): m is MatlData => !!m.MATNR); - // 1. MATL 테이블 Upsert (최상위 테이블 - unique 필드: MATNR) - await tx.insert(MODEL_MASTER_MATL) - .values(material) - .onConflictDoUpdate({ - target: MODEL_MASTER_MATL.MATNR, - set: { - ...material, - updatedAt: new Date(), - } - }); + const matnrs = materialRows.map((m) => m.MATNR as string); - // 2. 하위 테이블들 처리 - FK(MATNR) 기준 전체 삭제 후 재삽입 - // 전체 데이터셋 기반 처리로 데이터 일관성 확보 - await Promise.all([ - 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) - ]); - } + // 2) 하위 테이블 데이터 평탄화 + const descriptions = processedMaterials.flatMap((m) => m.descriptions); + const plants = processedMaterials.flatMap((m) => m.plants); + const units = processedMaterials.flatMap((m) => m.units); + const classAssignments = processedMaterials.flatMap((m) => m.classAssignments); + const characteristicAssignments = processedMaterials.flatMap((m) => m.characteristicAssignments); + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, MODEL_MASTER_MATL, materialRows, 'MATNR'); + + // 4) 하위 테이블 교체 (배치) + await Promise.all([ + bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_DESC, descriptions, MODEL_MASTER_MATL_DESC.MATNR, matnrs), + bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_PLNT, plants, MODEL_MASTER_MATL_PLNT.MATNR, matnrs), + bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_UNIT, units, MODEL_MASTER_MATL_UNIT.MATNR, matnrs), + bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_CLASSASGN, classAssignments, MODEL_MASTER_MATL_CLASSASGN.MATNR, matnrs), + bulkReplaceSubTableData(tx, MODEL_MASTER_MATL_CHARASGN, characteristicAssignments, MODEL_MASTER_MATL_CHARASGN.MATNR, matnrs) + ]); }); console.log(`데이터베이스 저장 완료: ${processedMaterials.length}개 모델`); 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 987d4002..3051fd8f 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 @@ -30,7 +30,12 @@ import { createSuccessResponse, replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; +import { coerce } from "zod/v4"; // 스키마에서 직접 타입 추론 type CctrData = typeof ORGANIZATION_MASTER_HRHMTB_CCTR.$inferInsert; @@ -376,75 +381,49 @@ async function saveToDatabase(processedOrganizations: ProcessedOrganizationData) try { await db.transaction(async (tx) => { - // CCTR 테이블 처리 (unique 필드: CCTR) - for (const { cctr, texts } of processedOrganizations.cctrItems) { - if (!cctr.CCTR) continue; - - await tx.insert(ORGANIZATION_MASTER_HRHMTB_CCTR) - .values(cctr) - .onConflictDoUpdate({ - target: ORGANIZATION_MASTER_HRHMTB_CCTR.CCTR, - set: { ...cctr, updatedAt: new Date() } - }); - - await replaceSubTableData(tx, ORGANIZATION_MASTER_HRHMTB_CCTR_TEXT, texts, 'CCTR', cctr.CCTR); - } - - // PCTR 테이블 처리 (unique 필드: PCTR) - for (const { pctr, texts } of processedOrganizations.pctrItems) { - if (!pctr.PCTR) continue; - - await tx.insert(ORGANIZATION_MASTER_HRHMTB_PCTR) - .values(pctr) - .onConflictDoUpdate({ - target: ORGANIZATION_MASTER_HRHMTB_PCTR.PCTR, - set: { ...pctr, updatedAt: new Date() } - }); - - // PCTR의 TEXT는 CCTR_TEXT 테이블을 사용하므로 처리하지 않음 - } - // 나머지 단일 테이블들 처리 - const tableProcessors = [ - { items: processedOrganizations.zbukrsItems, table: ORGANIZATION_MASTER_HRHMTB_ZBUKRS, key: 'ZBUKRS' }, - { items: processedOrganizations.zekgrpItems, table: ORGANIZATION_MASTER_HRHMTB_ZEKGRP, key: 'ZEKGRP' }, - { items: processedOrganizations.zekorgItems, table: ORGANIZATION_MASTER_HRHMTB_ZEKORG, key: 'ZEKORG' }, - { items: processedOrganizations.zlgortItems, table: ORGANIZATION_MASTER_HRHMTB_ZLGORT, key: 'ZLGORT' }, - { items: processedOrganizations.zspartItems, table: ORGANIZATION_MASTER_HRHMTB_ZSPART, key: 'ZSPART' }, - { items: processedOrganizations.zvkburItems, table: ORGANIZATION_MASTER_HRHMTB_ZVKBUR, key: 'ZVKBUR' }, - { items: processedOrganizations.zvkgrpItems, table: ORGANIZATION_MASTER_HRHMTB_ZVKGRP, key: 'ZVKGRP' }, - { items: processedOrganizations.zvkorgItems, table: ORGANIZATION_MASTER_HRHMTB_ZVKORG, key: 'ZVKORG' }, - { items: processedOrganizations.zvstelItems, table: ORGANIZATION_MASTER_HRHMTB_ZVSTEL, key: 'ZVSTEL' }, - { items: processedOrganizations.zvtwegItems, table: ORGANIZATION_MASTER_HRHMTB_ZVTWEG, key: 'ZVTWEG' }, - { items: processedOrganizations.zwerksItems, table: ORGANIZATION_MASTER_HRHMTB_ZWERKS, key: 'ZWERKS' } - ]; - - for (const { items, table, key } of tableProcessors) { - for (const item of items) { - if (!(item as any)[key]) continue; - - await tx.insert(table) - .values(item) - .onConflictDoUpdate({ - target: (table as any)[key], - set: { ...item, updatedAt: new Date() } - }); - } - } - - // ZGSBER 테이블 처리 (TEXT 포함) - for (const { zgsber, texts } of processedOrganizations.zgsberItems) { - if (!zgsber.ZGSBER) continue; - - await tx.insert(ORGANIZATION_MASTER_HRHMTB_ZGSBER) - .values(zgsber) - .onConflictDoUpdate({ - target: ORGANIZATION_MASTER_HRHMTB_ZGSBER.ZGSBER, - set: { ...zgsber, updatedAt: new Date() } - }); - - await replaceSubTableData(tx, ORGANIZATION_MASTER_HRHMTB_ZGSBER_TEXT, texts, 'ZGSBER', zgsber.ZGSBER); - } + // 1) 부모 테이블 데이터 준비 (root) + const cctrRows = processedOrganizations.cctrItems.map((c) => c.cctr).filter((c): c is CctrData => !!c.CCTR); + const pctrRows = processedOrganizations.pctrItems; + const zbukrsRows = processedOrganizations.zbukrsItems; + const zekgrpRows = processedOrganizations.zekgrpItems; + const zekorgRows = processedOrganizations.zekorgItems; + const zgsberRows = processedOrganizations.zgsberItems.map((zgsber) => zgsber.zgsber).filter((zgsber): zgsber is ZgsberData => !!zgsber.ZGSBER); + const zlgortRows = processedOrganizations.zlgortItems; + const zspartRows = processedOrganizations.zspartItems; + const zvkburRows = processedOrganizations.zvkburItems; + const zvkgrpRows = processedOrganizations.zvkgrpItems; + const zvkorgRows = processedOrganizations.zvkorgItems; + const zvstelRows = processedOrganizations.zvstelItems; + const zvtwegRows = processedOrganizations.zvtwegItems; + const zwerksRows = processedOrganizations.zwerksItems; + + const cctrIds = cctrRows.map((cctr) => cctr.CCTR as string); + const zgsberIds = zgsberRows.map((zgsber) => zgsber.ZGSBER as string); + + // 2) 하위 테이블 데이터 평탄화 (2건) + const cctrTexts = processedOrganizations.cctrItems.flatMap((cctr) => cctr.texts); + const zgsberTexts = processedOrganizations.zgsberItems.flatMap((zgsber) => zgsber.texts); + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_CCTR, cctrRows, 'CCTR'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_PCTR, pctrRows, 'PCTR'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZBUKRS, zbukrsRows, 'ZBUKRS'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZEKGRP, zekgrpRows, 'ZEKGRP'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZEKORG, zekorgRows, 'ZEKORG'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZGSBER, zgsberRows, 'ZGSBER'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZLGORT, zlgortRows, 'ZLGORT'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZSPART, zspartRows, 'ZSPART'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZVKBUR, zvkburRows, 'ZVKBUR'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZVKGRP, zvkgrpRows, 'ZVKGRP'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZVKORG, zvkorgRows, 'ZVKORG'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZVSTEL, zvstelRows, 'ZVSTEL'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZVTWEG, zvtwegRows, 'ZVTWEG'); + await bulkUpsert(tx, ORGANIZATION_MASTER_HRHMTB_ZWERKS, zwerksRows, 'ZWERKS'); + + // 4) 하위 테이블 교체 (배치) (2건) + await bulkReplaceSubTableData(tx, ORGANIZATION_MASTER_HRHMTB_CCTR_TEXT, cctrTexts, ORGANIZATION_MASTER_HRHMTB_CCTR_TEXT.CCTR, cctrIds); + await bulkReplaceSubTableData(tx, ORGANIZATION_MASTER_HRHMTB_ZGSBER_TEXT, zgsberTexts, ORGANIZATION_MASTER_HRHMTB_ZGSBER_TEXT.ZGSBER, zgsberIds); }); console.log('조직 마스터 데이터 처리 완료.'); diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts index 93071c69..c1563859 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_PROJECT_MASTER/route.ts @@ -13,7 +13,9 @@ import { createErrorResponse, createSuccessResponse, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { bulkUpsert } from "@/lib/soap/batch-utils"; // 서브테이블 없음 + // 스키마에서 직접 타입 추론 type ProjectData = typeof PROJECT_MASTER_CMCTB_PROJ_MAST.$inferInsert; @@ -102,8 +104,7 @@ function transformProjectData(projectData: ProjectXML[]): ProcessedProjectData[] return projectData.map(proj => { // Project 데이터 변환 const project = convertXMLToDBData<ProjectData>( - proj as Record<string, string | undefined>, - ['PROJ_NO'] + proj as Record<string, string | undefined> ); // 필수 필드 보정 @@ -119,32 +120,19 @@ function transformProjectData(projectData: ProjectXML[]): ProcessedProjectData[] // 데이터베이스 저장 함수 async function saveToDatabase(processedProjects: ProcessedProjectData[]) { - console.log(`데이터베이스 저장 함수가 호출됨. ${processedProjects.length}개의 프로젝트 데이터 수신.`); - + console.log(`데이터베이스(배치) 저장 시작: ${processedProjects.length}개 프로젝트`); try { await db.transaction(async (tx) => { - for (const projectData of processedProjects) { - const { project } = projectData; - - if (!project.PROJ_NO) { - console.warn('프로젝트번호(PROJ_NO)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + const projectRows = processedProjects + .map((p) => p.project) + .filter((p): p is ProjectData => !!p.PROJ_NO); - // Project 테이블 Upsert - await tx.insert(PROJECT_MASTER_CMCTB_PROJ_MAST) - .values(project) - .onConflictDoUpdate({ - target: PROJECT_MASTER_CMCTB_PROJ_MAST.PROJ_NO, - set: { - ...project, - updatedAt: new Date(), - } - }); - } + if (!projectRows.length) return; + + await bulkUpsert(tx, PROJECT_MASTER_CMCTB_PROJ_MAST, projectRows, 'PROJ_NO'); }); - console.log(`${processedProjects.length}개의 프로젝트 데이터 처리 완료.`); + console.log(`데이터베이스(배치) 저장 완료: ${processedProjects.length}개 프로젝트`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); 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 75f8cd62..61269937 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 @@ -24,9 +24,12 @@ import { processNestedArray, createErrorResponse, createSuccessResponse, - replaceSubTableData, withSoapLogging -} from "@/lib/soap/mdg/utils"; +} from "@/lib/soap/utils"; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; // 스키마에서 직접 타입 추론 type VendorHeaderData = typeof VENDOR_MASTER_BP_HEADER.$inferInsert; @@ -308,53 +311,54 @@ function transformVendorData(vendorHeaderData: VendorHeaderXML[]): ProcessedVend * @param processedVendors 변환된 VENDOR 데이터 배열 */ async function saveToDatabase(processedVendors: ProcessedVendorData[]) { - console.log(`데이터베이스 저장 시작: ${processedVendors.length}개 벤더 데이터`); - + console.log(`데이터베이스(배치) 저장 시작: ${processedVendors.length}개 벤더 데이터`); try { await db.transaction(async (tx) => { - for (const vendorData of processedVendors) { - const { vendorHeader, addresses, adEmails, adFaxes, adPostals, adTels, adUrls, - bpTaxnums, bpVengens, bpCompnies, bpWhtaxes, bpPorgs, zvpfns } = vendorData; - - if (!vendorHeader.VNDRCD) { - console.warn('벤더코드(VNDRCD)가 없는 항목 발견, 건너뜁니다.'); - continue; - } + // 1) 부모 테이블 데이터 준비 + const vendorHeaderRows = processedVendors + .map((v) => v.vendorHeader) + .filter((v): v is VendorHeaderData => !!v.VNDRCD); - // 1. BP_HEADER 테이블 Upsert (최상위 테이블 - unique 필드: VNDRCD) - await tx.insert(VENDOR_MASTER_BP_HEADER) - .values(vendorHeader) - .onConflictDoUpdate({ - target: VENDOR_MASTER_BP_HEADER.VNDRCD, - set: { - ...vendorHeader, - updatedAt: new Date(), - } - }); + const vndrCds = vendorHeaderRows.map((v) => v.VNDRCD as string); - // 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단계 테이블들 - 동일하게 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), - replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL, adTels, 'VNDRCD', vendorHeader.VNDRCD), - replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL, adUrls, 'VNDRCD', vendorHeader.VNDRCD), - replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY, bpCompnies, 'VNDRCD', vendorHeader.VNDRCD), - replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY_BP_WHTAX, bpWhtaxes, 'VNDRCD', vendorHeader.VNDRCD), - replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG, bpPorgs, 'VNDRCD', vendorHeader.VNDRCD), - replaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG_ZVPFN, zvpfns, 'VNDRCD', vendorHeader.VNDRCD), - ]); - } + // 2) 하위 테이블 데이터 평탄화 + const addresses = processedVendors.flatMap((v) => v.addresses); + const adEmails = processedVendors.flatMap((v) => v.adEmails); + const adFaxes = processedVendors.flatMap((v) => v.adFaxes); + const adPostals = processedVendors.flatMap((v) => v.adPostals); + const adTels = processedVendors.flatMap((v) => v.adTels); + const adUrls = processedVendors.flatMap((v) => v.adUrls); + const bpTaxnums = processedVendors.flatMap((v) => v.bpTaxnums); + const bpVengens = processedVendors.flatMap((v) => v.bpVengens); + const bpCompnies = processedVendors.flatMap((v) => v.bpCompnies); + const bpWhtaxes = processedVendors.flatMap((v) => v.bpWhtaxes); + const bpPorgs = processedVendors.flatMap((v) => v.bpPorgs); + const zvpfns = processedVendors.flatMap((v) => v.zvpfns); + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, VENDOR_MASTER_BP_HEADER, vendorHeaderRows, 'VNDRCD'); + + // 4) 하위 테이블 교체 (배치) + // 정의서에서 하위 테이블 키를 알려주지 않았고, 정시템도 모른다고 하므로 최상위 테이블 PK 기준 전체 삭제 후 삽입 + await Promise.all([ + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS, addresses, VENDOR_MASTER_BP_HEADER_ADDRESS.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_TAXNUM, bpTaxnums, VENDOR_MASTER_BP_HEADER_BP_TAXNUM.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN, bpVengens, VENDOR_MASTER_BP_HEADER_BP_VENGEN.VNDRCD, vndrCds), + + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, adEmails, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_FAX, adFaxes, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_FAX.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, adPostals, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL, adTels, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL, adUrls, VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL.VNDRCD, vndrCds), + + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY, bpCompnies, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY_BP_WHTAX, bpWhtaxes, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_COMPNY_BP_WHTAX.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG, bpPorgs, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG.VNDRCD, vndrCds), + bulkReplaceSubTableData(tx, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG_ZVPFN, zvpfns, VENDOR_MASTER_BP_HEADER_BP_VENGEN_BP_PORG_ZVPFN.VNDRCD, vndrCds), + ]); }); - console.log(`데이터베이스 저장 완료: ${processedVendors.length}개 벤더`); + console.log(`데이터베이스(배치) 저장 완료: ${processedVendors.length}개 벤더`); return true; } catch (error) { console.error('데이터베이스 저장 중 오류 발생:', error); |
