"use server"; import * as cron from 'node-cron'; import { oracleKnex } from '@/lib/oracle-db/db'; import db from '@/db/db'; import { paymentTerms, incoterms, placeOfShipping } from '@/db/schema/procurementRFQ'; import { inArray } from 'drizzle-orm'; // 간단한 로거 const logger = { info: (message: string, ...args: unknown[]) => console.log(`[PROCUREMENT-SYNC] ${message}`, ...args), error: (message: string, ...args: unknown[]) => console.error(`[PROCUREMENT-SYNC ERROR] ${message}`, ...args), warn: (message: string, ...args: unknown[]) => console.warn(`[PROCUREMENT-SYNC WARN] ${message}`, ...args), success: (message: string, ...args: unknown[]) => console.log(`[PROCUREMENT-SYNC SUCCESS] ${message}`, ...args), header: (message: string) => { console.log('\n' + '='.repeat(80)); console.log(`[PROCUREMENT-SYNC] ${message}`); console.log('='.repeat(80) + '\n'); } }; /** * Oracle DB 연결 테스트 */ async function testOracleConnection(): Promise { try { const result = await oracleKnex.raw('SELECT 1 FROM DUAL'); return result && result.length > 0; } catch (error) { logger.error('Oracle DB 연결 테스트 실패:', error); return false; } } /** * 지불조건 동기화 */ async function syncPaymentTerms(): Promise { logger.header('지불조건 동기화 시작'); try { // Oracle에서 지불조건 데이터 조회 const oracleData = await oracleKnex.raw(` SELECT stc.CD_1 as code, stc.DSC as description FROM SRMPTB_TYPE_CODE stc WHERE stc.cd = 'PAYT' `); const paymentTermsData = oracleData || []; logger.info(`Oracle에서 ${paymentTermsData.length}개의 지불조건 데이터 조회`); if (paymentTermsData.length === 0) { logger.warn('Oracle에서 지불조건 데이터가 없습니다'); return; } // PostgreSQL에서 기존 데이터 조회 const existingData = await db.select().from(paymentTerms); const existingCodes = new Set(existingData.map(item => item.code)); // Oracle에서 조회한 코드들 const oracleCodes = new Set(paymentTermsData.map((item: { CODE: string; DESCRIPTION: string }) => item.CODE)); // 업데이트할 데이터 준비 const upsertData = paymentTermsData.map((item: { CODE: string; DESCRIPTION: string }) => ({ code: item.CODE, description: item.DESCRIPTION || 'NONSAP에서 설명 기입 요망', isActive: true // 기본값 true })); // PostgreSQL에 upsert for (const data of upsertData) { await db.insert(paymentTerms) .values(data) .onConflictDoUpdate({ target: paymentTerms.code, set: { description: data.description, isActive: data.isActive } }); } // PostgreSQL에만 있는 데이터는 isActive = false로 설정 const codesToDeactivate = new Set([...existingCodes].filter(code => !oracleCodes.has(code))); if (codesToDeactivate.size > 0) { await db.update(paymentTerms) .set({ isActive: false }) .where(inArray(paymentTerms.code, Array.from(codesToDeactivate))); logger.info(`${codesToDeactivate.size}개의 지불조건을 비활성화했습니다`); } logger.success(`지불조건 동기화 완료: ${upsertData.length}개 업데이트`); } catch (error) { logger.error('지불조건 동기화 중 오류:', error); throw error; } } /** * 인코텀즈 동기화 */ async function syncIncoterms(): Promise { logger.header('인코텀즈 동기화 시작'); try { // Oracle에서 인코텀즈 데이터 조회 const oracleData = await oracleKnex.raw(` SELECT stc.CD_1 as code, stc.DSC as description FROM SRMPTB_TYPE_CODE stc WHERE stc.cd = 'INCO' `); const incotermsData = oracleData || []; logger.info(`Oracle에서 ${incotermsData.length}개의 인코텀즈 데이터 조회`); if (incotermsData.length === 0) { logger.warn('Oracle에서 인코텀즈 데이터가 없습니다'); return; } // PostgreSQL에서 기존 데이터 조회 const existingData = await db.select().from(incoterms); const existingCodes = new Set(existingData.map(item => item.code)); // Oracle에서 조회한 코드들 const oracleCodes = new Set(incotermsData.map((item: { CODE: string; DESCRIPTION: string }) => item.CODE)); // 업데이트할 데이터 준비 const upsertData = incotermsData.map((item: { CODE: string; DESCRIPTION: string }) => ({ code: item.CODE, description: item.DESCRIPTION || 'NONSAP에서 설명 기입 요망', isActive: true // 기본값 true })); // PostgreSQL에 upsert for (const data of upsertData) { await db.insert(incoterms) .values(data) .onConflictDoUpdate({ target: incoterms.code, set: { description: data.description, isActive: data.isActive } }); } // PostgreSQL에만 있는 데이터는 isActive = false로 설정 const codesToDeactivate = new Set([...existingCodes].filter(code => !oracleCodes.has(code))); if (codesToDeactivate.size > 0) { await db.update(incoterms) .set({ isActive: false }) .where(inArray(incoterms.code, Array.from(codesToDeactivate))); logger.info(`${codesToDeactivate.size}개의 인코텀즈를 비활성화했습니다`); } logger.success(`인코텀즈 동기화 완료: ${upsertData.length}개 업데이트`); } catch (error) { logger.error('인코텀즈 동기화 중 오류:', error); throw error; } } /** * 선적/하역지 동기화 */ async function syncPlaceOfShipping(): Promise { logger.header('선적/하역지 동기화 시작'); try { // Oracle에서 선적/하역지 데이터 조회 const oracleData = await oracleKnex.raw(` SELECT cd.CD as code, cdnm.CDNM as description, cd.DEL_YN as isActive FROM CMCTB_CD cd INNER JOIN CMCTB_CDNM cdnm ON cdnm.cd = cd.cd AND CDNM.CD_CLF = CD.CD_CLF WHERE cd.CD_CLF = 'MMM050' ORDER BY cd.CD asc, cdnm.CDNM asc `); const placeOfShippingData = oracleData || []; logger.info(`Oracle에서 ${placeOfShippingData.length}개의 선적/하역지 데이터 조회`); if (placeOfShippingData.length === 0) { logger.warn('Oracle에서 선적/하역지 데이터가 없습니다'); return; } // PostgreSQL에서 기존 데이터 조회 const existingData = await db.select().from(placeOfShipping); const existingCodes = new Set(existingData.map(item => item.code)); // Oracle에서 조회한 코드들 const oracleCodes = new Set(placeOfShippingData.map((item: { CODE: string; DESCRIPTION: string; ISACTIVE: string }) => item.CODE)); // 업데이트할 데이터 준비 (isActive = "Y"인 경우 true, 그 외는 기본값 true) const upsertData = placeOfShippingData.map((item: { CODE: string; DESCRIPTION: string; ISACTIVE: string }) => ({ code: item.CODE, description: item.DESCRIPTION || 'NONSAP에서 설명 기입 요망', isActive: item.ISACTIVE === 'Y' ? true : true // 기본값 true })); // PostgreSQL에 upsert for (const data of upsertData) { await db.insert(placeOfShipping) .values(data) .onConflictDoUpdate({ target: placeOfShipping.code, set: { description: data.description, isActive: data.isActive } }); } // PostgreSQL에만 있는 데이터는 isActive = false로 설정 const codesToDeactivate = new Set([...existingCodes].filter(code => !oracleCodes.has(code))); if (codesToDeactivate.size > 0) { await db.update(placeOfShipping) .set({ isActive: false }) .where(inArray(placeOfShipping.code, Array.from(codesToDeactivate))); logger.info(`${codesToDeactivate.size}개의 선적/하역지를 비활성화했습니다`); } logger.success(`선적/하역지 동기화 완료: ${upsertData.length}개 업데이트`); } catch (error) { logger.error('선적/하역지 동기화 중 오류:', error); throw error; } } /** * 모든 procurement 관련 테이블 동기화 */ export async function syncAllProcurementTables(): Promise { logger.header('Procurement 테이블 동기화 시작'); try { await syncPaymentTerms(); await syncIncoterms(); await syncPlaceOfShipping(); logger.success('모든 Procurement 테이블 동기화 완료'); } catch (error) { logger.error('Procurement 테이블 동기화 중 오류:', error); throw error; } } /** * 수동 동기화 트리거 */ export async function triggerProcurementSync(): Promise { logger.info('수동 Procurement 동기화 시작'); await syncAllProcurementTables(); logger.success('수동 Procurement 동기화 완료'); } /** * Procurement 동기화 스케줄러 시작 */ export async function startProcurementSyncScheduler(): Promise { logger.info('Initializing Procurement data synchronization scheduler...'); // Oracle DB 연결 테스트 (비동기) testOracleConnection().then(isConnected => { if (!isConnected) { logger.warn('Oracle DB connection failed - procurement sync scheduler will be disabled'); logger.warn('Application will continue to run normally'); return; } logger.success('Oracle DB connection test passed'); }).catch(error => { logger.error('Oracle DB connection test error:', error); logger.warn('Procurement sync scheduler will be disabled, application continues'); }); try { // 매일 새벽 2시에 실행 (기존 스케줄러들과 시간 분산) cron.schedule('0 2 * * *', async () => { try { logger.info('Cron job triggered: Starting scheduled procurement sync'); // 동기화 전 Oracle 연결 확인 const isConnected = await testOracleConnection(); if (!isConnected) { logger.warn('Oracle DB not available, skipping procurement sync'); return; } await syncAllProcurementTables(); } catch (error) { logger.error('Scheduled procurement sync failed:', error); // 동기화 실패해도 다음 스케줄은 계속 실행 } }, { timezone: 'Asia/Seoul' }); logger.success('Procurement data synchronization cron job registered (daily at 2:00 AM)'); // 애플리케이션 시작 시 한 번 실행 (선택사항) if (process.env.PROCUREMENT_SYNC_ON_START === 'true') { logger.info('Initial procurement sync on startup enabled'); setTimeout(async () => { try { const isConnected = await testOracleConnection(); if (isConnected) { await syncAllProcurementTables(); } else { logger.warn('Initial procurement sync skipped - Oracle DB not available'); } } catch (error) { logger.error('Initial procurement sync failed:', error); } }, 15000); // 15초 후 실행 (다른 스케줄러들과 시간 분산) } } catch (error) { logger.error('Failed to set up procurement cron scheduler:', error); logger.warn('Application will continue without procurement sync scheduler'); } }