From 6e25ab8da8a90a6d9bf40ccc83e36f119fb27568 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 1 Jul 2025 10:44:02 +0000 Subject: (김준회) 비활성화한 node-cron 진입점 (instrumentation.ts) 추가 및 NONSAP 동기화 개발건 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/admin/nonsap-sync/page.tsx | 267 +++++++++++++++++++++++++++++++++++ app/api/nonsap-sync/config/route.ts | 22 +++ app/api/nonsap-sync/cron/route.ts | 41 ++++++ app/api/nonsap-sync/health/route.ts | 63 +++++++++ app/api/nonsap-sync/status/route.ts | 22 +++ app/api/nonsap-sync/trigger/route.ts | 25 ++++ 6 files changed, 440 insertions(+) create mode 100644 app/[lng]/admin/nonsap-sync/page.tsx create mode 100644 app/api/nonsap-sync/config/route.ts create mode 100644 app/api/nonsap-sync/cron/route.ts create mode 100644 app/api/nonsap-sync/health/route.ts create mode 100644 app/api/nonsap-sync/status/route.ts create mode 100644 app/api/nonsap-sync/trigger/route.ts (limited to 'app') diff --git a/app/[lng]/admin/nonsap-sync/page.tsx b/app/[lng]/admin/nonsap-sync/page.tsx new file mode 100644 index 00000000..4cc78c27 --- /dev/null +++ b/app/[lng]/admin/nonsap-sync/page.tsx @@ -0,0 +1,267 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +interface SyncProgress { + tableName: string; + lastSyncDate: string; + currentPage: number; + totalProcessed: number; + status: 'running' | 'completed' | 'error' | 'skipped'; + lastError?: string; + syncType: 'full' | 'delta' | 'rebuild'; + startTime: number; + endTime?: number; + recordsSkipped?: number; +} + +interface SyncConfig { + pageSize: number; + batchSize: number; + maxWorkers: number; + deltaSyncEnabled: boolean; + cronSchedule: string; + autoRefreshInterval: number; + environment: string; +} + +export default function NONSAPSyncPage() { + const [syncProgress, setSyncProgress] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [lastUpdated, setLastUpdated] = useState(''); + const [syncConfig, setSyncConfig] = useState(null); + + // 설정 정보 조회 + const fetchSyncConfig = async () => { + try { + const response = await fetch('/api/nonsap-sync/config'); + const result = await response.json(); + + if (result.success) { + setSyncConfig(result.data); + } else { + console.error('Failed to fetch sync config:', result.error); + } + } catch (error) { + console.error('Error fetching sync config:', error); + } + }; + + // 동기화 상태 조회 + const fetchSyncStatus = async () => { + try { + const response = await fetch('/api/nonsap-sync/status'); + const result = await response.json(); + + if (result.success) { + setSyncProgress(result.data); + setLastUpdated(result.timestamp); + } else { + console.error('Failed to fetch sync status:', result.error); + } + } catch (error) { + console.error('Error fetching sync status:', error); + } + }; + + // 수동 동기화 트리거 + const triggerSync = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/nonsap-sync/trigger', { + method: 'POST' + }); + const result = await response.json(); + + if (result.success) { + // 상태 새로고침 + setTimeout(fetchSyncStatus, 2000); + } else { + alert('동기화 시작에 실패했습니다: ' + result.error); + } + } catch (error) { + console.error('Error triggering sync:', error); + alert('동기화 트리거 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + // 상태별 배지 색상 + const getStatusBadge = (status: string) => { + switch (status) { + case 'running': + return 실행 중; + case 'completed': + return 완료; + case 'error': + return 오류; + case 'skipped': + return 완료됨; + default: + return 알 수 없음; + } + }; + + // Cron 스케줄을 사용자 친화적으로 변환 + const formatCronSchedule = (cronSchedule: string) => { + if (cronSchedule === '0 */30 * * * *') { + return '30분마다'; + } + if (cronSchedule === '0 0 1 * * *') { + return '매일 새벽 1시'; + } + return cronSchedule; // 기본값으로 원본 반환 + }; + + + + // 동기화 타입별 배지 색상 + const getSyncTypeBadge = (syncType: string) => { + switch (syncType) { + case 'delta': + return 차분 동기화; + case 'full': + return 전체 동기화; + case 'rebuild': + return 삭제 후 재구성; + default: + return {syncType}; + } + }; + + // 초기 데이터 로드 + useEffect(() => { + fetchSyncConfig(); + fetchSyncStatus(); + }, []); + + // 자동 새로고침 + useEffect(() => { + if (!syncConfig) return; + + const interval = setInterval(fetchSyncStatus, syncConfig.autoRefreshInterval); + return () => clearInterval(interval); + }, [syncConfig]); + + return ( +
+
+
+

NONSAP 데이터 동기화

+

+ Oracle DB와 PostgreSQL 간의 데이터 동기화 상태를 모니터링합니다. +

+
+
+ + +
+
+ + {lastUpdated && ( + + +

+ 마지막 업데이트: {new Date(lastUpdated).toLocaleString('ko-KR')} +

+
+
+ )} + +
+ {syncProgress.map((progress) => ( + + +
+ {progress.tableName} +
+ {getSyncTypeBadge(progress.syncType)} + {getStatusBadge(progress.status)} +
+
+ + 마지막 동기화: {new Date(progress.lastSyncDate).toLocaleString('ko-KR')} + +
+ +
+
+ 현재 페이지: + {progress.currentPage} +
+
+ 처리된 레코드: + {progress.totalProcessed.toLocaleString()} +
+ {progress.recordsSkipped && progress.recordsSkipped > 0 && ( +
+ 스킵된 레코드: + {progress.recordsSkipped.toLocaleString()} +
+ )} + {progress.endTime && progress.startTime && ( +
+ 실행 시간: + + {((progress.endTime - progress.startTime) / 1000).toFixed(2)}초 + +
+ )} + {progress.lastError && ( +
+

+ 오류: {progress.lastError} +

+
+ )} +
+
+
+ ))} +
+ + {syncProgress.length === 0 && ( + + +

+ 동기화 정보가 없습니다. 동기화를 실행해보세요. +

+
+
+ )} + + {syncConfig && ( + + + 동기화 설정 + + 테이블별 최적 동기화 방식을 자동 선택하여 실행됩니다. + 멀티스레드로 병렬 처리하여 성능을 최적화합니다. + + + +
+

실행 환경: {syncConfig.environment}

+

동기화 방식: 테이블별 자동 선택 (차분/전체/재구성)

+

차분 동기화: {syncConfig.deltaSyncEnabled ? '활성화' : '비활성화'}

+

실행 주기: {formatCronSchedule(syncConfig.cronSchedule)}

+

페이지 크기: {syncConfig.pageSize.toLocaleString()}개 레코드

+

배치 크기: {syncConfig.batchSize.toLocaleString()}개 레코드

+

워커 수: {syncConfig.maxWorkers}개 (병렬 처리)

+

자동 새로고침: {(syncConfig.autoRefreshInterval / 1000)}초마다

+

동기화 대상: {syncProgress.length}개 테이블

+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/api/nonsap-sync/config/route.ts b/app/api/nonsap-sync/config/route.ts new file mode 100644 index 00000000..7441088b --- /dev/null +++ b/app/api/nonsap-sync/config/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server'; +import { getSyncConfigInfo } from '@/lib/nonsap-sync/sync-config'; + +export async function GET() { + try { + const config = getSyncConfigInfo(); + + return NextResponse.json({ + success: true, + data: config, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Failed to get sync config:', error); + + return NextResponse.json({ + success: false, + error: 'Failed to get sync config', + timestamp: new Date().toISOString() + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/nonsap-sync/cron/route.ts b/app/api/nonsap-sync/cron/route.ts new file mode 100644 index 00000000..80944280 --- /dev/null +++ b/app/api/nonsap-sync/cron/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + try { + // 인증 확인 (선택적) + const authToken = process.env.CRON_AUTH_TOKEN; + if (authToken) { + const providedToken = new URL(request.url).searchParams.get('token'); + if (providedToken !== authToken) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + } + + // 동적 import로 cron 스케줄러 호출 + const { triggerEnhancedSync } = await import('../../../../lib/nonsap-sync/enhanced-sync-service'); + + // 백그라운드에서 동기화 실행 + triggerEnhancedSync().catch(error => { + console.error('Cron sync failed:', error); + }); + + return NextResponse.json({ + success: true, + message: 'NONSAP sync triggered via cron endpoint', + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Cron endpoint error:', error); + + return NextResponse.json({ + success: false, + error: 'Failed to trigger sync', + timestamp: new Date().toISOString() + }, { status: 500 }); + } +} + +export async function POST() { + // POST 요청도 동일하게 처리 + return GET(); +} \ No newline at end of file diff --git a/app/api/nonsap-sync/health/route.ts b/app/api/nonsap-sync/health/route.ts new file mode 100644 index 00000000..6bfa4ffb --- /dev/null +++ b/app/api/nonsap-sync/health/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + try { + // Oracle DB 연결 테스트 + let oracleStatus = 'unknown'; + let oracleError = null; + + try { + const { oracleKnex } = await import('@/lib/oracle-db/db'); + const result = await oracleKnex.raw('SELECT 1 FROM DUAL'); + oracleStatus = result ? 'connected' : 'disconnected'; + } catch (error) { + oracleStatus = 'disconnected'; + oracleError = error instanceof Error ? error.message : String(error); + } + + // PostgreSQL 연결 테스트 + let postgresStatus = 'unknown'; + let postgresError = null; + + try { + const db = await import('@/db/db'); + await db.default.execute('SELECT 1'); + postgresStatus = 'connected'; + } catch (error) { + postgresStatus = 'disconnected'; + postgresError = error instanceof Error ? error.message : String(error); + } + + // 전체 상태 판단 + const overallStatus = oracleStatus === 'connected' && postgresStatus === 'connected' + ? 'healthy' + : 'unhealthy'; + + return NextResponse.json({ + status: overallStatus, + timestamp: new Date().toISOString(), + databases: { + oracle: { + status: oracleStatus, + error: oracleError + }, + postgres: { + status: postgresStatus, + error: postgresError + } + }, + environment: { + nodeEnv: process.env.NODE_ENV, + runtime: process.env.NEXT_RUNTIME || 'nodejs' + } + }); + } catch (error) { + console.error('Health check error:', error); + + return NextResponse.json({ + status: 'error', + timestamp: new Date().toISOString(), + error: error instanceof Error ? error.message : String(error) + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/nonsap-sync/status/route.ts b/app/api/nonsap-sync/status/route.ts new file mode 100644 index 00000000..0a847904 --- /dev/null +++ b/app/api/nonsap-sync/status/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server'; +import { getSyncProgressEnhanced } from '../../../../lib/nonsap-sync/enhanced-sync-service'; + +export async function GET() { + try { + const progress = await getSyncProgressEnhanced(); + + return NextResponse.json({ + success: true, + data: progress, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Error getting sync progress:', error); + + return NextResponse.json({ + success: false, + error: 'Failed to get sync progress', + timestamp: new Date().toISOString() + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/nonsap-sync/trigger/route.ts b/app/api/nonsap-sync/trigger/route.ts new file mode 100644 index 00000000..fbad7c4a --- /dev/null +++ b/app/api/nonsap-sync/trigger/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import { triggerEnhancedSync } from '../../../../lib/nonsap-sync/enhanced-sync-service'; + +export async function POST() { + try { + // 수동 동기화 트리거 (백그라운드에서 실행) + triggerEnhancedSync().catch(error => { + console.error('Manual sync failed:', error); + }); + + return NextResponse.json({ + success: true, + message: 'Manual sync triggered successfully', + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Error triggering manual sync:', error); + + return NextResponse.json({ + success: false, + error: 'Failed to trigger manual sync', + timestamp: new Date().toISOString() + }, { status: 500 }); + } +} \ No newline at end of file -- cgit v1.2.3