From e7b8ad6ebe3fd42d3511a9d346f72ce294210182 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 27 Aug 2025 11:59:02 +0000 Subject: (김준회) MDG 벤더 수신시 비즈니스 테이블 데이터 적재 처리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/soap/mdg/mapper/vendor-mapper.ts | 404 +++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 lib/soap/mdg/mapper/vendor-mapper.ts (limited to 'lib/soap') diff --git a/lib/soap/mdg/mapper/vendor-mapper.ts b/lib/soap/mdg/mapper/vendor-mapper.ts new file mode 100644 index 00000000..8a560cc4 --- /dev/null +++ b/lib/soap/mdg/mapper/vendor-mapper.ts @@ -0,0 +1,404 @@ +import { debugLog, debugSuccess, debugError } from '@/lib/debug-utils'; +import db from '@/db/db'; +import { vendors } from '@/db/schema/vendors'; +import { + VENDOR_MASTER_BP_HEADER_BP_VENGEN, + VENDOR_MASTER_BP_HEADER_BP_TAXNUM, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL, + VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL +} from '@/db/schema/MDG/mdg'; +import { eq } from 'drizzle-orm'; + +/** + * 벤더 매퍼 + * + * MDG에서 받은 벤더 데이터를 수신테이블에 적재하고 나서 + * 해당 벤더 데이터를 비즈니스 테이블로 매핑 & 적재해주는 도구 + * + * 수신테이블: mdg 스키마의 VENDOR_MASTER 관련 테이블 + * 비즈니스테이블: public 스키마의 vendors 테이블 + * + * 대부분 VENDOR_MASTER_BP_HEADER_BP_VENGEN 테이블에 적재된 데이터로 처리 가능함 + * 아래 매핑 규칙에서 좌측이 수신테이블이며, 우측은 비즈니스테이블(vendors)임 + * 모든 수신테이블 내 join 처리는 수신테이블의 외래키 필드인 VNDRCD 필드를 기준으로 처리함 + * 아래 순서는 vendors 테이블의 필드 순서를 기준으로 함. + * + * [BP_VENGEN]VNDRCD -> [vendors]vendorCode + * [BP_POSTAL]VNDRNM_1 -> [vendors]vendorName + * [BP_TAXNUM]BIZ_PTNR_TX_NO -> [vendors]taxId + * [BP_POSTAL]ETC_ADR1 -> [vendors]address + * [BP_POSTAL]ADR_1 + ADR_2 -> [vendors]addressDetail + * [BP_POSTAL]CITY_ZIP_NO -> [vendors]postalCode + * [BP_POSTAL]NTN_CD -> [vendors]country (국가코드) + * [BP_ADDRESS_AD_TEL]TEL_NO -> [vendors]phone (수신테이블쪽이 여러개일 수 있는데 1개만 처리) + * [BP_ADDRESS_AD_EMAIL]EMAIL_ADR -> [vendors]email (수신테이블쪽이 여러개일 수 있는데 1개만 처리) + * [BP_ADDRESS_AD_URL]URL -> [vendors]website (수신테이블쪽이 여러개일 수 있는데 1개만 처리) + * [vendors]status 항목은 항상 'ACTIVE'로 처리 + * ? vendorTypeId는 기존 값에서 처리할 수 없음. null로 남겨두기. + * [BP_VENGEN]REPR_NM -> [vendors]representativeName (대표자명) + * [BP_VENGEN]REPR_RESNO -> [vendors]representativeBirth (대표자생년월일) (앞 6자리만 잘라 넣어야 함) + * [BP_ADDRESS_AD_EMAIL]EMAIL_ADR -> [vendors]representativeEmail (대표자이메일, email과 동일한 값 사용) + * [BP_VENGEN]REP_TEL_NO -> [vendors]representativePhone (대표자연락처) + * ? representativeWorkExpirence은 기존 값에서 처리할 수 없음. null로 남겨두기 (오타난 거 암. 나중에 바꿀 것) + * [BP_VENGEN]CO_REG_NO -> [vendors]corporateRegistrationNumber (법인등록번호) + * [BP_VENGEN]CO_VLM -> [vendors]businessSize (사업규모) + * + */ + +// MDG 벤더 데이터 타입 정의 +export type MDGVendorData = { + VNDRCD: string; + VNDRNM_1?: string; + BIZ_PTNR_TX_NO?: string; + ETC_ADR_1?: string; + ADR_1?: string; + ADR_2?: string; + CITY_ZIP_NO?: string; + NTN_CD?: string; + TEL_NO?: string; + EMAIL_ADR?: string; + URL?: string; + REPR_NM?: string; + REPR_RESNO?: string; + REPR_EMAIL?: string; + REP_TEL_NO?: string; + CO_REG_NO?: string; + CO_VLM?: string; +}; + +// 비즈니스 테이블 데이터 타입 정의 +export type VendorData = typeof vendors.$inferInsert; + +/** + * MDG 벤더 마스터 데이터를 비즈니스 테이블 vendors에 매핑하여 저장 + */ +export async function mapAndSaveMDGVendorData( + vndrCodes: string[] +): Promise<{ success: boolean; message: string; processedCount: number }> { + try { + debugLog('MDG 벤더 마스터 데이터 매핑 시작', { count: vndrCodes.length }); + + if (vndrCodes.length === 0) { + return { success: true, message: '처리할 데이터가 없습니다', processedCount: 0 }; + } + + // MDG 수신테이블에서 데이터 조회 + const mdgVendorDataList = await fetchMDGVendorData(vndrCodes); + + if (mdgVendorDataList.length === 0) { + return { success: false, message: 'MDG 수신테이블에서 데이터를 찾을 수 없습니다', processedCount: 0 }; + } + + const mappedVendors: VendorData[] = []; + let processedCount = 0; + + for (const mdgVendorData of mdgVendorDataList) { + try { + // MDG 데이터를 vendors 테이블 구조에 매핑 + const mappedVendor = mapMDGToVendor(mdgVendorData); + + if (mappedVendor) { + mappedVendors.push(mappedVendor); + processedCount++; + } + } catch (error) { + debugError('개별 벤더 매핑 중 오류', { + vndrCode: mdgVendorData.VNDRCD, + error: error instanceof Error ? error.message : 'Unknown error' + }); + // 개별 오류는 로그만 남기고 계속 진행 + continue; + } + } + + if (mappedVendors.length === 0) { + return { success: false, message: '매핑된 벤더가 없습니다', processedCount: 0 }; + } + + // 데이터베이스에 저장 + await saveVendorsToDatabase(mappedVendors); + + debugSuccess('MDG 벤더 마스터 데이터 매핑 완료', { + total: vndrCodes.length, + processed: processedCount + }); + + return { + success: true, + message: `${processedCount}개 벤더 매핑 및 저장 완료`, + processedCount + }; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + debugError('MDG 벤더 마스터 데이터 매핑 중 오류 발생', { error: errorMessage }); + return { + success: false, + message: `매핑 실패: ${errorMessage}`, + processedCount: 0 + }; + } +} + +/** + * MDG 수신테이블에서 벤더 데이터 조회 + * 여러 테이블을 조인하여 매핑에 필요한 모든 데이터를 수집 + */ +async function fetchMDGVendorData(vndrCodes: string[]): Promise { + try { + debugLog('MDG 수신테이블에서 벤더 데이터 조회 시작', { vndrCodes }); + + const vendorDataList: MDGVendorData[] = []; + + for (const vndrCode of vndrCodes) { + try { + // 1. BP_VENGEN 테이블에서 기본 벤더 정보 조회 + const bpVengenData = await db + .select() + .from(VENDOR_MASTER_BP_HEADER_BP_VENGEN) + .where(eq(VENDOR_MASTER_BP_HEADER_BP_VENGEN.VNDRCD, vndrCode)) + .limit(1); + + if (bpVengenData.length === 0) { + debugError('BP_VENGEN 데이터가 없음', { vndrCode }); + continue; + } + + const vengenData = bpVengenData[0]; + + // 2. BP_TAXNUM 테이블에서 사업자번호 조회 + const bpTaxnumData = await db + .select() + .from(VENDOR_MASTER_BP_HEADER_BP_TAXNUM) + .where(eq(VENDOR_MASTER_BP_HEADER_BP_TAXNUM.VNDRCD, vndrCode)) + .limit(1); + + // 3. BP_POSTAL 테이블에서 주소 정보 조회 + const bpPostalData = await db + .select() + .from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL) + .where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_POSTAL.VNDRCD, vndrCode)) + .limit(1); + + // 4. BP_ADDRESS_AD_TEL 테이블에서 전화번호 조회 (첫 번째만) + const bpTelData = await db + .select() + .from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL) + .where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_TEL.VNDRCD, vndrCode)) + .limit(1); + + // 5. BP_ADDRESS_AD_EMAIL 테이블에서 이메일 조회 (첫 번째만) + const bpEmailData = await db + .select() + .from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL) + .where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_EMAIL.VNDRCD, vndrCode)) + .limit(1); + + // 6. BP_ADDRESS_AD_URL 테이블에서 웹사이트 조회 (첫 번째만) + const bpUrlData = await db + .select() + .from(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL) + .where(eq(VENDOR_MASTER_BP_HEADER_ADDRESS_AD_URL.VNDRCD, vndrCode)) + .limit(1); + + // 데이터 병합 + const mdgVendorData: MDGVendorData = { + VNDRCD: vndrCode, + VNDRNM_1: bpPostalData[0]?.VNDRNM_1 || undefined, // BP_POSTAL 테이블에서 벤더명 조회 + BIZ_PTNR_TX_NO: bpTaxnumData[0]?.BIZ_PTNR_TX_NO || undefined, + ETC_ADR_1: bpPostalData[0]?.ETC_ADR_1 || undefined, + ADR_1: bpPostalData[0]?.ADR_1 || undefined, + ADR_2: bpPostalData[0]?.ADR_2 || undefined, + CITY_ZIP_NO: bpPostalData[0]?.CITY_ZIP_NO || undefined, + NTN_CD: bpPostalData[0]?.NTN_CD || undefined, + TEL_NO: bpTelData[0]?.TELNO || undefined, + EMAIL_ADR: bpEmailData[0]?.EMAIL_ADR || undefined, + URL: bpUrlData[0]?.URL || undefined, + REPR_NM: vengenData.REPR_NM || undefined, + REPR_RESNO: vengenData.REPR_RESNO || undefined, + REPR_EMAIL: bpEmailData[0]?.EMAIL_ADR || undefined, // email과 동일한 값 사용 + REP_TEL_NO: vengenData.REP_TEL_NO || undefined, + CO_REG_NO: vengenData.CO_REG_NO || undefined, + CO_VLM: vengenData.CO_VLM || undefined, + }; + + vendorDataList.push(mdgVendorData); + + } catch (error) { + debugError('개별 벤더 데이터 조회 중 오류', { vndrCode, error }); + continue; + } + } + + debugSuccess('MDG 수신테이블에서 벤더 데이터 조회 완료', { + requested: vndrCodes.length, + found: vendorDataList.length + }); + + return vendorDataList; + + } catch (error) { + debugError('MDG 벤더 데이터 조회 중 오류', { error }); + throw error; + } +} + +/** + * MDG 벤더 데이터를 비즈니스 테이블 vendors 구조로 변환 + * + * vendors 테이블 구조: + * - id: serial (자동) + * - vendorName: varchar (필수) + * - vendorCode: varchar + * - taxId: varchar (필수) + * - address: text + * - addressDetail: text + * - postalCode: varchar + * - country: varchar + * - phone: varchar + * - email: varchar + * - website: varchar + * - status: varchar (기본값 'ACTIVE') + * - vendorTypeId: integer (null 허용) + * - representativeName: varchar + * - representativeBirth: varchar + * - representativeEmail: varchar + * - representativePhone: varchar + * - representativeWorkExpirence: boolean (기본값 false) + * - corporateRegistrationNumber: varchar + * - businessSize: varchar + * - createdAt: timestamp (자동) + * - updatedAt: timestamp (자동) + */ +function mapMDGToVendor(mdgVendorData: MDGVendorData): VendorData | null { + try { + // 필수 필드 검증 + if (!mdgVendorData.VNDRCD) { + debugError('VNDRCD가 없는 벤더 데이터', { data: mdgVendorData }); + return null; + } + + // taxId가 없으면 처리하지 않음 (vendors 테이블에서 필수) + if (!mdgVendorData.BIZ_PTNR_TX_NO) { + debugError('BIZ_PTNR_TX_NO가 없는 벤더 데이터', { vndrCode: mdgVendorData.VNDRCD }); + return null; + } + + // vendorName이 없으면 처리하지 않음 (vendors 테이블에서 필수) + if (!mdgVendorData.VNDRNM_1) { + debugError('VNDRNM_1이 없는 벤더 데이터', { vndrCode: mdgVendorData.VNDRCD }); + return null; + } + + // 주소 상세 정보 결합 + const addressDetail = [mdgVendorData.ADR_1, mdgVendorData.ADR_2] + .filter(Boolean) + .join(' ') + .trim() || undefined; + + // 대표자 생년월일 처리 (앞 6자리만) + const representativeBirth = mdgVendorData.REPR_RESNO + ? mdgVendorData.REPR_RESNO.substring(0, 6) + : undefined; + + const mappedVendor: VendorData = { + vendorCode: mdgVendorData.VNDRCD, + vendorName: mdgVendorData.VNDRNM_1, + taxId: mdgVendorData.BIZ_PTNR_TX_NO, + address: mdgVendorData.ETC_ADR_1 || undefined, + addressDetail: addressDetail, + postalCode: mdgVendorData.CITY_ZIP_NO || undefined, + country: mdgVendorData.NTN_CD || undefined, + phone: mdgVendorData.TEL_NO || undefined, + email: mdgVendorData.EMAIL_ADR || undefined, + website: mdgVendorData.URL || undefined, + status: 'ACTIVE', // 매핑 규칙에 따라 항상 'ACTIVE' + vendorTypeId: undefined, // 매핑 규칙에 따라 null + representativeName: mdgVendorData.REPR_NM || undefined, + representativeBirth: representativeBirth, + representativeEmail: mdgVendorData.REPR_EMAIL || undefined, // email과 동일한 값 + representativePhone: mdgVendorData.REP_TEL_NO || undefined, + representativeWorkExpirence: undefined, // 매핑 규칙에 따라 null + corporateRegistrationNumber: mdgVendorData.CO_REG_NO || undefined, + businessSize: mdgVendorData.CO_VLM || undefined, + // id, createdAt, updatedAt는 자동 생성 + }; + + debugLog('벤더 매핑 완료', { + original: mdgVendorData.VNDRCD, + mapped: mappedVendor.vendorCode + }); + + return mappedVendor; + + } catch (error) { + debugError('벤더 매핑 중 오류', { + vndrCode: mdgVendorData.VNDRCD, + error: error instanceof Error ? error.message : 'Unknown error' + }); + return null; + } +} + +/** + * 매핑된 벤더 데이터를 데이터베이스에 저장 + */ +async function saveVendorsToDatabase(mappedVendors: VendorData[]): Promise { + try { + debugLog('벤더 데이터베이스 저장 시작', { count: mappedVendors.length }); + + await db.transaction(async (tx) => { + // 기존 데이터와 중복 체크 및 UPSERT + for (const vendor of mappedVendors) { + if (vendor.vendorCode) { + // vendorCode가 있는 경우 기존 데이터 확인 + const existingVendor = await tx + .select({ id: vendors.id }) + .from(vendors) + .where(eq(vendors.vendorCode, vendor.vendorCode)) + .limit(1); + + if (existingVendor.length > 0) { + // 기존 데이터 업데이트 + await tx + .update(vendors) + .set({ + vendorName: vendor.vendorName, + taxId: vendor.taxId, + address: vendor.address, + addressDetail: vendor.addressDetail, + postalCode: vendor.postalCode, + country: vendor.country, + phone: vendor.phone, + email: vendor.email, + website: vendor.website, + status: vendor.status, + representativeName: vendor.representativeName, + representativeBirth: vendor.representativeBirth, + representativeEmail: vendor.representativeEmail, + representativePhone: vendor.representativePhone, + corporateRegistrationNumber: vendor.corporateRegistrationNumber, + businessSize: vendor.businessSize, + updatedAt: new Date(), + }) + .where(eq(vendors.vendorCode, vendor.vendorCode)); + } else { + // 새 데이터 삽입 + await tx.insert(vendors).values(vendor); + } + } else { + // vendorCode가 없는 경우 새 데이터 삽입 + await tx.insert(vendors).values(vendor); + } + } + }); + + debugSuccess('벤더 데이터베이스 저장 완료', { count: mappedVendors.length }); + + } catch (error) { + debugError('벤더 데이터베이스 저장 중 오류', { error }); + throw error; + } +} \ No newline at end of file -- cgit v1.2.3