release: 2026-03-25 (2건 커밋) #19
@ -4,6 +4,8 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026-03-25]
|
||||
|
||||
### 추가
|
||||
- 동기화 현황 메뉴 추가: 도메인 탭 + 테이블 아코디언 + 인라인 데이터 조회 (#1)
|
||||
- SyncStatusService: batch_flag 기반 테이블별 N/P/S 집계 (병렬 조회)
|
||||
@ -11,6 +13,8 @@
|
||||
- abandon/stale 실행 관리 엔드포인트 구현 (#7)
|
||||
|
||||
### 변경
|
||||
- 동기화 현황 데이터 로딩 속도 개선: 쿼리 합침 + Caffeine 캐시(10분 TTL) + 인덱스 DDL (#9)
|
||||
- 30초 폴링 제거 → 수동 새로고침 방식으로 변경
|
||||
- 동기화 현황 노출 테이블 목록 정리: ship-001/ship-002 제거, source-target 키 매핑 정리 (#11)
|
||||
- BaseSyncReader 추출: 49개 Reader 공통 로직 통합, 1 chunk = 1 job_execution_id 보장
|
||||
- chunk 경계 제어를 GroupByExecutionIdPolicy에서 Reader 자체 제어로 변경
|
||||
|
||||
163
docs/create-sync-indexes.sql
Normal file
163
docs/create-sync-indexes.sql
Normal file
@ -0,0 +1,163 @@
|
||||
-- ============================================================
|
||||
-- 동기화 현황 조회 성능 개선 인덱스
|
||||
-- 대상 스키마: std_snp_data
|
||||
--
|
||||
-- 쿼리 패턴:
|
||||
-- SELECT batch_flag, COUNT(*), MAX(CASE WHEN batch_flag='S' THEN crt_dt END)
|
||||
-- FROM table a
|
||||
-- INNER JOIN batch_job_execution b ON a.job_execution_id = b.job_execution_id AND b.status = 'COMPLETED'
|
||||
-- WHERE a.batch_flag IN ('N', 'P', 'S')
|
||||
-- GROUP BY a.batch_flag
|
||||
--
|
||||
-- 인덱스 전략:
|
||||
-- 1. 각 소스 테이블: (batch_flag, job_execution_id) 복합 인덱스
|
||||
-- → batch_flag 필터링 + JOIN 조건을 인덱스만으로 처리
|
||||
-- → crt_dt INCLUDE로 커버링 인덱스 (Index Only Scan 가능)
|
||||
-- 2. batch_job_execution: (job_execution_id, status) 복합 인덱스
|
||||
-- → JOIN 조건을 인덱스만으로 처리
|
||||
-- ============================================================
|
||||
|
||||
-- batch_job_execution 테이블 (1회만 실행)
|
||||
CREATE INDEX IF NOT EXISTS idx_batch_job_execution_status
|
||||
ON std_snp_data.batch_job_execution (job_execution_id, status);
|
||||
|
||||
-- Ship 테이블
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_info_mst_sync ON std_snp_data.tb_ship_info_mst (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_add_info_sync ON std_snp_data.tb_ship_add_info (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_bbctr_hstry_sync ON std_snp_data.tb_ship_bbctr_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_idntf_info_hstry_sync ON std_snp_data.tb_ship_idntf_info_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_clfic_hstry_sync ON std_snp_data.tb_ship_clfic_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_company_rel_sync ON std_snp_data.tb_ship_company_rel (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_crew_list_sync ON std_snp_data.tb_ship_crew_list (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_dark_actv_idnty_sync ON std_snp_data.tb_ship_dark_actv_idnty (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_country_hstry_sync ON std_snp_data.tb_ship_country_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_group_revn_ownr_hstry_sync ON std_snp_data.tb_ship_group_revn_ownr_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_ice_grd_sync ON std_snp_data.tb_ship_ice_grd (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_nm_chg_hstry_sync ON std_snp_data.tb_ship_nm_chg_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_operator_hstry_sync ON std_snp_data.tb_ship_operator_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_ownr_hstry_sync ON std_snp_data.tb_ship_ownr_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_prtc_rpn_hstry_sync ON std_snp_data.tb_ship_prtc_rpn_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_sfty_mng_evdc_hstry_sync ON std_snp_data.tb_ship_sfty_mng_evdc_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_mng_company_hstry_sync ON std_snp_data.tb_ship_mng_company_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_sstrvsl_rel_sync ON std_snp_data.tb_ship_sstrvsl_rel (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_spc_fetr_sync ON std_snp_data.tb_ship_spc_fetr (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_status_hstry_sync ON std_snp_data.tb_ship_status_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_cargo_capacity_sync ON std_snp_data.tb_ship_cargo_capacity (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_inspection_ymd_sync ON std_snp_data.tb_ship_inspection_ymd (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_inspection_ymd_hstry_sync ON std_snp_data.tb_ship_inspection_ymd_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_tech_mng_company_hstry_sync ON std_snp_data.tb_ship_tech_mng_company_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_thrstr_info_sync ON std_snp_data.tb_ship_thrstr_info (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
|
||||
-- Company
|
||||
CREATE INDEX IF NOT EXISTS idx_company_dtl_info_sync ON std_snp_data.tb_company_dtl_info (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
|
||||
-- Event
|
||||
CREATE INDEX IF NOT EXISTS idx_event_mst_sync ON std_snp_data.tb_event_mst (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_cargo_sync ON std_snp_data.tb_event_cargo (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_humn_acdnt_sync ON std_snp_data.tb_event_humn_acdnt (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_rel_sync ON std_snp_data.tb_event_rel (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
|
||||
-- Facility
|
||||
CREATE INDEX IF NOT EXISTS idx_port_facility_info_sync ON std_snp_data.tb_port_facility_info (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
|
||||
-- PSC
|
||||
CREATE INDEX IF NOT EXISTS idx_psc_mst_sync ON std_snp_data.tb_psc_mst (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_psc_defect_sync ON std_snp_data.tb_psc_defect (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_psc_oa_certf_sync ON std_snp_data.tb_psc_oa_certf (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
|
||||
-- Movements
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_anchrgcall_hstry_sync ON std_snp_data.tb_ship_anchrgcall_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_berthcall_hstry_sync ON std_snp_data.tb_ship_berthcall_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_now_status_hstry_sync ON std_snp_data.tb_ship_now_status_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_dest_hstry_sync ON std_snp_data.tb_ship_dest_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_prtcll_hstry_sync ON std_snp_data.tb_ship_prtcll_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_sts_opert_hstry_sync ON std_snp_data.tb_ship_sts_opert_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_teminalcall_hstry_sync ON std_snp_data.tb_ship_teminalcall_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_trnst_hstry_sync ON std_snp_data.tb_ship_trnst_hstry (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
|
||||
-- Code
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_type_cd_sync ON std_snp_data.tb_ship_type_cd (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_country_cd_sync ON std_snp_data.tb_ship_country_cd (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
|
||||
-- Risk & Compliance
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_risk_info_sync ON std_snp_data.tb_ship_risk_info (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_compliance_info_sync ON std_snp_data.tb_ship_compliance_info (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
CREATE INDEX IF NOT EXISTS idx_company_compliance_info_sync ON std_snp_data.tb_company_compliance_info (batch_flag, job_execution_id) INCLUDE (crt_dt);
|
||||
|
||||
-- ============================================================
|
||||
-- std_snp_svc (타겟 스키마) - 데이터 미리보기 조회 성능 개선
|
||||
--
|
||||
-- 쿼리 패턴:
|
||||
-- SELECT * FROM std_snp_svc.table ORDER BY crt_dt DESC NULLS LAST LIMIT 20
|
||||
--
|
||||
-- 인덱스 전략:
|
||||
-- crt_dt DESC 인덱스 → ORDER BY + LIMIT을 Index Scan으로 처리
|
||||
-- ============================================================
|
||||
|
||||
-- Ship
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_info_mst_crt ON std_snp_svc.tb_ship_info_mst (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_add_info_crt ON std_snp_svc.tb_ship_add_info (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_bbctr_hstry_crt ON std_snp_svc.tb_ship_bbctr_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_idntf_info_hstry_crt ON std_snp_svc.tb_ship_idntf_info_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_clfic_hstry_crt ON std_snp_svc.tb_ship_clfic_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_company_rel_crt ON std_snp_svc.tb_ship_company_rel (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_crew_list_crt ON std_snp_svc.tb_ship_crew_list (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_dark_actv_idnty_crt ON std_snp_svc.tb_ship_dark_actv_idnty (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_country_hstry_crt ON std_snp_svc.tb_ship_country_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_group_revn_ownr_hstry_crt ON std_snp_svc.tb_ship_group_revn_ownr_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_ice_grd_crt ON std_snp_svc.tb_ship_ice_grd (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_nm_chg_hstry_crt ON std_snp_svc.tb_ship_nm_chg_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_operator_hstry_crt ON std_snp_svc.tb_ship_operator_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_ownr_hstry_crt ON std_snp_svc.tb_ship_ownr_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_prtc_rpn_hstry_crt ON std_snp_svc.tb_ship_prtc_rpn_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_sfty_mng_evdc_hstry_crt ON std_snp_svc.tb_ship_sfty_mng_evdc_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_mng_company_hstry_crt ON std_snp_svc.tb_ship_mng_company_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_sstrvsl_rel_crt ON std_snp_svc.tb_ship_sstrvsl_rel (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_spc_fetr_crt ON std_snp_svc.tb_ship_spc_fetr (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_status_hstry_crt ON std_snp_svc.tb_ship_status_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_cargo_capacity_crt ON std_snp_svc.tb_ship_cargo_capacity (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_inspection_ymd_crt ON std_snp_svc.tb_ship_inspection_ymd (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_inspection_ymd_hstry_crt ON std_snp_svc.tb_ship_inspection_ymd_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_tech_mng_company_hstry_crt ON std_snp_svc.tb_ship_tech_mng_company_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_thrstr_info_crt ON std_snp_svc.tb_ship_thrstr_info (crt_dt DESC);
|
||||
|
||||
-- Company
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_company_dtl_info_crt ON std_snp_svc.tb_company_dtl_info (crt_dt DESC);
|
||||
|
||||
-- Event
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_event_mst_crt ON std_snp_svc.tb_event_mst (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_event_cargo_crt ON std_snp_svc.tb_event_cargo (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_event_humn_acdnt_crt ON std_snp_svc.tb_event_humn_acdnt (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_event_rel_crt ON std_snp_svc.tb_event_rel (crt_dt DESC);
|
||||
|
||||
-- Facility
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_port_facility_info_crt ON std_snp_svc.tb_port_facility_info (crt_dt DESC);
|
||||
|
||||
-- PSC
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_psc_mst_crt ON std_snp_svc.tb_psc_mst (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_psc_defect_crt ON std_snp_svc.tb_psc_defect (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_psc_oa_certf_crt ON std_snp_svc.tb_psc_oa_certf (crt_dt DESC);
|
||||
|
||||
-- Movements
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_anchrgcall_hstry_crt ON std_snp_svc.tb_ship_anchrgcall_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_berthcall_hstry_crt ON std_snp_svc.tb_ship_berthcall_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_now_status_hstry_crt ON std_snp_svc.tb_ship_now_status_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_dest_hstry_crt ON std_snp_svc.tb_ship_dest_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_prtcll_hstry_crt ON std_snp_svc.tb_ship_prtcll_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_sts_opert_hstry_crt ON std_snp_svc.tb_ship_sts_opert_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_teminalcall_hstry_crt ON std_snp_svc.tb_ship_teminalcall_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_trnst_hstry_crt ON std_snp_svc.tb_ship_trnst_hstry (crt_dt DESC);
|
||||
|
||||
-- Code
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_type_cd_crt ON std_snp_svc.tb_ship_type_cd (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_country_cd_crt ON std_snp_svc.tb_ship_country_cd (crt_dt DESC);
|
||||
|
||||
-- Risk & Compliance
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_risk_info_crt ON std_snp_svc.tb_ship_risk_info (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_risk_hstry_crt ON std_snp_svc.tb_ship_risk_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_compliance_info_crt ON std_snp_svc.tb_ship_compliance_info (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_compliance_hstry_crt ON std_snp_svc.tb_ship_compliance_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_ship_compliance_info_hstry_crt ON std_snp_svc.tb_ship_compliance_info_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_company_compliance_info_crt ON std_snp_svc.tb_company_compliance_info (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_company_compliance_hstry_crt ON std_snp_svc.tb_company_compliance_hstry (crt_dt DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_svc_company_compliance_info_hstry_crt ON std_snp_svc.tb_company_compliance_info_hstry (crt_dt DESC);
|
||||
@ -453,8 +453,8 @@ export const batchApi = {
|
||||
`${BASE}/failed-records/reset-retry`, { ids }),
|
||||
|
||||
// Sync Status
|
||||
getSyncStatus: () =>
|
||||
fetchJson<SyncStatusResponse>(`${BASE}/sync-status`),
|
||||
getSyncStatus: (refresh = false) =>
|
||||
fetchJson<SyncStatusResponse>(`${BASE}/sync-status?refresh=${refresh}`),
|
||||
|
||||
getSyncDataPreview: (tableKey: string, limit = 10) =>
|
||||
fetchJson<SyncDataPreviewResponse>(`${BASE}/sync-status/${tableKey}/preview?limit=${limit}`),
|
||||
|
||||
@ -3,11 +3,11 @@ import { useThemeContext } from '../contexts/ThemeContext';
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: '대시보드', icon: '📊' },
|
||||
{ path: '/sync-status', label: '동기화 현황', icon: '🔄' },
|
||||
{ path: '/executions', label: '실행 이력', icon: '📋' },
|
||||
{ path: '/jobs', label: '작업', icon: '⚙️' },
|
||||
{ path: '/schedules', label: '스케줄', icon: '🕐' },
|
||||
{ path: '/schedule-timeline', label: '타임라인', icon: '📅' },
|
||||
{ path: '/sync-status', label: '동기화 현황', icon: '🔄' },
|
||||
];
|
||||
|
||||
export default function Navbar() {
|
||||
|
||||
@ -5,15 +5,12 @@ import {
|
||||
type SyncTableStatus,
|
||||
type SyncDataPreviewResponse,
|
||||
} from '../api/batchApi';
|
||||
import { usePoller } from '../hooks/usePoller';
|
||||
import { useToastContext } from '../contexts/ToastContext';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import EmptyState from '../components/EmptyState';
|
||||
import ConfirmModal from '../components/ConfirmModal';
|
||||
import GuideModal, { HelpButton } from '../components/GuideModal';
|
||||
|
||||
const POLLING_INTERVAL = 30000;
|
||||
|
||||
const DOMAIN_ICONS: Record<string, string> = {
|
||||
ship: '🚢',
|
||||
company: '🏢',
|
||||
@ -56,19 +53,27 @@ export default function SyncStatus() {
|
||||
const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
|
||||
const [resetting, setResetting] = useState(false);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const loadData = useCallback(async (refresh = false) => {
|
||||
try {
|
||||
const result = await batchApi.getSyncStatus();
|
||||
const result = await batchApi.getSyncStatus(refresh);
|
||||
setData(result);
|
||||
} catch {
|
||||
if (loading) showToast('동기화 현황 조회 실패', 'error');
|
||||
showToast('동기화 현황 조회 실패', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
usePoller(loadData, POLLING_INTERVAL);
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
loadData(true);
|
||||
};
|
||||
|
||||
const toggleAccordion = (tableKey: string) => {
|
||||
setExpandedTable((prev) => (prev === tableKey ? '' : tableKey));
|
||||
@ -106,7 +111,21 @@ export default function SyncStatus() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold text-wing-text">동기화 현황</h1>
|
||||
<HelpButton onClick={() => setGuideOpen(true)} />
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg border border-wing-border transition-colors
|
||||
${refreshing
|
||||
? 'text-wing-muted cursor-not-allowed'
|
||||
: 'text-wing-text hover:bg-wing-hover'
|
||||
}`}
|
||||
>
|
||||
<span className={refreshing ? 'animate-spin' : ''}>↻</span>
|
||||
{refreshing ? '조회 중...' : '새로고침'}
|
||||
</button>
|
||||
<HelpButton onClick={() => setGuideOpen(true)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Domain Tabs ── */}
|
||||
|
||||
10
pom.xml
10
pom.xml
@ -106,6 +106,16 @@
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Cache -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
24
src/main/java/com/snp/batch/global/config/CacheConfig.java
Normal file
24
src/main/java/com/snp/batch/global/config/CacheConfig.java
Normal file
@ -0,0 +1,24 @@
|
||||
package com.snp.batch.global.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
CaffeineCacheManager manager = new CaffeineCacheManager("syncStatus", "syncDataPreview", "syncStuckRecords");
|
||||
manager.setCaffeine(Caffeine.newBuilder()
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.maximumSize(100));
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
@ -388,9 +388,13 @@ public class BatchController {
|
||||
|
||||
@Operation(summary = "동기화 현황 조회", description = "전체 테이블의 batch_flag 기반 동기화 현황을 조회합니다")
|
||||
@GetMapping("/sync-status")
|
||||
public ResponseEntity<SyncStatusResponse> getSyncStatus() {
|
||||
log.info("Received request to get sync status");
|
||||
public ResponseEntity<SyncStatusResponse> getSyncStatus(
|
||||
@Parameter(description = "캐시 무시 여부") @RequestParam(defaultValue = "false") boolean refresh) {
|
||||
log.info("Received request to get sync status (refresh: {})", refresh);
|
||||
try {
|
||||
if (refresh) {
|
||||
syncStatusService.evictAllCaches();
|
||||
}
|
||||
SyncStatusResponse status = syncStatusService.getSyncStatus();
|
||||
return ResponseEntity.ok(status);
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -8,6 +8,9 @@ import com.snp.batch.global.dto.SyncStatusResponse.SyncStatusSummary;
|
||||
import com.snp.batch.global.dto.SyncStatusResponse.SyncTableStatus;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cache.annotation.Caching;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -63,8 +66,9 @@ public class SyncStatusService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 동기화 현황 조회
|
||||
* 전체 동기화 현황 조회 (10분 TTL 캐싱)
|
||||
*/
|
||||
@Cacheable(value = "syncStatus", key = "'all'")
|
||||
public SyncStatusResponse getSyncStatus() {
|
||||
// 테이블을 병렬 조회 (HikariCP pool=10 기준 동시 10개)
|
||||
ExecutorService executor = Executors.newFixedThreadPool(
|
||||
@ -136,8 +140,9 @@ public class SyncStatusService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테이블의 최근 동기화 데이터 미리보기
|
||||
* 특정 테이블의 최근 동기화 데이터 미리보기 (10분 TTL 캐싱)
|
||||
*/
|
||||
@Cacheable(value = "syncDataPreview", key = "#tableKey + '-' + #limit")
|
||||
public SyncDataPreviewResponse getDataPreview(String tableKey, int limit) {
|
||||
String targetTable = targetTables.get(tableKey);
|
||||
if (targetTable == null) {
|
||||
@ -147,7 +152,7 @@ public class SyncStatusService {
|
||||
String countSql = "SELECT COUNT(*) FROM %s.%s".formatted(targetSchema, targetTable);
|
||||
Long totalCount = businessJdbc.queryForObject(countSql, Long.class);
|
||||
|
||||
String sql = "SELECT * FROM %s.%s ORDER BY mdfcn_dt DESC NULLS LAST LIMIT %d"
|
||||
String sql = "SELECT * FROM %s.%s ORDER BY crt_dt DESC NULLS LAST LIMIT %d"
|
||||
.formatted(targetSchema, targetTable, limit);
|
||||
|
||||
List<Map<String, Object>> rows = businessJdbc.queryForList(sql);
|
||||
@ -167,8 +172,9 @@ public class SyncStatusService {
|
||||
}
|
||||
|
||||
/**
|
||||
* P 상태 고착 레코드 조회
|
||||
* P 상태 고착 레코드 조회 (10분 TTL 캐싱)
|
||||
*/
|
||||
@Cacheable(value = "syncStuckRecords", key = "#tableKey + '-' + #limit")
|
||||
public SyncDataPreviewResponse getStuckRecords(String tableKey, int limit) {
|
||||
String sourceTable = sourceTables.get(tableKey);
|
||||
if (sourceTable == null) {
|
||||
@ -192,7 +198,7 @@ public class SyncStatusService {
|
||||
ON a.job_execution_id = b.job_execution_id
|
||||
AND b.status = 'COMPLETED'
|
||||
WHERE a.batch_flag = 'P'
|
||||
ORDER BY a.mdfcn_dt DESC NULLS LAST
|
||||
ORDER BY a.crt_dt DESC NULLS LAST
|
||||
LIMIT %d
|
||||
""".formatted(sourceSchema, sourceTable, sourceSchema, limit);
|
||||
|
||||
@ -213,8 +219,24 @@ public class SyncStatusService {
|
||||
}
|
||||
|
||||
/**
|
||||
* P 상태 고착 레코드를 N으로 리셋
|
||||
* 전체 캐시 무효화
|
||||
*/
|
||||
@Caching(evict = {
|
||||
@CacheEvict(value = "syncStatus", allEntries = true),
|
||||
@CacheEvict(value = "syncDataPreview", allEntries = true),
|
||||
@CacheEvict(value = "syncStuckRecords", allEntries = true)
|
||||
})
|
||||
public void evictAllCaches() {
|
||||
log.info("동기화 현황 캐시 전체 무효화");
|
||||
}
|
||||
|
||||
/**
|
||||
* P 상태 고착 레코드를 N으로 리셋 (관련 캐시 무효화)
|
||||
*/
|
||||
@Caching(evict = {
|
||||
@CacheEvict(value = "syncStatus", allEntries = true),
|
||||
@CacheEvict(value = "syncStuckRecords", allEntries = true)
|
||||
})
|
||||
public int resetStuckRecords(String tableKey) {
|
||||
String sourceTable = sourceTables.get(tableKey);
|
||||
if (sourceTable == null) {
|
||||
@ -235,10 +257,11 @@ public class SyncStatusService {
|
||||
}
|
||||
|
||||
private SyncTableStatus queryTableStatus(String tableKey, String sourceTable, String targetTable) {
|
||||
// batch_job_execution.status = 'COMPLETED'인 데이터만 집계
|
||||
// (수집/적재가 완전히 완료된 데이터만 동기화 대상)
|
||||
// batch_flag 집계 + 최근 동기화 시간을 1개 쿼리로 조회
|
||||
String sql = """
|
||||
SELECT a.batch_flag, COUNT(*) AS cnt
|
||||
SELECT a.batch_flag,
|
||||
COUNT(*) AS cnt,
|
||||
MAX(CASE WHEN a.batch_flag = 'S' THEN a.crt_dt END) AS last_sync
|
||||
FROM %s.%s a
|
||||
INNER JOIN %s.batch_job_execution b
|
||||
ON a.job_execution_id = b.job_execution_id
|
||||
@ -251,28 +274,17 @@ public class SyncStatusService {
|
||||
counts.put("N", 0L);
|
||||
counts.put("P", 0L);
|
||||
counts.put("S", 0L);
|
||||
final String[] lastSyncTime = {null};
|
||||
|
||||
businessJdbc.query(sql, rs -> {
|
||||
counts.put(rs.getString("batch_flag"), rs.getLong("cnt"));
|
||||
String flag = rs.getString("batch_flag");
|
||||
counts.put(flag, rs.getLong("cnt"));
|
||||
String lastSync = rs.getString("last_sync");
|
||||
if (lastSync != null) {
|
||||
lastSyncTime[0] = lastSync;
|
||||
}
|
||||
});
|
||||
|
||||
// 최근 동기화 시간 (COMPLETED된 job의 batch_flag='S'인 가장 최근 mdfcn_dt)
|
||||
String lastSyncSql = """
|
||||
SELECT MAX(a.mdfcn_dt)
|
||||
FROM %s.%s a
|
||||
INNER JOIN %s.batch_job_execution b
|
||||
ON a.job_execution_id = b.job_execution_id
|
||||
AND b.status = 'COMPLETED'
|
||||
WHERE a.batch_flag = 'S'
|
||||
""".formatted(sourceSchema, sourceTable, sourceSchema);
|
||||
|
||||
String lastSyncTime = null;
|
||||
try {
|
||||
lastSyncTime = businessJdbc.queryForObject(lastSyncSql, String.class);
|
||||
} catch (Exception e) {
|
||||
log.trace("최근 동기화 시간 조회 실패: {}", tableKey);
|
||||
}
|
||||
|
||||
boolean stuck = counts.get("P") > 0;
|
||||
|
||||
return SyncTableStatus.builder()
|
||||
@ -283,7 +295,7 @@ public class SyncStatusService {
|
||||
.pendingCount(counts.get("N"))
|
||||
.processingCount(counts.get("P"))
|
||||
.completedCount(counts.get("S"))
|
||||
.lastSyncTime(lastSyncTime)
|
||||
.lastSyncTime(lastSyncTime[0])
|
||||
.stuck(stuck)
|
||||
.build();
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user