-
+
어구별 구조 도식 비교
@@ -815,7 +816,7 @@ export function AIModelManagement() {
-
{g.no}
+
{g.no}
{g.name}
{g.nameEn}
@@ -854,7 +855,7 @@ export function AIModelManagement() {
-
+
어구별 AIS 신호 특성 및 이상 판정 기준
@@ -873,7 +874,7 @@ export function AIModelManagement() {
{DAR03_AIS_SIGNALS.map((s) => (
- {s.no}
+ {s.no}
{s.name}
{s.aisType}
@@ -891,13 +892,13 @@ export function AIModelManagement() {
{s.threshold.map((th) => (
-
+
{th}
))}
- {s.gCodes}
+ {s.gCodes}
))}
@@ -921,8 +922,8 @@ export function AIModelManagement() {
@@ -974,7 +975,7 @@ export function AIModelManagement() {
- 대상 선박 현황 (906척, 6개 업종)
+ 대상 선박 현황 (906척, 6개 업종)
@@ -991,7 +992,7 @@ export function AIModelManagement() {
{TARGET_VESSELS.map((v) => (
- {v.code}
+ {v.code}
{v.name}
{v.count}
{v.zones}
@@ -1014,7 +1015,7 @@ export function AIModelManagement() {
{ALARM_SEVERITY.map((a) => (
@@ -1064,8 +1065,8 @@ export function AIModelManagement() {
@@ -1114,7 +1115,7 @@ export function AIModelManagement() {
-
+
RESTful API 엔드포인트
@@ -1155,7 +1156,7 @@ export function AIModelManagement() {
{api.method}
- {api.endpoint}
+ {api.endpoint}
{api.unit}
{api.desc}
{api.sfr}
@@ -1175,7 +1176,7 @@ export function AIModelManagement() {
-
+
API 호출 예시
@@ -1231,7 +1232,7 @@ export function AIModelManagement() {
-
+
후속 서비스 연계 매핑
@@ -1255,7 +1256,7 @@ export function AIModelManagement() {
{s.desc}
{s.apis.map((a) => (
- {a}
+ {a}
))}
@@ -1272,13 +1273,13 @@ export function AIModelManagement() {
{[
{ label: '총 호출', value: '142,856', color: 'text-heading' },
- { label: 'grid 조회', value: '68,420', color: 'text-blue-400' },
- { label: 'zone 조회', value: '32,115', color: 'text-green-400' },
- { label: 'time 조회', value: '18,903', color: 'text-yellow-400' },
- { label: 'vessel 조회', value: '15,210', color: 'text-orange-400' },
- { label: 'alarms', value: '8,208', color: 'text-red-400' },
- { label: '평균 응답', value: '23ms', color: 'text-cyan-400' },
- { label: '오류율', value: '0.03%', color: 'text-green-400' },
+ { label: 'grid 조회', value: '68,420', color: 'text-blue-600 dark:text-blue-400' },
+ { label: 'zone 조회', value: '32,115', color: 'text-green-600 dark:text-green-400' },
+ { label: 'time 조회', value: '18,903', color: 'text-yellow-600 dark:text-yellow-400' },
+ { label: 'vessel 조회', value: '15,210', color: 'text-orange-600 dark:text-orange-400' },
+ { label: 'alarms', value: '8,208', color: 'text-red-600 dark:text-red-400' },
+ { label: '평균 응답', value: '23ms', color: 'text-cyan-600 dark:text-cyan-400' },
+ { label: '오류율', value: '0.03%', color: 'text-green-600 dark:text-green-400' },
].map((s) => (
{s.value}
diff --git a/frontend/src/features/ai-operations/MLOpsPage.tsx b/frontend/src/features/ai-operations/MLOpsPage.tsx
index b4fad15..79e7fe2 100644
--- a/frontend/src/features/ai-operations/MLOpsPage.tsx
+++ b/frontend/src/features/ai-operations/MLOpsPage.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { Button } from '@shared/components/ui/button';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import { PageContainer, PageHeader } from '@shared/components/layout';
@@ -117,7 +118,7 @@ export function MLOpsPage() {
(
setTab(t.key)}
- className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-blue-400 border-blue-400' : 'text-hint border-transparent hover:text-label'}`}>
+ className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-blue-600 dark:text-blue-400 border-blue-400' : 'text-hint border-transparent hover:text-label'}`}>
{t.label}
))}
@@ -160,7 +161,7 @@ export function MLOpsPage() {
DEPLOYED
{m.name}
{m.ver}
- F1 {m.f1}%
+ F1 {m.f1}%
))}
@@ -188,7 +189,7 @@ export function MLOpsPage() {
{TEMPLATES.map((t, i) => (
setSelectedTmpl(i)}
className={`p-3 rounded-lg text-center cursor-pointer transition-colors ${selectedTmpl === i ? 'bg-blue-600/15 border border-blue-500/30' : 'bg-surface-overlay border border-transparent hover:border-border'}`}>
-
+
{t.name}
{t.desc}
@@ -198,7 +199,7 @@ export function MLOpsPage() {
{EXPERIMENTS.map(e => (
@@ -209,7 +210,7 @@ export function MLOpsPage() {
{e.epoch}
{e.time}
- {e.f1 > 0 &&
F1 {e.f1} }
+ {e.f1 > 0 &&
F1 {e.f1} }
))}
@@ -262,7 +263,7 @@ export function MLOpsPage() {
{DEPLOYS.map((d, i) => (
{d.model}
- {d.ver}
+ {d.ver}
{d.endpoint}
{d.latency}
@@ -289,7 +290,7 @@ export function MLOpsPage() {
{MODELS.filter(m => m.status === 'APPROVED').map(m => (
{m.name} {m.ver}
- 배포
+ }>배포
))}
@@ -314,15 +315,15 @@ export function MLOpsPage() {
"version": "v2.1.0"
}`} />
- 실행
+ }>실행
초기화
RESPONSE
- 상태 200 OK
- 지연 23ms
+ 상태 200 OK
+ 지연 23ms
{`{
"risk_score": 87.5,
@@ -354,7 +355,7 @@ export function MLOpsPage() {
{ key: 'llmtest' as LLMSubTab, label: 'LLM 테스트' },
]).map(t => (
setLlmSub(t.key)}
- className={`px-4 py-2 text-[11px] font-medium border-b-2 ${llmSub === t.key ? 'text-blue-400 border-blue-400' : 'text-hint border-transparent hover:text-label'}`}>{t.label}
+ className={`px-4 py-2 text-[11px] font-medium border-b-2 ${llmSub === t.key ? 'text-blue-600 dark:text-blue-400 border-blue-400' : 'text-hint border-transparent hover:text-label'}`}>{t.label}
))}
@@ -368,7 +369,7 @@ export function MLOpsPage() {
{LLM_MODELS.map((m, i) => (
setSelectedLLM(i)}
className={`p-3 rounded-lg text-center cursor-pointer ${selectedLLM === i ? 'bg-blue-600/15 border border-blue-500/30' : 'bg-surface-overlay border border-transparent'}`}>
-
+
{m.name}
{m.sub}
@@ -382,7 +383,7 @@ export function MLOpsPage() {
))}
- 학습 시작
+ }>학습 시작
@@ -418,10 +419,10 @@ export function MLOpsPage() {
{k} {v}
))}
- 검색 시작
+ }>검색 시작
- HPS 시도 결과
Best: Trial #3 (F1=0.912)
+ HPS 시도 결과
Best: Trial #3 (F1=0.912)
{['Trial', 'LR', 'Batch', 'Dropout', 'Hidden', 'F1 Score', ''].map(h => {h} )}
{HPS_TRIALS.map(t => (
@@ -506,7 +507,7 @@ export function MLOpsPage() {
-
+ } />
diff --git a/frontend/src/features/auth/LoginPage.tsx b/frontend/src/features/auth/LoginPage.tsx
index 6f488fe..5636d99 100644
--- a/frontend/src/features/auth/LoginPage.tsx
+++ b/frontend/src/features/auth/LoginPage.tsx
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Shield, Eye, EyeOff, Lock, User, Fingerprint, KeyRound, AlertCircle } from 'lucide-react';
+import { Button } from '@shared/components/ui/button';
import { useAuth } from '@/app/auth/AuthContext';
import { LoginError } from '@/services/authApi';
import { DemoQuickLogin, type DemoAccount } from './DemoQuickLogin';
@@ -105,7 +106,7 @@ export function LoginPage() {
{/* 로고 영역 */}
-
+
{t('title')}
{t('subtitle')}
@@ -122,7 +123,7 @@ export function LoginPage() {
disabled={m.disabled}
className={`flex-1 flex flex-col items-center gap-1 py-3 transition-colors ${
authMethod === m.key
- ? 'bg-blue-600/10 border-b-2 border-blue-500 text-blue-400'
+ ? 'bg-blue-600/10 border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'
: 'text-hint hover:bg-surface-overlay hover:text-label'
} ${m.disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
title={m.disabled ? '향후 도입 예정' : ''}
@@ -188,16 +189,18 @@ export function LoginPage() {
{error && (
-
+
)}
-
{loading ? (
<>
@@ -205,7 +208,7 @@ export function LoginPage() {
{t('button.authenticating')}
>
) : t('button.login')}
-
+
{/* 데모 퀵로그인 (VITE_SHOW_DEMO_LOGIN=true일 때만 렌더링) */}
@@ -215,7 +218,7 @@ export function LoginPage() {
{/* GPKI 인증 (Phase 9 도입 예정) */}
{authMethod === 'gpki' && (
-
+
{t('gpki.title')}
향후 도입 예정 (Phase 9)
@@ -224,7 +227,7 @@ export function LoginPage() {
{/* SSO 연동 (Phase 9 도입 예정) */}
{authMethod === 'sso' && (
-
+
{t('sso.title')}
향후 도입 예정 (Phase 9)
diff --git a/frontend/src/features/dashboard/Dashboard.tsx b/frontend/src/features/dashboard/Dashboard.tsx
index 670d95a..99efbb8 100644
--- a/frontend/src/features/dashboard/Dashboard.tsx
+++ b/frontend/src/features/dashboard/Dashboard.tsx
@@ -55,7 +55,7 @@ function RiskBar({ value, size = 'default' }: { value: number; size?: 'default'
// backend riskScore.score는 0~100 정수. 0~1 범위로 들어오는 경우도 호환.
const pct = Math.max(0, Math.min(100, value <= 1 ? value * 100 : value));
const color = pct > 70 ? 'bg-red-500' : pct > 50 ? 'bg-orange-500' : pct > 30 ? 'bg-yellow-500' : 'bg-blue-500';
- const textColor = pct > 70 ? 'text-red-400' : pct > 50 ? 'text-orange-400' : pct > 30 ? 'text-yellow-400' : 'text-blue-400';
+ const textColor = pct > 70 ? 'text-red-600 dark:text-red-400' : pct > 50 ? 'text-orange-600 dark:text-orange-400' : pct > 30 ? 'text-yellow-600 dark:text-yellow-400' : 'text-blue-600 dark:text-blue-400';
const barW = size === 'sm' ? 'w-16' : 'w-24';
return (
@@ -78,7 +78,7 @@ function KpiCard({ label, value, prev, icon: Icon, color, desc }: KpiCardProps)
-
+
{isUp ?
:
}
{Math.abs(diff)}
@@ -207,16 +207,16 @@ function SeaAreaMap() {
{/* LIVE 인디케이터 */}
-
실시간 해역 위협도
+
실시간 해역 위협도
);
@@ -468,8 +468,8 @@ export function Dashboard() {
85 ? '#ef4444' : area.risk > 60 ? '#f97316' : area.risk > 40 ? '#eab308' : '#3b82f6'
}}>{area.risk}
- {area.trend === 'up' &&
}
- {area.trend === 'down' &&
}
+ {area.trend === 'up' &&
}
+ {area.trend === 'down' &&
}
{area.trend === 'stable' &&
— }
))}
@@ -544,7 +544,7 @@ export function Dashboard() {
-
+
해상 기상 현황
@@ -557,19 +557,19 @@ export function Dashboard() {
돌풍 {WEATHER_DATA.wind.gust}m/s
-
+
{WEATHER_DATA.wave.height}m
파고
주기 {WEATHER_DATA.wave.period}s
-
+
{WEATHER_DATA.temp.air}°C
기온
수온 {WEATHER_DATA.temp.water}°C
-
+
{WEATHER_DATA.visibility}km
시정
해상{WEATHER_DATA.seaState}급
diff --git a/frontend/src/features/detection/ChinaFishing.tsx b/frontend/src/features/detection/ChinaFishing.tsx
index e13c997..d37cdfa 100644
--- a/frontend/src/features/detection/ChinaFishing.tsx
+++ b/frontend/src/features/detection/ChinaFishing.tsx
@@ -1,6 +1,10 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
+import { Button } from '@shared/components/ui/button';
+import { Input } from '@shared/components/ui/input';
+import { Select } from '@shared/components/ui/select';
+import { TabBar, TabButton } from '@shared/components/ui/tabs';
import { PageContainer } from '@shared/components/layout';
import {
Search, Clock, ChevronRight, ChevronLeft, Cloud,
@@ -339,22 +343,19 @@ export function ChinaFishing() {
return (
{/* ── 모드 탭 (AI 대시보드 / 환적 탐지) ── */}
-
+
{modeTabs.map((tab) => (
- setMode(tab.key)}
- className={`flex items-center gap-1.5 px-4 py-2 rounded-md text-[11px] font-medium transition-colors ${
- mode === tab.key
- ? 'bg-blue-600 text-on-vivid'
- : 'text-muted-foreground hover:text-foreground hover:bg-surface-overlay'
- }`}
+ icon={ }
>
-
{tab.label}
-
+
))}
-
+
{/* 환적 탐지 모드 */}
{mode === 'transfer' && }
@@ -372,7 +373,7 @@ export function ChinaFishing() {
)}
- {apiError && 에러: {apiError}
}
+ {apiError && {tcCommon('error.errorPrefix', { msg: apiError })}
}
{apiLoading && (
@@ -389,16 +390,21 @@ export function ChinaFishing() {
기준 : {formatDateTime(new Date())}
-
-
-
+ }
+ />
-
-
+
@@ -456,13 +462,13 @@ export function ChinaFishing() {
@@ -480,29 +486,32 @@ export function ChinaFishing() {
관심영역 안전도
데모 데이터
-
+
영역 A
영역 B
-
+
설정한 관심 영역을 선택후 조회를 눌러주세요.
-
+
특이운항
- 정상
+ 정상
-
+
비허가
- 정상
+ 정상
{/* 탭 헤더 — 특이운항만 활성, 나머지 3개는 데이터 소스 미연동 */}
-
+
{vesselTabs.map((tab) => {
const disabled = tab !== '특이운항';
return (
- !disabled && setVesselTab(tab)}
disabled={disabled}
- className={`flex-1 py-2.5 text-[11px] font-medium transition-colors flex items-center justify-center gap-1 ${
- vesselTab === tab
- ? 'text-heading border-b-2 border-blue-500 bg-surface-overlay'
- : disabled
- ? 'text-hint opacity-50 cursor-not-allowed'
- : 'text-hint hover:text-label'
- }`}
+ className="flex-1 justify-center"
>
{tab}
{disabled && (
준비중
)}
-
+
);
})}
-
+
{/* 선박 목록 */}
@@ -599,22 +604,20 @@ export function ChinaFishing() {
{/* 탭 — 월별 집계 API 미연동 */}
-
+
{statsTabs.map((tab) => (
- setStatsTab(tab)}
- className={`flex-1 py-2.5 text-[11px] font-medium transition-colors flex items-center justify-center gap-1 ${
- statsTab === tab
- ? 'text-heading border-b-2 border-green-500 bg-surface-overlay'
- : 'text-hint hover:text-label'
- }`}
+ className="flex-1 justify-center"
>
{tab}
준비중
-
+
))}
-
+
{/* 월별 통계 - API 미지원, 준비중 안내 */}
@@ -659,9 +662,9 @@ export function ChinaFishing() {
{/* 다운로드 버튼 */}
-
+
다운로드
-
+
@@ -677,7 +680,7 @@ export function ChinaFishing() {
최근 위성영상 분석
데모 데이터
- 자세히 보기
+ 자세히 보기
@@ -704,11 +707,11 @@ export function ChinaFishing() {
기상 예보
데모 데이터
-
자세히 보기
+
자세히 보기
-
+
전남서부남해앞바다
@@ -730,7 +733,7 @@ export function ChinaFishing() {
VTS연계 현황
데모 데이터
-
자세히 보기
+
자세히 보기
{VTS_ITEMS.map((vts) => (
@@ -738,22 +741,28 @@ export function ChinaFishing() {
key={vts.name}
className={`flex items-center gap-1.5 px-2 py-1 rounded text-[10px] ${
vts.active
- ? 'bg-orange-500/15 text-orange-400 border border-orange-500/20'
+ ? 'bg-orange-500/15 text-orange-600 dark:text-orange-400 border border-orange-500/20'
: 'bg-surface-overlay text-muted-foreground border border-slate-700/30'
}`}
>
-
+
{vts.name}
))}
-
-
-
-
-
-
+ }
+ />
+ }
+ />
diff --git a/frontend/src/features/detection/DarkVesselDetection.tsx b/frontend/src/features/detection/DarkVesselDetection.tsx
index 6d7c179..0b34ba1 100644
--- a/frontend/src/features/detection/DarkVesselDetection.tsx
+++ b/frontend/src/features/detection/DarkVesselDetection.tsx
@@ -93,15 +93,18 @@ export function DarkVesselDetection() {
{ key: 'darkScore', label: '의심점수', width: '80px', align: 'center', sortable: true,
render: (v) => {
const n = v as number;
- return = 70 ? 'text-red-400' : n >= 50 ? 'text-orange-400' : 'text-yellow-400'}`}>{n} ;
+ const c = n >= 70 ? 'text-red-600 dark:text-red-400'
+ : n >= 50 ? 'text-orange-600 dark:text-orange-400'
+ : 'text-yellow-600 dark:text-yellow-400';
+ return {n} ;
} },
{ key: 'name', label: '선박 유형', sortable: true,
- render: (v) => {v as string} },
+ render: (v) => {v as string} },
{ key: 'mmsi', label: 'MMSI', width: '100px',
render: (v) => {
const mmsi = v as string;
return (
- { e.stopPropagation(); navigate(`/vessel/${mmsi}`); }}>
{mmsi}
@@ -116,7 +119,10 @@ export function DarkVesselDetection() {
{ key: 'risk', label: '위험도', width: '70px', align: 'center', sortable: true,
render: (v) => {
const n = v as number;
- return = 70 ? 'text-red-400' : n >= 50 ? 'text-yellow-400' : 'text-green-400'}`}>{n} ;
+ const c = n >= 70 ? 'text-red-600 dark:text-red-400'
+ : n >= 50 ? 'text-yellow-600 dark:text-yellow-400'
+ : 'text-green-600 dark:text-green-400';
+ return {n} ;
} },
{ key: 'darkPatterns', label: '의심 패턴', minWidth: '120px',
render: (v) => {v as string} },
@@ -252,7 +258,7 @@ export function DarkVesselDetection() {
}
/>
- {error && 에러: {error}
}
+ {error && {tc('error.errorPrefix', { msg: error })}
}
{loading && (
@@ -263,10 +269,10 @@ export function DarkVesselDetection() {
{/* KPI — tier 기반 */}
{[
- { l: '전체', v: tierCounts.total, c: 'text-red-400', filter: '' },
- { l: 'CRITICAL', v: tierCounts.CRITICAL, c: 'text-red-400', filter: 'CRITICAL' },
- { l: 'HIGH', v: tierCounts.HIGH, c: 'text-orange-400', filter: 'HIGH' },
- { l: 'WATCH', v: tierCounts.WATCH, c: 'text-yellow-400', filter: 'WATCH' },
+ { l: '전체', v: tierCounts.total, c: 'text-red-600 dark:text-red-400', filter: '' },
+ { l: 'CRITICAL', v: tierCounts.CRITICAL, c: 'text-red-600 dark:text-red-400', filter: 'CRITICAL' },
+ { l: 'HIGH', v: tierCounts.HIGH, c: 'text-orange-600 dark:text-orange-400', filter: 'HIGH' },
+ { l: 'WATCH', v: tierCounts.WATCH, c: 'text-yellow-600 dark:text-yellow-400', filter: 'WATCH' },
].map((k) => (
setTierFilter(k.filter)}
@@ -304,7 +310,7 @@ export function DarkVesselDetection() {
-
{tierCounts.CRITICAL}척
+
{tierCounts.CRITICAL}척
CRITICAL Dark Vessel
diff --git a/frontend/src/features/detection/GearDetection.tsx b/frontend/src/features/detection/GearDetection.tsx
index 31cd0f1..ef1d3b2 100644
--- a/frontend/src/features/detection/GearDetection.tsx
+++ b/frontend/src/features/detection/GearDetection.tsx
@@ -2,6 +2,8 @@ import { useEffect, useState, useMemo, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
+import { Button } from '@shared/components/ui/button';
+import { Checkbox } from '@shared/components/ui/checkbox';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
import { Anchor, AlertTriangle, Loader2, Filter, X } from 'lucide-react';
@@ -143,11 +145,13 @@ function FilterCheckGroup({ label, selected, onChange, options }: {
{label} {selected.size > 0 && ({selected.size}) }
{options.map(o => (
-
- toggle(o.value)}
- className="w-3 h-3 rounded border-border accent-primary" />
- {o.label}
-
+ toggle(o.value)}
+ label={o.label}
+ className="w-3 h-3"
+ />
))}
@@ -169,7 +173,7 @@ export function GearDetection() {
{ key: 'id', label: 'ID', width: '70px', render: v =>
{v as string} },
{ key: 'type', label: '그룹 유형', width: '100px', sortable: true, render: v =>
{v as string} },
{ key: 'owner', label: '어구 그룹', sortable: true,
- render: v =>
{v as string} },
+ render: v =>
{v as string} },
{ key: 'memberCount', label: '멤버', width: '50px', align: 'center',
render: v =>
{v as number}척 },
{ key: 'zone', label: '설치 해역', width: '130px', sortable: true,
@@ -191,7 +195,13 @@ export function GearDetection() {
) : - },
{ key: 'risk', label: '위험도', width: '65px', align: 'center', sortable: true,
- render: v => { const r = v as string; const c = r === '고위험' ? 'text-red-400' : r === '중위험' ? 'text-yellow-400' : 'text-green-400'; return {r} ; } },
+ render: v => {
+ const r = v as string;
+ const c = r === '고위험' ? 'text-red-600 dark:text-red-400'
+ : r === '중위험' ? 'text-yellow-600 dark:text-yellow-400'
+ : 'text-green-600 dark:text-green-400';
+ return {r} ;
+ } },
{ key: 'parentStatus', label: '모선 상태', width: '90px', sortable: true,
render: v => {
const s = v as string;
@@ -200,13 +210,13 @@ export function GearDetection() {
return {label} ;
} },
{ key: 'parentMmsi', label: '추정 모선', width: '100px',
- render: v => { const m = v as string; return m !== '-' ? {m} : - ; } },
+ render: v => { const m = v as string; return m !== '-' ? {m} : - ; } },
{ key: 'topScore', label: '후보 일치', width: '75px', align: 'center', sortable: true,
render: (v: unknown) => {
const s = v as number;
if (s <= 0) return - ;
const pct = Math.round(s * 100);
- const c = pct >= 72 ? 'text-green-400' : pct >= 50 ? 'text-yellow-400' : 'text-hint';
+ const c = pct >= 72 ? 'text-green-600 dark:text-green-400' : pct >= 50 ? 'text-yellow-600 dark:text-yellow-400' : 'text-hint';
return {pct}% ;
} },
{ key: 'lastSignal', label: '최종 신호', width: '80px', render: v => {v as string} },
@@ -460,7 +470,7 @@ export function GearDetection() {
)}
- {error && 에러: {error}
}
+ {error && {tc('error.errorPrefix', { msg: error })}
}
{loading && (
@@ -472,9 +482,9 @@ export function GearDetection() {
{[
{ l: '전체 어구 그룹', v: filteredData.length, c: 'text-heading' },
- { l: '불법 의심', v: filteredData.filter(d => d.status.includes('불법')).length, c: 'text-red-400' },
- { l: '확인 중', v: filteredData.filter(d => d.status === '확인 중').length, c: 'text-yellow-400' },
- { l: '정상', v: filteredData.filter(d => d.status === '정상').length, c: 'text-green-400' },
+ { l: '불법 의심', v: filteredData.filter(d => d.status.includes('불법')).length, c: 'text-red-600 dark:text-red-400' },
+ { l: '확인 중', v: filteredData.filter(d => d.status === '확인 중').length, c: 'text-yellow-600 dark:text-yellow-400' },
+ { l: '정상', v: filteredData.filter(d => d.status === '정상').length, c: 'text-green-600 dark:text-green-400' },
].map(k => (
{k.v} {k.l}
@@ -510,11 +520,15 @@ export function GearDetection() {
{hasActiveFilter && (
<>
{filteredData.length}/{DATA.length}건
- { setFilterZone(new Set()); setFilterStatus(new Set()); setFilterRisk(new Set()); setFilterParentStatus(new Set()); setFilterPermit(new Set()); setFilterMemberMin(2); setFilterMemberMax(filterOptions.maxMember || Infinity); }}
- className="flex items-center gap-1 px-2 py-1 text-[10px] text-hint hover:text-label rounded hover:bg-surface-raised">
- 초기화
-
+ icon={ }
+ >
+ 초기화
+
>
)}
@@ -562,11 +576,15 @@ export function GearDetection() {
{/* 패널 내 초기화 */}
{filteredData.length}/{DATA.length}건 표시
- { setFilterZone(new Set()); setFilterStatus(new Set()); setFilterRisk(new Set()); setFilterParentStatus(new Set()); setFilterPermit(new Set()); setFilterMemberMin(2); setFilterMemberMax(filterOptions.maxMember || Infinity); }}
- className="flex items-center gap-1 px-2 py-1 text-[10px] text-hint hover:text-label rounded hover:bg-surface-overlay">
- 전체 초기화
-
+ icon={ }
+ >
+ 전체 초기화
+
)}
@@ -620,8 +638,8 @@ export function GearDetection() {
-
-
{DATA.length}건
+
+
{DATA.length}건
어구 그룹
{/* 리플레이 컨트롤러 (활성 시 표시) */}
diff --git a/frontend/src/features/detection/GearIdentification.tsx b/frontend/src/features/detection/GearIdentification.tsx
index 575a430..4a2184f 100644
--- a/frontend/src/features/detection/GearIdentification.tsx
+++ b/frontend/src/features/detection/GearIdentification.tsx
@@ -7,6 +7,7 @@ import {
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
+import { Button } from '@shared/components/ui/button';
import { getAlertLevelIntent, isValidAlertLevel } from '@shared/constants/alertLevels';
import { getZoneCodeLabel } from '@shared/constants/zoneCodes';
import { formatDateTime } from '@shared/utils/dateFormat';
@@ -573,7 +574,7 @@ function GearComparisonTable() {
-
+
한·중 어구 비교 레퍼런스 (GB/T 5147-2003 기반)
@@ -593,18 +594,18 @@ function GearComparisonTable() {
-
중국어선 특징
+
중국어선 특징
{row.chinaFeatures.map((f, i) => (
- - {f}
+ - {f}
))}
-
한국어선 특징
+
한국어선 특징
{row.koreaFeatures.map((f, i) => (
- - {f}
+ - {f}
))}
@@ -714,7 +715,7 @@ export function GearIdentification() {
-
+
{t('gearId.title')}
@@ -722,13 +723,14 @@ export function GearIdentification() {
- setShowReference(!showReference)}
- className="px-3 py-1.5 bg-secondary border border-slate-700/50 rounded-md text-[11px] text-label hover:bg-switch-background transition-colors flex items-center gap-1.5"
+ icon={ }
>
-
{showReference ? '레퍼런스 닫기' : '비교 레퍼런스'}
-
+
@@ -741,7 +743,7 @@ export function GearIdentification() {
자동탐지 연계
MMSI
- {autoSelected.mmsi}
+ {autoSelected.mmsi}
·
어구
{autoSelected.gearCode}
@@ -751,12 +753,14 @@ export function GearIdentification() {
하단 자동탐지 목록에서 클릭한 선박 정보가 아래 폼에 프리필되었습니다. 추가 정보 입력 후 판별 실행하세요.
-
{ setAutoSelected(null); setInput(DEFAULT_INPUT); setResult(null); }}
- className="text-[10px] text-hint hover:text-heading shrink-0"
+ className="shrink-0 text-[10px]"
>
해제
-
+
)}
@@ -878,19 +882,22 @@ export function GearIdentification() {
{/* 판별 버튼 */}
- }
+ className="flex-1 font-bold"
>
-
어구 국적 판별 실행
-
-
+
초기화
-
+
@@ -950,7 +957,7 @@ export function GearIdentification() {
-
+
판별 근거 ({result.reasons.length}건)
@@ -958,7 +965,7 @@ export function GearIdentification() {
{result.reasons.map((reason, i) => (
-
+
{reason}
))}
@@ -970,7 +977,7 @@ export function GearIdentification() {
{result.warnings.length > 0 && (
-
+
경고 / 위반 사항 ({result.warnings.length}건)
@@ -979,8 +986,8 @@ export function GearIdentification() {
{result.warnings.map((warning, i) => (
-
- {warning}
+
+ {warning}
))}
@@ -992,13 +999,13 @@ export function GearIdentification() {
-
+
AI 탐지 Rule (해당 어구)
{result.gearType === 'trawl' && (
-
+
{`# 트롤 탐지 조건 (Trawl Detection Rule)
if speed in range(2.0, 5.0) # knots
and trajectory == 'parallel_sweep' # 반복 평행선
@@ -1013,7 +1020,7 @@ and speed_sync > 0.92 # 2선 속도 동기화`}
)}
{result.gearType === 'gillnet' && (
-
+
{`# 자망 탐지 조건 (Gillnet Detection Rule)
if speed < 2.0 # knots
and stop_duration > 30 # min
@@ -1028,7 +1035,7 @@ and sar_vessel_detect == True # SAR 위치 확인
)}
{result.gearType === 'purseSeine' && (
-
+
{`# 선망 탐지 조건 (Purse Seine Detection Rule)
if trajectory == 'circular' # 원형 궤적
and speed_change > 5.0 # kt (고→저 급변)
@@ -1044,7 +1051,7 @@ and vessel_spacing < 1000 # m
)}
{result.gearType === 'setNet' && (
-
+
{`# 정치망 — EEZ 내 중국어선 미허가 어구
# GB/T 코드: Z__ (ZD: 단묘장망, ZS: 복묘장망)
#
@@ -1070,7 +1077,7 @@ and vessel_spacing < 1000 # m
-
+
다중 센서 교차 검증 파이프라인
@@ -1164,20 +1171,23 @@ function AutoGearDetectionSection({
-
+
최근 자동탐지 결과 (prediction, 최근 1시간 중국 선박)
GET /api/analysis/gear-detections · MMSI 412 · gear_code / gear_judgment NOT NULL · 행 클릭 시 상단 입력 폼에 프리필
-
-
-
+
}
+ />
- {error && 에러: {error}
}
+ {error && {t('error.errorPrefix', { msg: error })}
}
{loading &&
}
{!loading && (
@@ -1212,7 +1222,7 @@ function AutoGearDetectionSection({
}`}
title="클릭하면 상단 입력 폼에 자동으로 채워집니다"
>
- {v.mmsi}
+ {v.mmsi}
{v.vesselType ?? '-'}
{v.gearCode}
diff --git a/frontend/src/features/detection/RealGearGroups.tsx b/frontend/src/features/detection/RealGearGroups.tsx
index fd537a2..4d2c053 100644
--- a/frontend/src/features/detection/RealGearGroups.tsx
+++ b/frontend/src/features/detection/RealGearGroups.tsx
@@ -2,6 +2,9 @@ import { useEffect, useState, useCallback } from 'react';
import { Loader2, RefreshCw, MapPin } from 'lucide-react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
+import { Button } from '@shared/components/ui/button';
+import { Select } from '@shared/components/ui/select';
+import type { BadgeIntent } from '@lib/theme/variants';
import { fetchGroups, type GearGroupItem } from '@/services/vesselAnalysisApi';
import { getGearGroupTypeIntent, getGearGroupTypeLabel } from '@shared/constants/gearGroupTypes';
import { getParentResolutionIntent, getParentResolutionLabel } from '@shared/constants/parentResolutionStatuses';
@@ -62,29 +65,37 @@ export function RealGearGroups() {
- setFilterType(e.target.value)}
- className="bg-surface-overlay border border-border rounded px-2 py-1 text-[10px] text-heading">
+ setFilterType(e.target.value)}
+ >
전체
FLEET
GEAR_IN_ZONE
GEAR_OUT_ZONE
-
-
-
-
+
+ }
+ />
{/* 통계 */}
-
-
-
-
-
+
+
+
+
+
- {error && 에러: {error}
}
+ {error && {tc('error.errorPrefix', { msg: error })}
}
{loading &&
}
{!loading && (
@@ -142,11 +153,22 @@ export function RealGearGroups() {
);
}
-function StatBox({ label, value, color }: { label: string; value: number; color: string }) {
+const INTENT_TEXT_CLASS: Record = {
+ critical: 'text-red-600 dark:text-red-400',
+ high: 'text-orange-600 dark:text-orange-400',
+ warning: 'text-yellow-600 dark:text-yellow-400',
+ info: 'text-blue-600 dark:text-blue-400',
+ success: 'text-green-600 dark:text-green-400',
+ muted: 'text-heading',
+ purple: 'text-purple-600 dark:text-purple-400',
+ cyan: 'text-cyan-600 dark:text-cyan-400',
+};
+
+function StatBox({ label, value, intent = 'muted' }: { label: string; value: number; intent?: BadgeIntent }) {
return (
{label}
-
{value}
+
{value}
);
}
diff --git a/frontend/src/features/detection/RealVesselAnalysis.tsx b/frontend/src/features/detection/RealVesselAnalysis.tsx
index 95221b5..d1446b7 100644
--- a/frontend/src/features/detection/RealVesselAnalysis.tsx
+++ b/frontend/src/features/detection/RealVesselAnalysis.tsx
@@ -3,6 +3,9 @@ import { Loader2, RefreshCw, EyeOff, AlertTriangle, Radar } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
+import { Button } from '@shared/components/ui/button';
+import { Select } from '@shared/components/ui/select';
+import type { BadgeIntent } from '@lib/theme/variants';
import { getAlertLevelIntent } from '@shared/constants/alertLevels';
import { getVesselTypeLabel } from '@shared/constants/vesselTypes';
import type { VesselAnalysisItem } from '@/services/vesselAnalysisApi';
@@ -118,31 +121,38 @@ export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore
- setZoneFilter(e.target.value)}
- className="bg-surface-overlay border border-border rounded px-2 py-1 text-[10px] text-heading">
+ setZoneFilter(e.target.value)}
+ >
전체 해역
영해
접속수역
EEZ 외
-
-
-
-
+
+ }
+ />
{/* 통계 카드 — items(= mode 필터 적용된 선박) 기준 집계 */}
-
-
-
-
-
-
+
+
+
+
+
+
- {error && 에러: {error}
}
+ {error && {t('error.errorPrefix', { msg: error })}
}
{loading &&
}
{!loading && (
@@ -168,7 +178,7 @@ export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore
)}
{sortedByRisk.slice(0, 100).map((v) => (
- {v.mmsi}
+ {v.mmsi}
{getVesselTypeLabel(v.classification.vesselType, t, lang)}
{v.classification.confidence > 0 && (
@@ -193,7 +203,7 @@ export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore
{v.algorithms.gpsSpoofing.spoofingScore > 0 ? (
- {v.algorithms.gpsSpoofing.spoofingScore.toFixed(2)}
+ {v.algorithms.gpsSpoofing.spoofingScore.toFixed(2)}
) : - }
@@ -220,11 +230,22 @@ export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore
);
}
-function StatBox({ label, value, color }: { label: string; value: number | undefined; color: string }) {
+const INTENT_TEXT_CLASS: Record = {
+ critical: 'text-red-600 dark:text-red-400',
+ high: 'text-orange-600 dark:text-orange-400',
+ warning: 'text-yellow-600 dark:text-yellow-400',
+ info: 'text-blue-600 dark:text-blue-400',
+ success: 'text-green-600 dark:text-green-400',
+ muted: 'text-heading',
+ purple: 'text-purple-600 dark:text-purple-400',
+ cyan: 'text-cyan-600 dark:text-cyan-400',
+};
+
+function StatBox({ label, value, intent = 'muted' }: { label: string; value: number | undefined; intent?: BadgeIntent }) {
return (
{label}
-
{(value ?? 0).toLocaleString()}
+
{(value ?? 0).toLocaleString()}
);
}
diff --git a/frontend/src/features/detection/components/DarkDetailPanel.tsx b/frontend/src/features/detection/components/DarkDetailPanel.tsx
index 7b388bf..a41ecb5 100644
--- a/frontend/src/features/detection/components/DarkDetailPanel.tsx
+++ b/frontend/src/features/detection/components/DarkDetailPanel.tsx
@@ -73,7 +73,7 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) {
{/* 헤더 */}
-
+
판정 상세
{darkTier}
{darkScore}점
@@ -87,12 +87,12 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) {
{/* 선박 기본 정보 */}
-
+
선박 정보
MMSI
-
navigate(`/vessel/${vessel.mmsi}`)}>
{vessel.mmsi}
@@ -116,7 +116,7 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) {
{/* 점수 산출 내역 */}
-
+
점수 산출 내역
({breakdown.items.length}개 패턴 적용)
@@ -126,7 +126,7 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) {
{/* GAP 상세 */}
-
+
GAP 상세
@@ -152,7 +152,7 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) {
{/* 과거 이력 */}
-
+
과거 이력 (7일)
diff --git a/frontend/src/features/detection/components/GearDetailPanel.tsx b/frontend/src/features/detection/components/GearDetailPanel.tsx
index 691ff35..cb33138 100644
--- a/frontend/src/features/detection/components/GearDetailPanel.tsx
+++ b/frontend/src/features/detection/components/GearDetailPanel.tsx
@@ -270,7 +270,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
{/* 헤더 */}
-
+
어구 판정 상세
{gear.id}
@@ -287,7 +287,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
{gear.gCodes.length > 0 && (
-
+
G코드 위반 내역
총 {gear.gearViolationScore}점
@@ -312,7 +312,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
{/* 어구 그룹 정보 */}
@@ -344,7 +344,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
{/* 모선 추론 정보 */}
-
+
모선 추론
{parentStatusLabel}
@@ -352,7 +352,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
추정 모선
{gear.parentMmsi !== '-' && gear.parentMmsi ? (
- navigate(`/vessel/${gear.parentMmsi}`)}>
{gear.parentMmsi}
@@ -366,7 +366,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
{/* 모선 추론 후보 상세 (Correlation) */}
-
+
추론 후보 상세
{corrLoading &&
}
{correlations.length}건
@@ -393,7 +393,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
className="w-3 h-3 rounded border-border accent-emerald-500 shrink-0 cursor-pointer"
aria-label={`${c.targetMmsi} 리플레이 선택`} />
navigate(`/vessel/${c.targetMmsi}`)}>
{c.targetMmsi}
@@ -467,9 +467,9 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
{/* 헤더 */}
-
+
후보 검토
- {cand.targetMmsi}
+ {cand.targetMmsi}
{cand.targetName}
@@ -575,7 +575,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
{hasPairTrawl && (
-
+
쌍끌이 트롤 공조
G-06
@@ -583,7 +583,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
상대 선박
{gear.pairTrawlPairMmsi ? (
- navigate(`/vessel/${gear.pairTrawlPairMmsi}`)}>
{gear.pairTrawlPairMmsi}
@@ -615,7 +615,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
{/* 위치 + 액션 */}
-
+
위치
diff --git a/frontend/src/features/detection/components/VesselAnomalyPanel.tsx b/frontend/src/features/detection/components/VesselAnomalyPanel.tsx
index c562c5b..52c64cb 100644
--- a/frontend/src/features/detection/components/VesselAnomalyPanel.tsx
+++ b/frontend/src/features/detection/components/VesselAnomalyPanel.tsx
@@ -38,14 +38,14 @@ export function VesselAnomalyPanel({ segments, loading, error, totalHistoryCount
-
+
특이운항 판별 구간
{segments.length > 0 && (
· {segments.length}구간
- {criticalCount > 0 && CRITICAL {criticalCount} }
- {warningCount > 0 && WARNING {warningCount} }
- {infoCount > 0 && INFO {infoCount} }
+ {criticalCount > 0 && CRITICAL {criticalCount} }
+ {warningCount > 0 && WARNING {warningCount} }
+ {infoCount > 0 && INFO {infoCount} }
)}
@@ -55,7 +55,7 @@ export function VesselAnomalyPanel({ segments, loading, error, totalHistoryCount
{error && (
-
+
diff --git a/frontend/src/features/detection/components/VesselMiniMap.tsx b/frontend/src/features/detection/components/VesselMiniMap.tsx
index d301a78..68f4842 100644
--- a/frontend/src/features/detection/components/VesselMiniMap.tsx
+++ b/frontend/src/features/detection/components/VesselMiniMap.tsx
@@ -182,7 +182,7 @@ export function VesselMiniMap({ mmsi, vesselName, hoursBack = 24, segments = [],
-
+
{vesselName ?? mmsi}
@@ -218,7 +218,7 @@ export function VesselMiniMap({ mmsi, vesselName, hoursBack = 24, segments = [],
)}
{!loading && error && (
-
+
{error}
)}
diff --git a/frontend/src/features/enforcement/EventList.tsx b/frontend/src/features/enforcement/EventList.tsx
index 1924eb6..651ef3e 100644
--- a/frontend/src/features/enforcement/EventList.tsx
+++ b/frontend/src/features/enforcement/EventList.tsx
@@ -131,7 +131,7 @@ export function EventList() {
},
},
{ key: 'vesselName', label: '선박명', minWidth: '100px', maxWidth: '220px', sortable: true,
- render: (val) =>
{val as string} ,
+ render: (val) =>
{val as string} ,
},
{ key: 'mmsi', label: 'MMSI', minWidth: '90px', maxWidth: '120px',
render: (_val, row) => {
@@ -140,7 +140,7 @@ export function EventList() {
return (
{ e.stopPropagation(); navigate(`/vessel/${mmsi}`); }}
>
{mmsi}
@@ -172,25 +172,25 @@ export function EventList() {
{isNew && (
{ e.stopPropagation(); handleAck(eid); }}>
)}
{ e.stopPropagation(); if (row.mmsi !== '-') navigate(`/vessel/${row.mmsi}`); }}>
{isActionable && (
<>
{ e.stopPropagation(); handleCreateEnforcement(row); }}>
{ e.stopPropagation(); handleFalsePositive(eid); }}>
@@ -317,7 +317,7 @@ export function EventList() {
{/* 에러 표시 */}
{error && (
-
+
데이터 로딩 실패: {error}
)}
@@ -327,7 +327,7 @@ export function EventList() {
이벤트 데이터 업로드
- setShowUpload(false)} className="text-hint hover:text-muted-foreground">
+ setShowUpload(false)} className="text-hint hover:text-muted-foreground">
@@ -343,7 +343,7 @@ export function EventList() {
{/* 로딩 인디케이터 */}
{loading && (
-
+
로딩 중...
)}
diff --git a/frontend/src/features/field-ops/AIAlert.tsx b/frontend/src/features/field-ops/AIAlert.tsx
index 4c12489..44932a0 100644
--- a/frontend/src/features/field-ops/AIAlert.tsx
+++ b/frontend/src/features/field-ops/AIAlert.tsx
@@ -37,7 +37,7 @@ const cols: DataColumn
[] = [
key: 'eventId',
label: '이벤트',
width: '80px',
- render: (v) => EVT-{v as number} ,
+ render: (v) => EVT-{v as number} ,
},
{
key: 'time',
@@ -58,7 +58,7 @@ const cols: DataColumn[] = [
{
key: 'recipient',
label: '수신 대상',
- render: (v) => {v as string} ,
+ render: (v) => {v as string} ,
},
{
key: 'confidence',
@@ -70,7 +70,7 @@ const cols: DataColumn[] = [
const s = v as string;
if (!s) return - ;
const n = parseFloat(s);
- const color = n > 0.9 ? 'text-red-400' : n > 0.8 ? 'text-orange-400' : 'text-yellow-400';
+ const color = n > 0.9 ? 'text-red-600 dark:text-red-400' : n > 0.8 ? 'text-orange-600 dark:text-orange-400' : 'text-yellow-600 dark:text-yellow-400';
return {(n * 100).toFixed(0)}% ;
},
},
@@ -149,10 +149,10 @@ export function AIAlert() {
if (error) {
return (
-
+
알림 조회 실패: {error}
-
+
재시도
@@ -164,15 +164,15 @@ export function AIAlert() {
{[
{ l: '총 발송', v: totalElements, c: 'text-heading' },
- { l: '수신확인', v: deliveredCount, c: 'text-green-400' },
- { l: '실패', v: failedCount, c: 'text-red-400' },
+ { l: '수신확인', v: deliveredCount, c: 'text-green-600 dark:text-green-400' },
+ { l: '실패', v: failedCount, c: 'text-red-600 dark:text-red-400' },
].map((k) => (
@@ -70,7 +70,7 @@ export function MobileService() {
{/* 긴급 경보 */}
-
[긴급] EEZ 침범 탐지
+
[긴급] EEZ 침범 탐지
N37°12' E124°38' · 08:47
{/* 지도 영역 — MapLibre GL */}
@@ -125,14 +125,14 @@ export function MobileService() {
{ icon: WifiOff, name: '오프라인 지원', desc: '통신 불안정 시 지도·객체 임시 저장' },
].map(f => (
))}
- 푸시 알림 설정
+ 푸시 알림 설정
{PUSH_SETTINGS.map(p => (
diff --git a/frontend/src/features/parent-inference/LabelSession.tsx b/frontend/src/features/parent-inference/LabelSession.tsx
index c9761a7..c7973fd 100644
--- a/frontend/src/features/parent-inference/LabelSession.tsx
+++ b/frontend/src/features/parent-inference/LabelSession.tsx
@@ -3,6 +3,7 @@ import { Tag, X, Loader2 } from 'lucide-react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button';
+import { Input } from '@shared/components/ui/input';
import { Select } from '@shared/components/ui/select';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { useAuth } from '@/app/auth/AuthContext';
@@ -119,26 +120,29 @@ export function LabelSession() {
신규 학습 세션 등록
- {!canCreate && 권한 없음 }
+ {!canCreate && 권한 없음 }
- setGroupKey(e.target.value)} placeholder="group_key"
- className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreate} />
- setSubCluster(e.target.value)} placeholder="sub"
- className="w-24 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreate} />
- setLabelMmsi(e.target.value)} placeholder="정답 parent MMSI"
- className="w-48 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreate} />
-
- {busy === -1 ? : }
+ setGroupKey(e.target.value)} placeholder="group_key"
+ className="flex-1" disabled={!canCreate} />
+ setSubCluster(e.target.value)} placeholder="sub"
+ className="w-24" disabled={!canCreate} />
+ setLabelMmsi(e.target.value)} placeholder="정답 parent MMSI"
+ className="w-48" disabled={!canCreate} />
+ : }
+ >
세션 생성
-
+
- {error &&
{tc('error.errorPrefix', { msg: error })}
}
+ {error &&
{tc('error.errorPrefix', { msg: error })}
}
{loading && (
@@ -171,7 +175,7 @@ export function LabelSession() {
{it.id}
{it.groupKey}
{it.subClusterId}
-
{it.labelParentMmsi}
+
{it.labelParentMmsi}
{getLabelSessionLabel(it.status, tc, lang)}
@@ -180,7 +184,7 @@ export function LabelSession() {
{it.status === 'ACTIVE' && (
handleCancel(it.id)}
- className="p-1 rounded hover:bg-red-500/20 disabled:opacity-30 text-red-400" title="취소">
+ className="p-1 rounded hover:bg-red-500/20 disabled:opacity-30 text-red-600 dark:text-red-400" title="취소" aria-label="취소">
)}
diff --git a/frontend/src/features/parent-inference/ParentExclusion.tsx b/frontend/src/features/parent-inference/ParentExclusion.tsx
index 66e0c32..11ff785 100644
--- a/frontend/src/features/parent-inference/ParentExclusion.tsx
+++ b/frontend/src/features/parent-inference/ParentExclusion.tsx
@@ -3,6 +3,7 @@ import { Ban, RotateCcw, Loader2, Globe, Layers } from 'lucide-react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button';
+import { Input } from '@shared/components/ui/input';
import { Select } from '@shared/components/ui/select';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { useAuth } from '@/app/auth/AuthContext';
@@ -138,23 +139,26 @@ export function ParentExclusion() {
GROUP 제외 (특정 그룹 한정)
- {!canCreateGroup && 권한 없음 }
+ {!canCreateGroup && 권한 없음 }
- setGrpKey(e.target.value)} placeholder="group_key"
- className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGroup} />
- setGrpSub(e.target.value)} placeholder="sub"
- className="w-24 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGroup} />
- setGrpMmsi(e.target.value)} placeholder="excluded MMSI"
- className="w-40 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGroup} />
- setGrpReason(e.target.value)} placeholder="사유"
- className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGroup} />
-
- {busy === -1 ? : }
+ setGrpKey(e.target.value)} placeholder="group_key"
+ className="flex-1" disabled={!canCreateGroup} />
+ setGrpSub(e.target.value)} placeholder="sub"
+ className="w-24" disabled={!canCreateGroup} />
+ setGrpMmsi(e.target.value)} placeholder="excluded MMSI"
+ className="w-40" disabled={!canCreateGroup} />
+ setGrpReason(e.target.value)} placeholder="사유"
+ className="flex-1" disabled={!canCreateGroup} />
+ : }
+ >
제외
-
+
@@ -164,24 +168,27 @@ export function ParentExclusion() {
GLOBAL 제외 (모든 그룹 영구 차단, 관리자 권한)
- {!canCreateGlobal && 권한 없음 }
+ {!canCreateGlobal && 권한 없음 }
- setGlbMmsi(e.target.value)} placeholder="excluded MMSI"
- className="w-40 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGlobal} />
- setGlbReason(e.target.value)} placeholder="사유"
- className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGlobal} />
-
- {busy === -2 ? : }
+ setGlbMmsi(e.target.value)} placeholder="excluded MMSI"
+ className="w-40" disabled={!canCreateGlobal} />
+ setGlbReason(e.target.value)} placeholder="사유"
+ className="flex-1" disabled={!canCreateGlobal} />
+ : }
+ >
전역 제외
-
+
- {error && {tc('error.errorPrefix', { msg: error })}
}
+ {error && {tc('error.errorPrefix', { msg: error })}
}
{loading && (
@@ -220,13 +227,13 @@ export function ParentExclusion() {
{it.groupKey || '-'}
{it.subClusterId ?? '-'}
-
{it.excludedMmsi}
+
{it.excludedMmsi}
{it.reason || '-'}
{it.actorAcnt || '-'}
{formatDateTime(it.createdAt)}
handleRelease(it.id)}
- className="p-1 rounded hover:bg-blue-500/20 disabled:opacity-30 text-blue-400" title="해제">
+ className="p-1 rounded hover:bg-blue-500/20 disabled:opacity-30 text-blue-600 dark:text-blue-400" title="해제" aria-label="해제">
diff --git a/frontend/src/features/parent-inference/ParentReview.tsx b/frontend/src/features/parent-inference/ParentReview.tsx
index ad96e70..8f3e2a7 100644
--- a/frontend/src/features/parent-inference/ParentReview.tsx
+++ b/frontend/src/features/parent-inference/ParentReview.tsx
@@ -3,6 +3,7 @@ import { CheckCircle, XCircle, RotateCcw, Loader2, GitMerge } from 'lucide-react
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button';
+import { Input } from '@shared/components/ui/input';
import { Select } from '@shared/components/ui/select';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { useAuth } from '@/app/auth/AuthContext';
@@ -151,39 +152,40 @@ export function ParentReview() {
신규 모선 확정 등록 (테스트)
- setNewGroupKey(e.target.value)}
placeholder="group_key (예: 渔船A)"
- className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs"
+ className="flex-1"
/>
- setNewSubCluster(e.target.value)}
placeholder="sub_cluster_id"
- className="w-32 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs"
+ className="w-32"
/>
- setNewMmsi(e.target.value)}
placeholder="parent MMSI"
- className="w-40 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs"
+ className="w-40"
/>
- : }
>
- {actionLoading === -1 ? : }
확정 등록
-
+
@@ -192,7 +194,7 @@ export function ParentReview() {
{!canUpdate && (
-
+
조회 전용 모드 (UPDATE 권한 없음). 확정/거부/리셋 액션이 비활성화됩니다.
@@ -202,7 +204,7 @@ export function ParentReview() {
{error && (
- {tc('error.errorPrefix', { msg: error })}
+ {tc('error.errorPrefix', { msg: error })}
)}
@@ -247,39 +249,42 @@ export function ParentReview() {
{getParentResolutionLabel(it.status, tc, lang)}
- {it.selectedParentMmsi || '-'}
+ {it.selectedParentMmsi || '-'}
{formatDateTime(it.updatedAt)}
- handleAction(it, 'CONFIRM')}
- className="p-1 rounded hover:bg-green-500/20 disabled:opacity-30 text-green-400"
title="확정"
- >
-
-
- }
+ />
+ handleAction(it, 'REJECT')}
- className="p-1 rounded hover:bg-red-500/20 disabled:opacity-30 text-red-400"
title="거부"
- >
-
-
- }
+ />
+ handleAction(it, 'RESET')}
- className="p-1 rounded hover:bg-blue-500/20 disabled:opacity-30 text-blue-400"
title="리셋"
- >
-
-
+ aria-label="리셋"
+ className="text-blue-600 dark:text-blue-400 hover:bg-blue-500/20"
+ icon={ }
+ />
diff --git a/frontend/src/features/statistics/ReportManagement.tsx b/frontend/src/features/statistics/ReportManagement.tsx
index d0ee776..54d720d 100644
--- a/frontend/src/features/statistics/ReportManagement.tsx
+++ b/frontend/src/features/statistics/ReportManagement.tsx
@@ -121,8 +121,8 @@ export function ReportManagement() {
증거 {r.evidence}건
- PDF
- 한글
+ PDF
+ 한글
))}
@@ -136,9 +136,9 @@ export function ReportManagement() {
보고서 미리보기
-
- 다운로드
-
+
}>
+ 다운로드
+
diff --git a/frontend/src/features/surveillance/LiveMapView.tsx b/frontend/src/features/surveillance/LiveMapView.tsx
index 2b8f162..0d3282f 100644
--- a/frontend/src/features/surveillance/LiveMapView.tsx
+++ b/frontend/src/features/surveillance/LiveMapView.tsx
@@ -56,7 +56,7 @@ function RiskBar({ value, size = 'md' }: { value: number; size?: 'sm' | 'md' })
-
{value.toFixed(2)}
+
{value.toFixed(2)}
);
}
@@ -244,7 +244,7 @@ export function LiveMapView() {
{loading && (
-
+
로드 중...
)}
@@ -252,8 +252,8 @@ export function LiveMapView() {
{!serviceAvailable && !loading && (
-
- 분석 서비스 오프라인
+
+ 분석 서비스 오프라인
이벤트 데이터만 표시됩니다.
@@ -278,7 +278,7 @@ export function LiveMapView() {
{evt.type}
-
+
{evt.mmsi} · {evt.nationality} · {evt.time}
@@ -318,7 +318,7 @@ export function LiveMapView() {
{/* 실시간 표시 */}
-
LIVE
+
LIVE
경보 {mapEvents.length}건 · 분석 {vesselItems.length}척
@@ -333,7 +333,7 @@ export function LiveMapView() {
-
+
{selectedEvent.vesselName}
@@ -348,7 +348,7 @@ export function LiveMapView() {
위험도 점수
- {Math.round(selectedEvent.risk * 100)}
+ {Math.round(selectedEvent.risk * 100)}
/100
@@ -364,29 +364,29 @@ export function LiveMapView() {
-
+
AI 판단 근거
신뢰도: High
-
-
{selectedEvent.type}
+
+
{selectedEvent.type}
선박: {selectedEvent.vesselName} ({selectedEvent.mmsi})
좌표: {selectedEvent.lat.toFixed(4)}, {selectedEvent.lng.toFixed(4)}
-
- 발생 시각
+
+ 발생 시각
{selectedEvent.time}
diff --git a/frontend/src/features/surveillance/MapControl.tsx b/frontend/src/features/surveillance/MapControl.tsx
index 3927575..b6527ea 100644
--- a/frontend/src/features/surveillance/MapControl.tsx
+++ b/frontend/src/features/surveillance/MapControl.tsx
@@ -124,7 +124,7 @@ const NTM_DATA: NtmRecord[] = [
const NTM_CATEGORIES = ['전체', '사격훈련', '군사훈련', '기뢰제거', '해양공사', '항로표지', '항로변경', '해양오염', '수중작업'];
const ntmColumns: DataColumn
[] = [
- { key: 'no', label: '통보번호', width: '120px', sortable: true, render: v => {v as string} },
+ { key: 'no', label: '통보번호', width: '120px', sortable: true, render: v => {v as string} },
{ key: 'date', label: '발령일', width: '90px', sortable: true, render: v => {v as string} },
{ key: 'category', label: '구분', width: '70px', align: 'center', sortable: true,
render: v => {
@@ -146,7 +146,7 @@ const ntmColumns: DataColumn[] = [
// 훈련구역 색상은 trainingZoneTypes 카탈로그에서 lookup
const columns: DataColumn[] = [
- { key: 'id', label: '구역번호', width: '80px', sortable: true, render: v => {v as string} },
+ { key: 'id', label: '구역번호', width: '80px', sortable: true, render: v => {v as string} },
{ key: 'type', label: '구분', width: '60px', align: 'center', sortable: true,
render: v => {v as string} },
{ key: 'sea', label: '해역', width: '60px', sortable: true },
@@ -301,7 +301,7 @@ export function MapControl() {
{ key: 'ntm' as Tab, label: '항행통보', icon: Bell },
]).map(t => (
setTab(t.key)}
- className={`flex items-center gap-1.5 px-4 py-2 text-[11px] font-medium border-b-2 ${tab === t.key ? 'text-cyan-400 border-cyan-400' : 'text-hint border-transparent hover:text-label'}`}>
+ className={`flex items-center gap-1.5 px-4 py-2 text-[11px] font-medium border-b-2 ${tab === t.key ? 'text-cyan-600 dark:text-cyan-400 border-cyan-500 dark:border-cyan-400' : 'text-hint border-transparent hover:text-label'}`}>
{t.label}
))}
@@ -310,7 +310,7 @@ export function MapControl() {
{['', '서해', '남해', '동해', '제주'].map(s => (
setSeaFilter(s)}
- className={`px-2.5 py-1 rounded text-[10px] ${seaFilter === s ? 'bg-cyan-600 text-on-vivid font-bold' : 'text-hint hover:bg-surface-overlay'}`}>
+ className={`px-2.5 py-1 rounded text-[10px] ${seaFilter === s ? 'bg-cyan-600 hover:bg-cyan-500 text-on-vivid font-bold' : 'text-hint hover:bg-surface-overlay'}`}>
{s || '전체'}
))}
@@ -324,9 +324,9 @@ export function MapControl() {
{[
{ label: '전체 통보', value: NTM_DATA.length, color: 'text-heading' },
- { label: '발령중', value: NTM_DATA.filter(n => n.status === '발령중').length, color: 'text-red-400' },
+ { label: '발령중', value: NTM_DATA.filter(n => n.status === '발령중').length, color: 'text-red-600 dark:text-red-400' },
{ label: '해제', value: NTM_DATA.filter(n => n.status === '해제').length, color: 'text-muted-foreground' },
- { label: '사격훈련', value: NTM_DATA.filter(n => n.category.includes('사격')).length, color: 'text-orange-400' },
+ { label: '사격훈련', value: NTM_DATA.filter(n => n.category.includes('사격')).length, color: 'text-orange-600 dark:text-orange-400' },
].map(k => (
{k.value}
@@ -341,14 +341,14 @@ export function MapControl() {
구분:
{NTM_CATEGORIES.map(c => (
setNtmCatFilter(c === '전체' ? '' : c)}
- className={`px-2.5 py-1 rounded text-[10px] ${(c === '전체' && !ntmCatFilter) || ntmCatFilter === c ? 'bg-cyan-600 text-on-vivid font-bold' : 'text-hint hover:bg-surface-overlay'}`}>{c}
+ className={`px-2.5 py-1 rounded text-[10px] ${(c === '전체' && !ntmCatFilter) || ntmCatFilter === c ? 'bg-cyan-600 hover:bg-cyan-500 text-on-vivid font-bold' : 'text-hint hover:bg-surface-overlay'}`}>{c}
))}
{/* 최근 발령 중 통보 하이라이트 */}
-
현재 발령 중 항행통보
+
현재 발령 중 항행통보
{NTM_DATA.filter(n => n.status === '발령중').map(n => (
@@ -411,7 +411,7 @@ export function MapControl() {
{/* 표시 구역 수 */}
- {visibleZones.length}개
+ {visibleZones.length}개
훈련구역 표시 중