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; } }