diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md
index 97a4a74..d227cf9 100644
--- a/docs/RELEASE-NOTES.md
+++ b/docs/RELEASE-NOTES.md
@@ -4,6 +4,13 @@
## [Unreleased]
+### 추가
+- 다중구역/STS API 최적화 — AreaSearch/VesselContact 동시성·메모리 관리 통합, 순차 통과 SQL 동적 N-구역(2~10) 확장, chnPrmShipOnly 파라미터 추가
+
+### 변경
+- 성능 최적화 — ArrayList 사전 할당, JTS Coordinate 재사용, equirectangular 거리 근사, stream→단일 루프 전환
+- DataPipeline 대시보드 차트 시각화 개선
+
## [2026-03-10.2]
### 추가
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index c434214..e8b52ab 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -207,6 +207,7 @@ export interface QueryMetricRow {
simplify_ms: number
cache_hit_days: number
db_query_days: number
+ client_ip: string | null
}
export interface QueryMetricsPage {
diff --git a/frontend/src/i18n/en.ts b/frontend/src/i18n/en.ts
index e0f4fb5..bafda34 100644
--- a/frontend/src/i18n/en.ts
+++ b/frontend/src/i18n/en.ts
@@ -198,6 +198,8 @@ const en = {
'metrics.allTypes': 'All',
'metrics.allPaths': 'All',
'metrics.resetFilters': 'Reset Filters',
+ 'metrics.responseSize': 'Size',
+ 'metrics.clientIp': 'IP',
// Time Range
'range.1d': '1D',
diff --git a/frontend/src/i18n/ko.ts b/frontend/src/i18n/ko.ts
index 762bd59..0b0d7ee 100644
--- a/frontend/src/i18n/ko.ts
+++ b/frontend/src/i18n/ko.ts
@@ -198,6 +198,8 @@ const ko = {
'metrics.allTypes': '전체',
'metrics.allPaths': '전체',
'metrics.resetFilters': '필터 초기화',
+ 'metrics.responseSize': '응답 크기',
+ 'metrics.clientIp': 'IP',
// Time Range
'range.1d': '1일',
diff --git a/frontend/src/pages/ApiMetrics.tsx b/frontend/src/pages/ApiMetrics.tsx
index 14c5cec..264ec8d 100644
--- a/frontend/src/pages/ApiMetrics.tsx
+++ b/frontend/src/pages/ApiMetrics.tsx
@@ -6,7 +6,7 @@ import { monitorApi } from '../api/monitorApi.ts'
import type { MetricsSummary, CacheStats, ProcessingDelay, CacheDetails, QueryMetricsPage, QueryMetricsSummary, QueryMetricsParams, QueryMetricRow } from '../api/types.ts'
import MetricCard from '../components/charts/MetricCard.tsx'
import DataTable, { type Column } from '../components/common/DataTable.tsx'
-import { formatNumber } from '../utils/formatters.ts'
+import { formatNumber, formatBytes } from '../utils/formatters.ts'
const POLL_INTERVAL = 10_000
const QUERY_POLL_INTERVAL = 30_000
@@ -68,8 +68,16 @@ export default function ApiMetrics() {
{
key: 'created_at', label: t('metrics.queryTime'), sortable: false,
render: (row) => {
- const ts = row.created_at ?? ''
- return ts.length >= 19 ? ts.substring(5, 19) : ts
+ if (!row.created_at) return '-'
+ const d = new Date(row.created_at)
+ // UTC → KST (+9h)
+ const kst = new Date(d.getTime() + 9 * 60 * 60 * 1000)
+ const mm = String(kst.getUTCMonth() + 1).padStart(2, '0')
+ const dd = String(kst.getUTCDate()).padStart(2, '0')
+ const hh = String(kst.getUTCHours()).padStart(2, '0')
+ const mi = String(kst.getUTCMinutes()).padStart(2, '0')
+ const ss = String(kst.getUTCSeconds()).padStart(2, '0')
+ return `${mm}-${dd} ${hh}:${mi}:${ss}`
},
},
{
@@ -120,6 +128,14 @@ export default function ApiMetrics() {
return {ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`}
},
},
+ {
+ key: 'response_bytes', label: t('metrics.responseSize'), align: 'right' as const, sortable: false,
+ render: (row) => row.response_bytes ? formatBytes(row.response_bytes) : '-',
+ },
+ {
+ key: 'client_ip', label: t('metrics.clientIp'), sortable: false,
+ render: (row) => row.client_ip ? {row.client_ip} : '-',
+ },
]
return (
diff --git a/src/main/java/gc/mda/signal_batch/batch/reader/AisTargetCacheManager.java b/src/main/java/gc/mda/signal_batch/batch/reader/AisTargetCacheManager.java
index 920aa27..a756a74 100644
--- a/src/main/java/gc/mda/signal_batch/batch/reader/AisTargetCacheManager.java
+++ b/src/main/java/gc/mda/signal_batch/batch/reader/AisTargetCacheManager.java
@@ -46,7 +46,7 @@ public class AisTargetCacheManager {
@Value("${app.cache.ais-target.ttl-minutes:120}")
private long ttlMinutes;
- @Value("${app.cache.ais-target.max-size:300000}")
+ @Value("${app.cache.ais-target.max-size:500000}")
private int maxSize;
@PostConstruct
diff --git a/src/main/java/gc/mda/signal_batch/domain/gis/controller/AreaSearchController.java b/src/main/java/gc/mda/signal_batch/domain/gis/controller/AreaSearchController.java
index b69b7ec..90f9bef 100644
--- a/src/main/java/gc/mda/signal_batch/domain/gis/controller/AreaSearchController.java
+++ b/src/main/java/gc/mda/signal_batch/domain/gis/controller/AreaSearchController.java
@@ -6,6 +6,7 @@ import gc.mda.signal_batch.domain.gis.dto.VesselContactRequest;
import gc.mda.signal_batch.domain.gis.dto.VesselContactResponse;
import gc.mda.signal_batch.domain.gis.service.AreaSearchService;
import gc.mda.signal_batch.domain.gis.service.VesselContactService;
+import gc.mda.signal_batch.global.exception.QueryTimeoutException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
@@ -219,4 +220,11 @@ public class AreaSearchController {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of("error", e.getMessage()));
}
+
+ @ExceptionHandler(QueryTimeoutException.class)
+ public ResponseEntity