summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-08-27 11:59:02 +0000
committerjoonhoekim <26rote@gmail.com>2025-08-27 11:59:02 +0000
commite7b8ad6ebe3fd42d3511a9d346f72ce294210182 (patch)
tree1788ed17c82e46e617a723b0e27259697e210914
parenta0c94e1d019339babf8bd45b1ff192ade47fc6e7 (diff)
(김준회) MDG 벤더 수신시 비즈니스 테이블 데이터 적재 처리
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_VENDOR_MASTER/route.ts8
-rw-r--r--lib/soap/mdg/mapper/vendor-mapper.ts404
2 files changed, 412 insertions, 0 deletions
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 9c74b5c5..9caaa61a 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
@@ -30,6 +30,7 @@ import {
bulkUpsert,
bulkReplaceSubTableData
} from "@/lib/soap/batch-utils";
+import { mapAndSaveMDGVendorData } from "@/lib/soap/mdg/mapper/vendor-mapper";
// 스키마에서 직접 타입 추론
type VendorHeaderData = typeof VENDOR_MASTER_BP_HEADER.$inferInsert;
@@ -165,6 +166,13 @@ export async function POST(request: NextRequest) {
// 데이터베이스 저장
await saveToDatabase(processedVendors);
+ // 벤더 매퍼를 사용하여 비즈니스 테이블에 데이터 매핑 및 저장
+ const vndrCodes = processedVendors.map(v => v.vendorHeader.VNDRCD).filter((code): code is string => !!code);
+ if (vndrCodes.length > 0) {
+ const mapperResult = await mapAndSaveMDGVendorData(vndrCodes);
+ console.log(`📋 벤더 매퍼 결과: ${mapperResult.message}`);
+ }
+
console.log(`🎉 처리 완료: ${processedVendors.length}개 벤더 데이터`);
return createSuccessResponse('http://60.101.108.100/api/IF_MDZ_EVCP_VENDOR_MASTER/');
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<MDGVendorData[]> {
+ 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<void> {
+ 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