diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b2a3839..7151cc8 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,26 +1,56 @@
-import { lazy } from 'react'
+import { lazy, Suspense, Component, type ReactNode } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { ThemeProvider } from './contexts/ThemeContext.tsx'
import { I18nProvider } from './i18n/I18nContext.tsx'
import AppLayout from './components/layout/AppLayout.tsx'
+import LoadingSpinner from './components/common/LoadingSpinner.tsx'
const Dashboard = lazy(() => import('./pages/Dashboard.tsx'))
const JobMonitor = lazy(() => import('./pages/JobMonitor.tsx'))
const BASE_URL = import.meta.env.VITE_BASE_URL || '/signal-batch'
+/* React Error Boundary — 페이지 렌더링 에러 시 전체 앱 crash 방지 */
+class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
+ state: { error: Error | null } = { error: null }
+
+ static getDerivedStateFromError(error: Error) {
+ return { error }
+ }
+
+ render() {
+ if (this.state.error) {
+ return (
+
+
Rendering Error
+
{this.state.error.message}
+
+
+ )
+ }
+ return this.props.children
+ }
+}
+
export default function App() {
return (
-
- }>
- } />
- } />
- {/* Phase 2+ 페이지 추가 예정 */}
-
-
+
+
+ }>
+ }>} />
+ }>} />
+ {/* Phase 2+ 페이지 추가 예정 */}
+
+
+
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index c19fb78..f8b353a 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -107,9 +107,9 @@ export default function Dashboard() {
{delay ? (
- {delay.delayMinutes}
+ {delay.delayMinutes ?? 0}
{t('dashboard.delayMin')}
-
+
@@ -166,7 +166,7 @@ export default function Dashboard() {
{t('dashboard.dbConn')}
-
{metrics.processing.recordsPerSecond.toFixed(0)}
+
{(metrics.processing?.recordsPerSecond ?? 0).toFixed(0)}
{t('dashboard.recordsSec')}
diff --git a/frontend/src/utils/formatters.ts b/frontend/src/utils/formatters.ts
index f7cc915..3ab8fc9 100644
--- a/frontend/src/utils/formatters.ts
+++ b/frontend/src/utils/formatters.ts
@@ -1,5 +1,6 @@
/** 숫자를 천 단위 콤마 포맷 */
-export function formatNumber(n: number): string {
+export function formatNumber(n: number | null | undefined): string {
+ if (n == null) return '0'
return n.toLocaleString('ko-KR')
}
diff --git a/src/main/java/gc/mda/signal_batch/batch/reader/FiveMinTrackCache.java b/src/main/java/gc/mda/signal_batch/batch/reader/FiveMinTrackCache.java
index 769af5f..053739c 100644
--- a/src/main/java/gc/mda/signal_batch/batch/reader/FiveMinTrackCache.java
+++ b/src/main/java/gc/mda/signal_batch/batch/reader/FiveMinTrackCache.java
@@ -123,6 +123,17 @@ public class FiveMinTrackCache {
stats.missCount());
}
+ public Map
getStatsMap() {
+ var stats = cache.stats();
+ return Map.of(
+ "size", cache.estimatedSize(),
+ "maxSize", (long) maxSize,
+ "hitCount", stats.hitCount(),
+ "missCount", stats.missCount(),
+ "hitRate", stats.hitRate() * 100
+ );
+ }
+
private String buildKey(String mmsi, LocalDateTime timeBucket) {
return mmsi + "::" + timeBucket.format(KEY_FORMATTER);
}
diff --git a/src/main/java/gc/mda/signal_batch/batch/reader/HourlyTrackCache.java b/src/main/java/gc/mda/signal_batch/batch/reader/HourlyTrackCache.java
index a41aae8..4731606 100644
--- a/src/main/java/gc/mda/signal_batch/batch/reader/HourlyTrackCache.java
+++ b/src/main/java/gc/mda/signal_batch/batch/reader/HourlyTrackCache.java
@@ -122,6 +122,17 @@ public class HourlyTrackCache {
stats.missCount());
}
+ public Map getStatsMap() {
+ var stats = cache.stats();
+ return Map.of(
+ "size", cache.estimatedSize(),
+ "maxSize", (long) maxSize,
+ "hitCount", stats.hitCount(),
+ "missCount", stats.missCount(),
+ "hitRate", stats.hitRate() * 100
+ );
+ }
+
private String buildKey(String mmsi, LocalDateTime timeBucket) {
return mmsi + "::" + timeBucket.format(KEY_FORMATTER);
}
diff --git a/src/main/java/gc/mda/signal_batch/monitoring/controller/CacheMonitoringController.java b/src/main/java/gc/mda/signal_batch/monitoring/controller/CacheMonitoringController.java
index d9bab54..38a9b02 100644
--- a/src/main/java/gc/mda/signal_batch/monitoring/controller/CacheMonitoringController.java
+++ b/src/main/java/gc/mda/signal_batch/monitoring/controller/CacheMonitoringController.java
@@ -1,85 +1,128 @@
package gc.mda.signal_batch.monitoring.controller;
+import gc.mda.signal_batch.batch.reader.AisTargetCacheManager;
+import gc.mda.signal_batch.batch.reader.FiveMinTrackCache;
+import gc.mda.signal_batch.batch.reader.HourlyTrackCache;
import gc.mda.signal_batch.domain.vessel.service.VesselLatestPositionCache;
+import gc.mda.signal_batch.global.websocket.service.DailyTrackCacheManager;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
/**
- * 캐시 모니터링 컨트롤러
+ * 다계층 캐시 모니터링 컨트롤러
+ *
+ * L1(5분) + L2(시간) + L3(일일) + AIS Target + Latest Position 전체 캐시 통합 통계
*
* 엔드포인트:
- * - GET /api/monitoring/cache/stats : 캐시 통계 조회
- * - POST /api/monitoring/cache/clear : 캐시 강제 초기화
+ * - GET /api/monitoring/cache/stats : 전체 캐시 집계 통계 (Dashboard용)
+ * - GET /api/monitoring/cache/details : 계층별 상세 통계
+ * - GET /api/monitoring/cache/health : 캐시 헬스체크
*/
@Slf4j
@RestController
@RequestMapping("/api/monitoring/cache")
-@RequiredArgsConstructor
-@Tag(name = "Cache Monitoring", description = "캐시 모니터링 API")
-@ConditionalOnProperty(name = "vessel.cache.latest-position.enabled", havingValue = "true")
+@Tag(name = "Cache Monitoring", description = "다계층 캐시 모니터링 API")
public class CacheMonitoringController {
@Autowired(required = false)
- private VesselLatestPositionCache cache;
+ private FiveMinTrackCache fiveMinTrackCache;
+
+ @Autowired(required = false)
+ private HourlyTrackCache hourlyTrackCache;
+
+ @Autowired(required = false)
+ private DailyTrackCacheManager dailyTrackCacheManager;
+
+ @Autowired(required = false)
+ private AisTargetCacheManager aisTargetCacheManager;
+
+ @Autowired(required = false)
+ private VesselLatestPositionCache latestPositionCache;
/**
- * 캐시 통계 조회
+ * 캐시 통계 조회 (Dashboard 표시용 — 전체 캐시 집계)
*/
@GetMapping("/stats")
- @Operation(summary = "캐시 통계 조회", description = "Vessel Latest Position 캐시의 통계 정보를 조회합니다.")
+ @Operation(summary = "캐시 통계 조회", description = "L1/L2/L3 + AIS Target + Latest Position 전체 캐시의 집계 통계를 조회합니다")
public ResponseEntity