/** * MDG 마이그레이션 진행되지 않은 상태라 PLM DB를 싱크해 사용했으므로, 추후 수정 필요 * PLM 쪽으로는 업데이트 불가능하므로, 최초 1회 마이그레이션한 데이터만 사용할 것임 * node-cron 으로 PLM 데이터 동기화할 필요도 없다는 얘기 */ import { eq } from 'drizzle-orm' import { cmctbVendorGeneral, cmctbVendorAddr, cmctbVendorCompny, cmctbVendorPorg, cmctbVendorRepremail, cmctbVendorInco, type CmctbVendorGeneral, type CmctbVendorAddr } from '@/db/schema/NONSAP/nonsap' import { vendors } from '@/db/schema/vendors' import db from '@/db/db' import { debugLog, debugError, debugWarn, debugSuccess } from '@/lib/debug-utils' // 구매조직별 정보 타입 정의 export interface PurchasingOrgInfo { PUR_ORG_CD: string // 구매조직 코드 PUR_ORD_CUR: string | null // 오더통화 SPLY_COND: string | null // 지급조건 DL_COND_1: string | null // 인도조건1 DL_COND_2: string | null // 인도조건2 GR_BSE_INVC_VR: string | null // GR기준송장검증 ORD_CNFM_REQ_ORDR: string | null // P/O 확인요청 CNFM_CTL_KEY: string | null // 확정제어키 PUR_HOLD_ORDR: string | null // 구매보류지시자 DEL_ORDR: string | null // 삭제지시자 AT_PUR_ORD_ORDR: string | null // 자동구매오더지시자 SALE_CHRGR_NM: string | null // 영업담당자명 VNDR_TELNO: string | null // VENDOR전화번호 PUR_HOLD_DT: string | null // 구매보류일자 PUR_HOLD_CAUS: string | null // 구매보류사유 } // 벤더 상세 정보 타입 정의 export interface VendorDetails { // 기본 정보 VNDRCD: string VNDRNM_1: string | null VNDRNM_2: string | null VNDRNM_ABRV_1: string | null BIZR_NO: string | null CO_REG_NO: string | null CO_VLM: string | null // 대표자 정보 REPR_NM: string | null REP_TEL_NO: string | null REPR_RESNO: string | null REPRESENTATIVE_EMAIL: string | null // 사업 정보 BIZTP: string | null BIZCON: string | null NTN_CD: string | null REG_DT: string | null // 주소 정보 ADR_1: string | null ADR_2: string | null POSTAL_CODE: string | null ADDR_DETAIL_1: string | null // 이전업체코드 PREVIOUS_VENDOR_CODE: string | null // 내외자구분 (사내협력사 정보) << 정확하지 않음. 추후 확인 필요 PRTNR_GB: string | null // 구매조직별 정보 (배열) PURCHASING_ORGS: PurchasingOrgInfo[] // 상태 정보 DEL_ORDR: string | null PUR_HOLD_ORDR: string | null } // 벤더 수정 데이터 타입 export interface VendorUpdateData { // 기본 정보 VNDRNM_1?: string VNDRNM_2?: string VNDRNM_ABRV_1?: string BIZR_NO?: string CO_REG_NO?: string CO_VLM?: string // 대표자 정보 REPR_NM?: string REP_TEL_NO?: string REPR_RESNO?: string REPRESENTATIVE_EMAIL?: string // 사업 정보 BIZTP?: string BIZCON?: string NTN_CD?: string // 주소 정보 ADR_1?: string ADR_2?: string POSTAL_CODE?: string ADDR_DETAIL_1?: string } // 벤더 목록 아이템 타입 export interface VendorListItem { VNDRCD: string VNDRNM_1: string | null VNDRNM_2: string | null BIZR_NO: string | null REG_DT: string | null DEL_ORDR: string | null PUR_HOLD_ORDR: string | null } export class VendorMdgService { /** * 벤더 ID로 벤더 코드 조회 * @param vendorId 벤더 ID * @returns 벤더 코드 (VNDRCD) */ async getVendorCodeByVendorId(vendorId: string): Promise { debugLog(`벤더 코드 조회 시작: ID ${vendorId}`) try { const vendor = await db .select({ vendorCode: vendors.vendorCode }) .from(vendors) .where(eq(vendors.id, parseInt(vendorId))) .limit(1) debugLog(`vendors 테이블 조회 결과:`, { found: vendor.length > 0, vendorCode: vendor[0]?.vendorCode || null }) if (vendor.length === 0) { debugWarn(`벤더 ID ${vendorId}에 해당하는 벤더를 찾을 수 없습니다.`) return null } const vendorCode = vendor[0].vendorCode if (!vendorCode) { debugWarn(`벤더 ID ${vendorId}의 vendor_code가 null입니다.`) return null } debugSuccess(`벤더 코드 조회 성공: ID ${vendorId} -> 코드 ${vendorCode}`) return vendorCode } catch (error) { debugError('벤더 코드 조회 중 오류 발생:', error) return null } } /** * 벤더 ID로 벤더 상세 정보 조회 * @param vendorId 벤더 ID * @returns 벤더 상세 정보 (데이터가 없어도 기본 구조 반환) */ async getVendorDetailsByVendorId(vendorId: string): Promise { debugLog(`벤더 ID로 상세 정보 조회 시작: ${vendorId}`) // 1. 벤더 코드 조회 const vendorCode = await this.getVendorCodeByVendorId(vendorId) if (!vendorCode) { debugWarn(`벤더 ID ${vendorId}에 대한 벤더 코드를 찾을 수 없습니다.`) return null } // 2. 벤더 코드로 상세 정보 조회 debugLog(`벤더 코드 ${vendorCode}로 상세 정보 조회 시작`) return await this.getVendorDetails(vendorCode) } /** * 벤더 코드로 벤더 상세 정보 조회 * @param vendorCode 벤더 코드 * @returns 벤더 상세 정보 (데이터가 없어도 기본 구조 반환) */ async getVendorDetails(vendorCode: string): Promise { debugLog(`벤더 정보 조회 시작: ${vendorCode}`) try { // 메인 쿼리: 벤더 일반 정보 debugLog(`CMCTB_VENDOR_GENERAL 테이블에서 ${vendorCode} 조회 중...`) const vendorGeneral = await db .select() .from(cmctbVendorGeneral) .where(eq(cmctbVendorGeneral.VNDRCD, vendorCode)) .limit(1) debugLog(`CMCTB_VENDOR_GENERAL 조회 결과:`, { found: vendorGeneral.length > 0, count: vendorGeneral.length, data: vendorGeneral.length > 0 ? vendorGeneral[0] : null }) const vendor = vendorGeneral[0] || null // 주소 정보 조회 debugLog(`CMCTB_VENDOR_ADDR 테이블에서 ${vendorCode} 조회 중...`) const vendorAddr = await db .select() .from(cmctbVendorAddr) .where(eq(cmctbVendorAddr.VNDRCD, vendorCode)) .limit(1) debugLog(`CMCTB_VENDOR_ADDR 조회 결과:`, { found: vendorAddr.length > 0, count: vendorAddr.length, data: vendorAddr.length > 0 ? vendorAddr[0] : null }) // 회사 정보 조회 (첫 번째 회사 코드) debugLog(`CMCTB_VENDOR_COMPNY 테이블에서 ${vendorCode} 조회 중...`) const vendorCompany = await db .select() .from(cmctbVendorCompny) .where(eq(cmctbVendorCompny.VNDRCD, vendorCode)) .limit(1) debugLog(`CMCTB_VENDOR_COMPNY 조회 결과:`, { found: vendorCompany.length > 0, count: vendorCompany.length, data: vendorCompany.length > 0 ? vendorCompany[0] : null }) // 모든 구매조직 정보 조회 debugLog(`CMCTB_VENDOR_PORG 테이블에서 ${vendorCode}의 모든 구매조직 조회 중...`) const vendorPorgs = await db .select() .from(cmctbVendorPorg) .where(eq(cmctbVendorPorg.VNDRCD, vendorCode)) debugLog(`CMCTB_VENDOR_PORG 조회 결과:`, { found: vendorPorgs.length > 0, count: vendorPorgs.length, data: vendorPorgs }) // 사내협력사 정보 조회 (내외자구분) debugLog(`CMCTB_VENDOR_INCO 테이블에서 ${vendorCode} 조회 중...`) const vendorInco = await db .select() .from(cmctbVendorInco) .where(eq(cmctbVendorInco.VNDRCD, vendorCode)) .limit(1) debugLog(`CMCTB_VENDOR_INCO 조회 결과:`, { found: vendorInco.length > 0, count: vendorInco.length, data: vendorInco.length > 0 ? vendorInco[0] : null }) // 대표자 이메일 조회 debugLog(`CMCTB_VENDOR_REPREMAIL 테이블에서 ${vendorCode} 조회 중...`) const vendorEmail = await db .select() .from(cmctbVendorRepremail) .where(eq(cmctbVendorRepremail.VNDRCD, vendorCode)) .limit(1) debugLog(`CMCTB_VENDOR_REPREMAIL 조회 결과:`, { found: vendorEmail.length > 0, count: vendorEmail.length, data: vendorEmail.length > 0 ? vendorEmail[0] : null }) const addr = vendorAddr[0] || null const company = vendorCompany[0] || null const inco = vendorInco[0] || null const email = vendorEmail[0] || null // 구매조직 정보 배열 구성 const purchasingOrgs: PurchasingOrgInfo[] = vendorPorgs.map(porg => ({ PUR_ORG_CD: porg.PUR_ORG_CD, PUR_ORD_CUR: porg.PUR_ORD_CUR, SPLY_COND: porg.SPLY_COND, DL_COND_1: porg.DL_COND_1, DL_COND_2: porg.DL_COND_2, GR_BSE_INVC_VR: porg.GR_BSE_INVC_VR, ORD_CNFM_REQ_ORDR: porg.ORD_CNFM_REQ_ORDR, CNFM_CTL_KEY: porg.CNFM_CTL_KEY, PUR_HOLD_ORDR: porg.PUR_HOLD_ORDR, DEL_ORDR: porg.DEL_ORDR, AT_PUR_ORD_ORDR: porg.AT_PUR_ORD_ORDR, SALE_CHRGR_NM: porg.SALE_CHRGR_NM, VNDR_TELNO: porg.VNDR_TELNO, PUR_HOLD_DT: porg.PUR_HOLD_DT, PUR_HOLD_CAUS: porg.PUR_HOLD_CAUS })) // 데이터 존재 여부 확인 const hasAnyData = vendor || addr || company || purchasingOrgs.length > 0 || inco || email if (!hasAnyData) { debugWarn(`벤더 ${vendorCode}에 대한 데이터가 전혀 없습니다. 기본 구조만 반환합니다.`) } else { debugSuccess(`벤더 ${vendorCode} 데이터 조회 완료`, { general: !!vendor, addr: !!addr, company: !!company, purchasingOrgs: purchasingOrgs.length, inco: !!inco, email: !!email }) } // 벤더 상세 정보 구성 (데이터가 없어도 기본 구조 제공) const vendorDetails: VendorDetails = { // 기본 정보 (General 테이블) VNDRCD: vendorCode, // 항상 요청된 벤더 코드 반환 VNDRNM_1: vendor?.VNDRNM_1 || null, VNDRNM_2: vendor?.VNDRNM_2 || null, VNDRNM_ABRV_1: vendor?.VNDRNM_ABRV_1 || null, BIZR_NO: vendor?.BIZR_NO || null, CO_REG_NO: vendor?.CO_REG_NO || null, CO_VLM: vendor?.CO_VLM || null, // 대표자 정보 REPR_NM: vendor?.REPR_NM || null, REP_TEL_NO: vendor?.REP_TEL_NO || null, REPR_RESNO: vendor?.REPR_RESNO || null, REPRESENTATIVE_EMAIL: email?.EMAIL_ADR || null, // 사업 정보 BIZTP: vendor?.BIZTP || null, BIZCON: vendor?.BIZCON || null, NTN_CD: vendor?.NTN_CD || null, REG_DT: vendor?.REG_DT || null, // 주소 정보 (Address 테이블 우선, 없으면 General 테이블) ADR_1: addr?.ADR_1 || vendor?.ADR_1 || null, ADR_2: addr?.ADR_2 || vendor?.ADR_2 || null, POSTAL_CODE: addr?.CITY_ZIP_NO || null, ADDR_DETAIL_1: addr?.ETC_ADR_1 || null, // 이전업체코드 PREVIOUS_VENDOR_CODE: company?.BF_VNDRCD || null, // 내외자구분 (사내협력사 정보) PRTNR_GB: inco?.PRTNR_GB || null, // 구매조직별 정보 배열 PURCHASING_ORGS: purchasingOrgs, // 상태 정보 (기본값 제공) DEL_ORDR: vendor?.DEL_ORDR || 'N', // 기본값: 활성 PUR_HOLD_ORDR: vendor?.PUR_HOLD_ORDR || null } debugLog(`최종 벤더 정보 구성 완료:`, vendorDetails) return vendorDetails } catch (error) { debugError('벤더 정보 조회 중 오류 발생:', error) // 오류가 발생해도 기본 구조는 제공 debugWarn(`오류로 인해 ${vendorCode}의 기본 구조만 반환합니다.`) return { VNDRCD: vendorCode, VNDRNM_1: null, VNDRNM_2: null, VNDRNM_ABRV_1: null, BIZR_NO: null, CO_REG_NO: null, CO_VLM: null, REPR_NM: null, REP_TEL_NO: null, REPR_RESNO: null, REPRESENTATIVE_EMAIL: null, BIZTP: null, BIZCON: null, NTN_CD: null, REG_DT: null, ADR_1: null, ADR_2: null, POSTAL_CODE: null, ADDR_DETAIL_1: null, PREVIOUS_VENDOR_CODE: null, PRTNR_GB: null, PURCHASING_ORGS: [], DEL_ORDR: 'N', // 기본값: 활성 PUR_HOLD_ORDR: null } } } /** * 벤더 기본 정보 수정 * @param vendorCode 벤더 코드 * @param updateData 수정할 데이터 * @returns 성공 여부 */ async updateVendorBasicInfo( vendorCode: string, updateData: VendorUpdateData ): Promise { try { // 트랜잭션으로 여러 테이블 업데이트 await db.transaction(async (tx) => { // 1. General 테이블 업데이트 const generalUpdateData: Partial = {} if (updateData.VNDRNM_1 !== undefined) generalUpdateData.VNDRNM_1 = updateData.VNDRNM_1 if (updateData.VNDRNM_2 !== undefined) generalUpdateData.VNDRNM_2 = updateData.VNDRNM_2 if (updateData.VNDRNM_ABRV_1 !== undefined) generalUpdateData.VNDRNM_ABRV_1 = updateData.VNDRNM_ABRV_1 if (updateData.BIZR_NO !== undefined) generalUpdateData.BIZR_NO = updateData.BIZR_NO if (updateData.CO_REG_NO !== undefined) generalUpdateData.CO_REG_NO = updateData.CO_REG_NO if (updateData.CO_VLM !== undefined) generalUpdateData.CO_VLM = updateData.CO_VLM if (updateData.REPR_NM !== undefined) generalUpdateData.REPR_NM = updateData.REPR_NM if (updateData.REP_TEL_NO !== undefined) generalUpdateData.REP_TEL_NO = updateData.REP_TEL_NO if (updateData.REPR_RESNO !== undefined) generalUpdateData.REPR_RESNO = updateData.REPR_RESNO if (updateData.BIZTP !== undefined) generalUpdateData.BIZTP = updateData.BIZTP if (updateData.BIZCON !== undefined) generalUpdateData.BIZCON = updateData.BIZCON if (updateData.NTN_CD !== undefined) generalUpdateData.NTN_CD = updateData.NTN_CD if (updateData.ADR_1 !== undefined) generalUpdateData.ADR_1 = updateData.ADR_1 if (updateData.ADR_2 !== undefined) generalUpdateData.ADR_2 = updateData.ADR_2 // 현재 시간 설정 generalUpdateData.CHG_DT = new Date().toISOString().slice(0, 10).replace(/-/g, '') generalUpdateData.CHG_TM = new Date().toTimeString().slice(0, 8).replace(/:/g, '') if (Object.keys(generalUpdateData).length > 2) { // CHG_DT, CHG_TM 외에 다른 필드가 있는 경우만 업데이트 await tx .update(cmctbVendorGeneral) .set(generalUpdateData) .where(eq(cmctbVendorGeneral.VNDRCD, vendorCode)) } // 2. Address 테이블 업데이트 (있는 경우) if (updateData.ADR_1 || updateData.ADR_2 || updateData.POSTAL_CODE || updateData.ADDR_DETAIL_1) { const addrUpdateData: Partial = {} if (updateData.ADR_1 !== undefined) addrUpdateData.ADR_1 = updateData.ADR_1 if (updateData.ADR_2 !== undefined) addrUpdateData.ADR_2 = updateData.ADR_2 if (updateData.POSTAL_CODE !== undefined) addrUpdateData.CITY_ZIP_NO = updateData.POSTAL_CODE if (updateData.ADDR_DETAIL_1 !== undefined) addrUpdateData.ETC_ADR_1 = updateData.ADDR_DETAIL_1 // 주소 레코드가 있는지 확인 const existingAddr = await tx .select() .from(cmctbVendorAddr) .where(eq(cmctbVendorAddr.VNDRCD, vendorCode)) .limit(1) if (existingAddr.length > 0) { // 기존 주소 업데이트 await tx .update(cmctbVendorAddr) .set(addrUpdateData) .where(eq(cmctbVendorAddr.VNDRCD, vendorCode)) } else { // 새 주소 레코드 생성 await tx .insert(cmctbVendorAddr) .values({ VNDRCD: vendorCode, ADR_NO: '0001', INTL_ADR_VER_ID: '1', ...addrUpdateData }) } } // 3. 대표자 이메일 업데이트 (있는 경우) if (updateData.REPRESENTATIVE_EMAIL !== undefined) { // 기존 이메일 레코드가 있는지 확인 const existingEmail = await tx .select() .from(cmctbVendorRepremail) .where(eq(cmctbVendorRepremail.VNDRCD, vendorCode)) .limit(1) const currentDate = new Date().toISOString().slice(0, 10).replace(/-/g, '') if (existingEmail.length > 0) { // 기존 이메일 업데이트 await tx .update(cmctbVendorRepremail) .set({ EMAIL_ADR: updateData.REPRESENTATIVE_EMAIL, IF_DT: currentDate, IF_TM: new Date().toTimeString().slice(0, 8).replace(/:/g, ''), IF_STAT: '1' }) .where(eq(cmctbVendorRepremail.VNDRCD, vendorCode)) } else if (updateData.REPRESENTATIVE_EMAIL) { // 새 이메일 레코드 생성 (빈 문자열이 아닌 경우만) await tx .insert(cmctbVendorRepremail) .values({ VNDRCD: vendorCode, ADR_NO: '0001', REPR_SER: '001', VLD_ST_DT: currentDate, EMAIL_ADR: updateData.REPRESENTATIVE_EMAIL, IF_DT: currentDate, IF_TM: new Date().toTimeString().slice(0, 8).replace(/:/g, ''), IF_STAT: '1' }) } } }) return true } catch (error) { console.error('벤더 정보 수정 중 오류 발생:', error) throw new Error('벤더 정보를 수정할 수 없습니다.') } } /** * 벤더 목록 조회 (페이징) * @param page 페이지 번호 (1부터 시작) * @param limit 페이지당 개수 * @param searchTerm 검색어 (업체명 검색) * @returns 벤더 목록과 총 개수 */ async getVendorList( page: number = 1, limit: number = 20, searchTerm?: string ): Promise<{ vendors: VendorListItem[], total: number }> { try { const offset = (page - 1) * limit // 기본 조건: 활성 벤더만 조회 const whereCondition = eq(cmctbVendorGeneral.DEL_ORDR, 'N') // 검색어가 있는 경우 업체명으로 필터링 (추후 구현 시 사용) void searchTerm // 현재는 미사용 // 총 개수 조회 const totalResult = await db .select({ count: cmctbVendorGeneral.VNDRCD }) .from(cmctbVendorGeneral) .where(whereCondition) // 벤더 목록 조회 const vendors = await db .select({ VNDRCD: cmctbVendorGeneral.VNDRCD, VNDRNM_1: cmctbVendorGeneral.VNDRNM_1, VNDRNM_2: cmctbVendorGeneral.VNDRNM_2, BIZR_NO: cmctbVendorGeneral.BIZR_NO, REG_DT: cmctbVendorGeneral.REG_DT, DEL_ORDR: cmctbVendorGeneral.DEL_ORDR, PUR_HOLD_ORDR: cmctbVendorGeneral.PUR_HOLD_ORDR }) .from(cmctbVendorGeneral) .where(whereCondition) .limit(limit) .offset(offset) return { vendors, total: totalResult.length } } catch (error) { console.error('벤더 목록 조회 중 오류 발생:', error) throw new Error('벤더 목록을 조회할 수 없습니다.') } } /** * 벤더 상태 변경 (활성/비활성) * @param vendorCode 벤더 코드 * @param isActive 활성 상태 여부 * @returns 성공 여부 */ async updateVendorStatus(vendorCode: string, isActive: boolean): Promise { try { await db .update(cmctbVendorGeneral) .set({ DEL_ORDR: isActive ? 'N' : 'Y', CHG_DT: new Date().toISOString().slice(0, 10).replace(/-/g, ''), CHG_TM: new Date().toTimeString().slice(0, 8).replace(/:/g, '') }) .where(eq(cmctbVendorGeneral.VNDRCD, vendorCode)) return true } catch (error) { console.error('벤더 상태 변경 중 오류 발생:', error) throw new Error('벤더 상태를 변경할 수 없습니다.') } } } // 싱글톤 인스턴스 생성 export const vendorMdgService = new VendorMdgService()