fix(dashboard): Top 클라이언트 IP/ID 토글 및 메트릭 표시 오류 수정

- 토글 활성 상태 시각적 구분 강화 (bg-secondary + font-medium)
- IP 모드 "-" 표시 수정 — 백엔드 client 필드명 매핑 보정
- ID 데이터 없을 때 섹션 사라지는 대신 안내 메시지 표시
- 쿼리 이력(ApiMetrics)에 client_id 컬럼 추가
- history SQL에 client_id 컬럼 포함

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
htlee 2026-03-27 07:04:36 +09:00
부모 d6ab622480
커밋 d19a33b233
4개의 변경된 파일34개의 추가작업 그리고 22개의 파일을 삭제

파일 보기

@ -208,6 +208,7 @@ export interface QueryMetricRow {
cache_hit_days: number
db_query_days: number
client_ip: string | null
client_id: string | null
}
export interface QueryMetricsPage {
@ -252,7 +253,8 @@ export interface TimeSeriesBucket {
}
export interface TopClient {
client_ip: string
client: string
client_ip?: string
query_count: number
avg_elapsed_ms: number
}
@ -261,6 +263,7 @@ export interface QueryMetricsTimeSeries {
buckets: TimeSeriesBucket[]
topClients: TopClient[]
granularity: 'HOURLY' | 'DAILY'
groupBy?: 'ip' | 'id'
}
export interface QueryMetricsParams {

파일 보기

@ -136,6 +136,10 @@ export default function ApiMetrics() {
key: 'client_ip', label: t('metrics.clientIp'), sortable: false,
render: (row) => row.client_ip ? <span className="font-mono text-xs">{row.client_ip}</span> : '-',
},
{
key: 'client_id', label: 'ID', sortable: false,
render: (row) => row.client_id ? <span className="font-mono text-xs">{row.client_id}</span> : '-',
},
]
return (

파일 보기

@ -326,30 +326,31 @@ export default function Dashboard() {
</div>
{/* Top Clients */}
{queryTs.topClients.length > 0 && (
<div>
<div className="mb-2 flex items-center gap-2">
<span className="text-sm font-medium text-muted">{t('dashboard.topClients')}</span>
<div className="flex overflow-hidden rounded-md border border-[var(--border-primary)] text-xs">
<button
type="button"
className={`px-2 py-0.5 ${clientGroupBy === 'ip' ? 'bg-[var(--accent-primary)] text-white' : 'text-[var(--text-secondary)]'}`}
className={`px-2 py-0.5 transition-colors ${clientGroupBy === 'ip' ? 'bg-[var(--accent-primary)] text-white font-medium' : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'}`}
onClick={() => setClientGroupBy('ip')}
>IP</button>
<button
type="button"
className={`px-2 py-0.5 ${clientGroupBy === 'id' ? 'bg-[var(--accent-primary)] text-white' : 'text-[var(--text-secondary)]'}`}
className={`px-2 py-0.5 transition-colors ${clientGroupBy === 'id' ? 'bg-[var(--accent-primary)] text-white font-medium' : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'}`}
onClick={() => setClientGroupBy('id')}
>ID</button>
</div>
</div>
{queryTs.topClients.length > 0 ? (
<div className="space-y-2">
{queryTs.topClients.map((c, i) => {
const maxCount = queryTs.topClients[0].query_count
const pct = maxCount > 0 ? (c.query_count / maxCount) * 100 : 0
const label = c.client ?? c.client_ip ?? '-'
return (
<div key={c.client_ip ?? i} className="flex items-center gap-3 text-sm">
<span className="w-32 truncate font-mono text-xs">{c.client_ip ?? '-'}</span>
<div key={label + i} className="flex items-center gap-3 text-sm">
<span className="w-40 truncate font-mono text-xs" title={label}>{label}</span>
<div className="flex-1">
<div className="h-4 rounded bg-surface-hover">
<div
@ -365,8 +366,12 @@ export default function Dashboard() {
)
})}
</div>
) : (
<div className="py-4 text-center text-xs text-muted">
{clientGroupBy === 'id' ? '사용자 ID 데이터가 없습니다' : '클라이언트 데이터가 없습니다'}
</div>
)}
</div>
</>
) : (
<div className="py-8 text-center text-sm text-muted">{t('dashboard.noChartData')}</div>

파일 보기

@ -106,7 +106,7 @@ public class QueryMetricsController {
zoom_level, requested_mmsi, unique_vessels, total_tracks,
total_points, points_after_simplify, total_chunks,
response_bytes, elapsed_ms, db_query_ms, simplify_ms,
cache_hit_days, db_query_days, client_ip
cache_hit_days, db_query_days, client_ip, client_id
FROM signal.t_query_metrics
""" + whereClause +
" ORDER BY " + sortBy + " " + direction +