diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-04 11:07:10 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-04 11:07:10 +0000 |
| commit | 305a29e0e245ed9de1a7f4d32e96634116dcda82 (patch) | |
| tree | bba0f654d7072ab8b619ac08ccc4b988fef143f3 /lib/nonsap-sync/procurement-sync-service.ts | |
| parent | 4dc27e9495b005b29b4d7a2ad404c8c0644769eb (diff) | |
(김준회) SOAP ECC 개선, 린터 오류 해결, 구매 기준정보(인코텀즈, 지불조건, 장소(출발지, 도착지)) Non-SAP 동기화 처리
Diffstat (limited to 'lib/nonsap-sync/procurement-sync-service.ts')
| -rw-r--r-- | lib/nonsap-sync/procurement-sync-service.ts | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/lib/nonsap-sync/procurement-sync-service.ts b/lib/nonsap-sync/procurement-sync-service.ts new file mode 100644 index 00000000..1f719526 --- /dev/null +++ b/lib/nonsap-sync/procurement-sync-service.ts @@ -0,0 +1,327 @@ +"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<boolean> { + try { + const result = await oracleKnex.raw('SELECT 1 FROM DUAL'); + return result.rows && result.rows.length > 0; + } catch (error) { + logger.error('Oracle DB 연결 테스트 실패:', error); + return false; + } +} + +/** + * 지불조건 동기화 + */ +async function syncPaymentTerms(): Promise<void> { + 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.rows || []; + 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, + 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<void> { + 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.rows || []; + 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, + 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<void> { + 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.rows || []; + 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, + 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<void> { + 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<void> { + logger.info('수동 Procurement 동기화 시작'); + await syncAllProcurementTables(); + logger.success('수동 Procurement 동기화 완료'); +} + +/** + * Procurement 동기화 스케줄러 시작 + */ +export async function startProcurementSyncScheduler(): Promise<void> { + 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'); + } +}
\ No newline at end of file |
