summaryrefslogtreecommitdiff
path: root/lib/nonsap-sync
diff options
context:
space:
mode:
Diffstat (limited to 'lib/nonsap-sync')
-rw-r--r--lib/nonsap-sync/procurement-sync-service.ts327
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