diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 9b5eb3c..9ab9c5e 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,13 @@ ## [Unreleased] +### 변경 +- 현장분석: AI 파이프라인 더미 애니메이션 → analysisMap 기반 ON/OFF 실상태 표시 +- 현장분석: BD-09 변환 STANDBY → bd09OffsetM 실측 탐지 수 표시 +- 보고서: 수역별 허가업종 하드코딩 → ZONE_ALLOWED 상수 동적 참조 +- 보고서: 건의사항 월/최대 어구 선단 실데이터 연동 +- 보고서 버튼: 상단 헤더 → 현장분석 내부 닫기 버튼 좌측으로 이동 + ## [2026-03-25] ### 추가 diff --git a/frontend/src/components/korea/FieldAnalysisModal.tsx b/frontend/src/components/korea/FieldAnalysisModal.tsx index d2eb546..fdf0abd 100644 --- a/frontend/src/components/korea/FieldAnalysisModal.tsx +++ b/frontend/src/components/korea/FieldAnalysisModal.tsx @@ -110,6 +110,7 @@ interface Props { ships: Ship[]; vesselAnalysis?: UseVesselAnalysisResult; onClose: () => void; + onShowReport?: () => void; } const PIPE_STEPS = [ @@ -124,14 +125,14 @@ const PIPE_STEPS = [ const ALERT_ORDER: Record = { CRITICAL: 0, WATCH: 1, MONITOR: 2, NORMAL: 3 }; -export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) { +export function FieldAnalysisModal({ ships, vesselAnalysis, onClose, onShowReport }: Props) { const emptyMap = useMemo(() => new Map(), []); const analysisMap = vesselAnalysis?.analysisMap ?? emptyMap; const [activeFilter, setActiveFilter] = useState('ALL'); const [search, setSearch] = useState(''); const [selectedMmsi, setSelectedMmsi] = useState(null); const [logs, setLogs] = useState([]); - const [pipeStep, setPipeStep] = useState(0); + // pipeStep 제거 — 파이프라인 상태는 analysisMap 존재 여부로 판단 const [tick, setTick] = useState(0); // 중국 어선만 필터 @@ -189,9 +190,13 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) { // 통계 — Python 분석 결과 기반 const stats = useMemo(() => { let gpsAnomaly = 0; + let bd09Detected = 0; for (const v of processed) { const dto = analysisMap.get(v.ship.mmsi); - if (dto && dto.algorithms.gpsSpoofing.spoofingScore > 0.5) gpsAnomaly++; + if (dto) { + if (dto.algorithms.gpsSpoofing.spoofingScore > 0.5) gpsAnomaly++; + if (dto.algorithms.gpsSpoofing.bd09OffsetM > 100) bd09Detected++; + } } return { total: processed.length, @@ -199,6 +204,7 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) { fishing: processed.filter(v => v.state === 'FISHING').length, aisLoss: processed.filter(v => v.state === 'AIS_LOSS' || v.state === 'UNKNOWN').length, gpsAnomaly, + bd09Detected, clusters: new Set(processed.filter(v => v.cluster !== '—').map(v => v.cluster)).size, trawl: processed.filter(v => v.vtype === 'TRAWL').length, purse: processed.filter(v => v.vtype === 'PURSE').length, @@ -231,12 +237,6 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // AI 파이프라인 애니메이션 - useEffect(() => { - const t = setInterval(() => setPipeStep(s => s + 1), 1200); - return () => clearInterval(t); - }, []); - // 시계 tick useEffect(() => { const t = setInterval(() => setTick(s => s + 1), 1000); @@ -349,6 +349,19 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) { LIVE {new Date().toLocaleTimeString('ko-KR')} + {onShowReport && ( + + )} -