From 6fb0b049920c166deb45ec897f389fe54bafa60a Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 16 Apr 2026 15:19:55 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(prediction):=20=EA=B2=BD=EB=9F=89=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20riskScore=20=ED=95=B4=EC=83=81=EB=8F=84=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20+=20vessel=5Ftype=20=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 경량 경로 선박 60.8%가 45점 고정으로 수렴하고 98.6%가 vessel_type UNKNOWN 으로만 기록되던 문제를 해결한다. riskScore (compute_lightweight_risk_score) - dark_suspicion_score(0~100) 직접 반영: min(30, score*0.3) - EEZ_OR_BEYOND 기선 근접도 가산 (12NM 내 +15, 24NM 내 +8) - dark_history_24h 가산 (dark_suspicion_score 미반영 케이스만) - 허가 이력 +20 → +8/+15 차등 (dark_suspicion_score 있을 때 이중계산 방지) - gap_duration_min 4구간 차등 (fallback: 720m/180m/60m/30m) vessel_type (신규 vessel_type_mapping.py) - fleet_vessels fishery_code → VesselType 매핑: PT/PT-S/OT → TRAWL, GN → GILLNET, PS → PURSE, FC → CARGO - GILLNET / CARGO 2개 값 신규 추가 (기존 TRAWL/PURSE/LONGLINE/TRAP/UNKNOWN) - scheduler.py 경량 경로에서 등록선은 매핑, 미등록선은 UNKNOWN 유지 배포 후 검증 (redis-211 15:15 사이클) - risk_score 분포: 45점 60.8% → 0% (11~40 범위 고르게 분산) - vessel_type: UNKNOWN 98.6% → 89.1% (886척이 구체 유형으로 전환, TRAWL 518 / LONGLINE 171 / TRAP 78 / PURSE 73 / GILLNET 38 / CARGO 8) - 412354335 샘플: 45 MEDIUM 고정 → 20 LOW (dss=40 × 0.3 + 축소 허가) --- prediction/algorithms/risk.py | 53 +++++++++++++++----- prediction/algorithms/vessel_type_mapping.py | 27 ++++++++++ prediction/scheduler.py | 15 +++++- 3 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 prediction/algorithms/vessel_type_mapping.py diff --git a/prediction/algorithms/risk.py b/prediction/algorithms/risk.py index 4f2ef32..61cfd32 100644 --- a/prediction/algorithms/risk.py +++ b/prediction/algorithms/risk.py @@ -14,42 +14,71 @@ def compute_lightweight_risk_score( is_dark: bool = False, gap_duration_min: int = 0, spoofing_score: float = 0.0, + dark_suspicion_score: int = 0, + dist_from_baseline_nm: float = 999.0, + dark_history_24h: int = 0, ) -> Tuple[int, str]: """위치·허가·다크/스푸핑 기반 경량 위험도 (파이프라인 미통과 선박용). - pipeline path의 compute_vessel_risk_score와 동일한 임계값(70/50/30)을 사용해 - 분류 결과의 일관성을 유지한다. dark/spoofing 신호를 추가하여 max 100점 도달 가능. + compute_dark_suspicion 의 패턴 기반 0~100 점수를 직접 반영해 해상도를 높인다. + 이중계산 방지: dark_suspicion_score 는 이미 무허가/반복을 포함하므로 dark_suspicion_score > 0 + 인 경우 허가/반복 가산을 축소한다. + + 임계값 70/50/30 은 pipeline path(compute_vessel_risk_score)와 동일. Returns: (risk_score, risk_level) """ score = 0 - # 1. 위치 기반 (최대 40점) + # 1. 위치 기반 (최대 40점) — EEZ 외 기선 근접도 추가 zone = zone_info.get('zone', '') if zone == 'TERRITORIAL_SEA': score += 40 elif zone == 'CONTIGUOUS_ZONE': - score += 10 + score += 15 elif zone.startswith('ZONE_'): if is_permitted is not None and not is_permitted: score += 25 + elif zone == 'EEZ_OR_BEYOND': + # EEZ 외라도 기선 근접 시 가산 (공해·외해 분산) + if dist_from_baseline_nm < 12: + score += 15 + elif dist_from_baseline_nm < 24: + score += 8 - # 2. 다크 베셀 (최대 25점) + # 2. 다크 베셀 (최대 30점) — dark_suspicion_score 우선 if is_dark: - if gap_duration_min >= 60: - score += 25 - elif gap_duration_min >= 30: - score += 10 + if dark_suspicion_score >= 1: + # compute_dark_suspicion 이 산출한 패턴 기반 의심도 반영 + score += min(30, round(dark_suspicion_score * 0.3)) + else: + # fallback: gap 길이만 기준 + if gap_duration_min >= 720: + score += 25 + elif gap_duration_min >= 180: + score += 20 + elif gap_duration_min >= 60: + score += 15 + elif gap_duration_min >= 30: + score += 8 - # 3. 스푸핑 (최대 15점) + # 3. 스푸핑 (최대 15점) — 현재 중국 선박은 거의 0 (별도 PR 에서 산출 로직 재설계 예정) if spoofing_score > 0.7: score += 15 elif spoofing_score > 0.5: score += 8 - # 4. 허가 이력 (최대 20점) + # 4. 허가 이력 (최대 15점) — 이중계산 방지 if is_permitted is not None and not is_permitted: - score += 20 + # dark_suspicion_score 에 이미 무허가 +10 반영됨 → 축소 + score += 8 if dark_suspicion_score > 0 else 15 + + # 5. 반복 이력 (최대 10점) — dark_suspicion_score 미반영 케이스만 + if dark_suspicion_score == 0 and dark_history_24h > 0: + if dark_history_24h >= 5: + score += 10 + elif dark_history_24h >= 2: + score += 5 score = min(score, 100) diff --git a/prediction/algorithms/vessel_type_mapping.py b/prediction/algorithms/vessel_type_mapping.py new file mode 100644 index 0000000..0ca4d63 --- /dev/null +++ b/prediction/algorithms/vessel_type_mapping.py @@ -0,0 +1,27 @@ +"""한중어업협정 fishery_code → VesselType 매핑. + +파이프라인 미통과 선박(경량 분석 경로)은 AIS 샘플 부족으로 분류기가 UNKNOWN 을 반환한다. +등록선은 fishery_code 가 이미 확정이므로 이를 활용해 vessel_type 을 채운다. + +VesselType 값 확장: + 기존: TRAWL / PURSE / LONGLINE / TRAP / UNKNOWN + 신규: GILLNET (유자망) / CARGO (운반선) +""" +from typing import Optional + + +FISHERY_CODE_TO_VESSEL_TYPE = { + 'PT': 'TRAWL', # 쌍끌이 저인망 + 'PT-S': 'TRAWL', # 쌍끌이 부속선 + 'OT': 'TRAWL', # 단선 저인망 + 'GN': 'GILLNET', # 유자망 + 'PS': 'PURSE', # 대형선망/위망 + 'FC': 'CARGO', # 운반선 (조업 아님) +} + + +def fishery_code_to_vessel_type(fishery_code: Optional[str]) -> str: + """등록 어업 코드 → 선박 유형. 매칭 없으면 'UNKNOWN'.""" + if not fishery_code: + return 'UNKNOWN' + return FISHERY_CODE_TO_VESSEL_TYPE.get(fishery_code.upper(), 'UNKNOWN') diff --git a/prediction/scheduler.py b/prediction/scheduler.py index 91a786b..8661d02 100644 --- a/prediction/scheduler.py +++ b/prediction/scheduler.py @@ -507,6 +507,7 @@ def run_analysis_cycle(): # ── 5.5 경량 분석 — 파이프라인 미통과 412* 선박 ── # vessel_store._tracks의 24h 누적 궤적을 직접 활용하여 dark/spoof 신호도 산출. from algorithms.risk import compute_lightweight_risk_score + from algorithms.vessel_type_mapping import fishery_code_to_vessel_type pipeline_mmsis = {c['mmsi'] for c in classifications} lightweight_mmsis = vessel_store.get_chinese_mmsis() - pipeline_mmsis @@ -607,17 +608,29 @@ def run_analysis_cycle(): if spoof_score > 0.5: lw_spoof += 1 + # dark_features 에 저장된 패턴 기반 점수 + 반복 이력을 리스크 산출에 직접 연결 + # (경량 경로가 45점 포화되던 원인 해소) risk_score, risk_level = compute_lightweight_risk_score( zone_info, sog, is_permitted=is_permitted, is_dark=dark, gap_duration_min=gap_min, spoofing_score=spoof_score, + dark_suspicion_score=int(dark_features.get('dark_suspicion_score', 0) or 0), + dist_from_baseline_nm=float(zone_info.get('dist_from_baseline_nm', 999.0) or 999.0), + dark_history_24h=int(dark_features.get('dark_history_24h', 0) or 0), ) + # 등록선은 fishery_code 로 vessel_type 채움 (미등록선은 UNKNOWN 유지) + registered_fc = ( + fleet_tracker.get_registered_fishery_code(mmsi) + if hasattr(fleet_tracker, 'get_registered_fishery_code') else None + ) + vessel_type = fishery_code_to_vessel_type(registered_fc) + # BD-09 오프셋은 중국 선박이므로 제외 (412* = 중국) results.append(AnalysisResult( mmsi=mmsi, timestamp=ts, - vessel_type='UNKNOWN', + vessel_type=vessel_type, confidence=0.0, fishing_pct=0.0, lat=float(lat) if lat is not None else None, -- 2.45.2 From 524df19f205660be511012578dbf6f0bf4f9d42a Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 16 Apr 2026 15:20:08 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(frontend):=20=EC=84=A0=EB=B0=95=20?= =?UTF-8?q?=EC=9C=A0=ED=98=95=20=ED=95=9C=EA=B8=80=20=EC=B9=B4=ED=83=88?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20+=20=EC=A4=91=EA=B5=AD=20=EC=84=A0?= =?UTF-8?q?=EB=B0=95=20=EB=B6=84=EC=84=9D=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=ED=95=A9=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vessel_type 카탈로그 - shared/constants/vesselTypes.ts 신규 — TRAWL/PURSE/GILLNET/LONGLINE/ TRAP/CARGO/UNKNOWN 7종 + getVesselTypeLabel / getVesselTypeIntent 헬퍼. 기존 alertLevels 카탈로그 패턴 답습 - catalogRegistry 에 VESSEL_TYPES 등록 — design-system 쇼케이스에 자동 노출 RealVesselAnalysis 필터 props 확장 - Props 에 mmsiPrefix / minRiskScore / size 추가 (all·spoofing mode) - 선박 유형 컬럼을 한글 라벨로 렌더 - RealAllVessels 편의 export 를 mmsiPrefix='412' 로 고정 + 제목을 '중국 선박 전체 분석 결과 (실시간)' 로 변경 효과 - Tab 1 상단 그리드가 중국 선박만 표시해 페이지 성격과 일치 - 선박 유형 '저인망/선망/유자망/연승/통발/운반선/미분류' 한글 표시 - 55점 HIGH 같은 중국 선박이 상단/하단 양쪽에 일관되게 노출 --- .../src/features/detection/ChinaFishing.tsx | 22 ++++++-- .../features/detection/RealVesselAnalysis.tsx | 36 ++++++++++--- .../src/shared/constants/catalogRegistry.ts | 10 ++++ frontend/src/shared/constants/vesselTypes.ts | 53 +++++++++++++++++++ 4 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 frontend/src/shared/constants/vesselTypes.ts diff --git a/frontend/src/features/detection/ChinaFishing.tsx b/frontend/src/features/detection/ChinaFishing.tsx index aacb328..4f0ee86 100644 --- a/frontend/src/features/detection/ChinaFishing.tsx +++ b/frontend/src/features/detection/ChinaFishing.tsx @@ -9,6 +9,7 @@ import { } from 'lucide-react'; import { formatDateTime } from '@shared/utils/dateFormat'; import { ALERT_LEVELS, getAlertLevelLabel, type AlertLevel } from '@shared/constants/alertLevels'; +import { getVesselTypeLabel } from '@shared/constants/vesselTypes'; import { getVesselRingMeta } from '@shared/constants/vesselAnalysisStatuses'; import { useSettingsStore } from '@stores/settingsStore'; import { useTranslation } from 'react-i18next'; @@ -52,18 +53,29 @@ function deriveVesselStatus(score: number): VesselStatus { return '양호'; } -function mapToVesselItem(item: VesselAnalysisItem, idx: number): VesselItem { +function mapToVesselItem( + item: VesselAnalysisItem, + idx: number, + t: (k: string, opts?: { defaultValue?: string }) => string, + lang: 'ko' | 'en', +): VesselItem { const score = item.algorithms.riskScore.score; const vt = item.classification.vesselType; const hasType = vt && vt !== 'UNKNOWN' && vt !== ''; + // 이름: fleet_vessels 매핑으로 vessel_type 이 채워진 경우 한글 유형 라벨, 아니면 '중국어선' + const name = hasType ? getVesselTypeLabel(vt, t, lang) : '중국어선'; + // 타입 뱃지: fishingPct 기반 Fishing / 그 외는 vessel_type 라벨 + const type = item.classification.fishingPct > 0.5 + ? 'Fishing' + : hasType ? getVesselTypeLabel(vt, t, lang) : getVesselTypeLabel('UNKNOWN', t, lang); return { id: String(idx + 1), mmsi: item.mmsi, callSign: '-', channel: '', source: 'AIS', - name: hasType ? vt : '중국어선', - type: item.classification.fishingPct > 0.5 ? 'Fishing' : hasType ? 'Cargo' : '미분류', + name, + type, country: 'China', status: deriveVesselStatus(score), riskPct: score, @@ -289,8 +301,8 @@ export function ChinaFishing() { // 특이운항 선박 리스트 (서버에서 이미 riskScore >= 40 로 필터링된 상위 20척) const vesselList: VesselItem[] = useMemo( - () => topVessels.map((item, idx) => mapToVesselItem(item, idx)), - [topVessels], + () => topVessels.map((item, idx) => mapToVesselItem(item, idx, tcCommon, lang)), + [topVessels, tcCommon, lang], ); // 위험도별 분포 (도넛 차트용) — apiStats 기반 diff --git a/frontend/src/features/detection/RealVesselAnalysis.tsx b/frontend/src/features/detection/RealVesselAnalysis.tsx index 1c650a4..c22e88f 100644 --- a/frontend/src/features/detection/RealVesselAnalysis.tsx +++ b/frontend/src/features/detection/RealVesselAnalysis.tsx @@ -1,8 +1,10 @@ import { useEffect, useState, useCallback, useMemo } from 'react'; 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 { getAlertLevelIntent } from '@shared/constants/alertLevels'; +import { getVesselTypeLabel } from '@shared/constants/vesselTypes'; import type { VesselAnalysisItem } from '@/services/vesselAnalysisApi'; import { getAnalysisVessels, @@ -21,6 +23,12 @@ interface Props { mode: 'dark' | 'spoofing' | 'transship' | 'all'; title: string; icon?: React.ReactNode; + /** 'all' / 'spoofing' mode 에서 MMSI prefix 필터 (예: '412' — 중국 선박 한정) */ + mmsiPrefix?: string; + /** 'all' / 'spoofing' mode 에서 서버 측 최소 riskScore 필터 */ + minRiskScore?: number; + /** 서버 조회 건수 (dark/transship 기본 200) */ + size?: number; } const ZONE_LABELS: Record = { @@ -40,7 +48,9 @@ const ENDPOINT_LABEL: Record = { spoofing: 'GET /api/analysis/vessels (spoofing_score ≥ 0.3)', }; -export function RealVesselAnalysis({ mode, title, icon }: Props) { +export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore, size = 200 }: Props) { + const { t, i18n } = useTranslation('common'); + const lang = (i18n.language as 'ko' | 'en') || 'ko'; const [items, setItems] = useState([]); const [available, setAvailable] = useState(true); const [loading, setLoading] = useState(false); @@ -51,10 +61,10 @@ export function RealVesselAnalysis({ mode, title, icon }: Props) { setLoading(true); setError(''); try { const page = mode === 'dark' - ? await getDarkVessels({ hours: 1, size: 200 }) + ? await getDarkVessels({ hours: 1, size }) : mode === 'transship' - ? await getTransshipSuspects({ hours: 1, size: 200 }) - : await getAnalysisVessels({ hours: 1, size: 200 }); + ? await getTransshipSuspects({ hours: 1, size }) + : await getAnalysisVessels({ hours: 1, size, mmsiPrefix, minRiskScore }); setItems(page.content.map(toVesselItem)); setAvailable(true); } catch (e: unknown) { @@ -63,7 +73,7 @@ export function RealVesselAnalysis({ mode, title, icon }: Props) { } finally { setLoading(false); } - }, [mode]); + }, [mode, mmsiPrefix, minRiskScore, size]); useEffect(() => { load(); }, [load]); @@ -160,8 +170,10 @@ export function RealVesselAnalysis({ mode, title, icon }: Props) { {v.mmsi} - {v.classification.vesselType} - ({(v.classification.confidence * 100).toFixed(0)}%) + {getVesselTypeLabel(v.classification.vesselType, t, lang)} + {v.classification.confidence > 0 && ( + ({(v.classification.confidence * 100).toFixed(0)}%) + )} @@ -221,4 +233,12 @@ function StatBox({ label, value, color }: { label: string; value: number | undef export const RealDarkVessels = () => } />; export const RealSpoofingVessels = () => } />; export const RealTransshipSuspects = () => } />; -export const RealAllVessels = () => } />; +// 중국 선박 감시 페이지 전용 — MMSI prefix 412 고정 +export const RealAllVessels = () => ( + } + mmsiPrefix="412" + /> +); diff --git a/frontend/src/shared/constants/catalogRegistry.ts b/frontend/src/shared/constants/catalogRegistry.ts index 658318d..2fa9e7b 100644 --- a/frontend/src/shared/constants/catalogRegistry.ts +++ b/frontend/src/shared/constants/catalogRegistry.ts @@ -43,6 +43,7 @@ import { MLOPS_JOB_STATUSES } from './mlopsJobStatuses'; import { THREAT_LEVELS, AGENT_PERM_TYPES, AGENT_EXEC_RESULTS } from './aiSecurityStatuses'; import { ZONE_CODES } from './zoneCodes'; import { GEAR_VIOLATION_CODES } from './gearViolationCodes'; +import { VESSEL_TYPES } from './vesselTypes'; /** * 카탈로그 공통 메타 — 쇼케이스 렌더와 UI 일관성을 위한 최소 스키마 @@ -91,6 +92,15 @@ export const CATALOG_REGISTRY: CatalogEntry[] = [ source: 'backend ViolationType enum', items: VIOLATION_TYPES, }, + { + id: 'vessel-type', + showcaseId: 'TRK-CAT-vessel-type', + titleKo: '선박 유형', + titleEn: 'Vessel Type', + description: 'TRAWL / PURSE / GILLNET / LONGLINE / TRAP / CARGO / UNKNOWN — prediction 분류 + fleet_vessels 매핑', + source: 'prediction AnalysisResult.vessel_type', + items: VESSEL_TYPES, + }, { id: 'event-status', showcaseId: 'TRK-CAT-event-status', diff --git a/frontend/src/shared/constants/vesselTypes.ts b/frontend/src/shared/constants/vesselTypes.ts new file mode 100644 index 0000000..9cbe7a8 --- /dev/null +++ b/frontend/src/shared/constants/vesselTypes.ts @@ -0,0 +1,53 @@ +/** + * 선박 유형 공통 카탈로그 + * + * SSOT: prediction `AnalysisResult.vessel_type` (TRAWL / PURSE / LONGLINE / TRAP / GILLNET / CARGO / UNKNOWN). + * prediction 은 분류 파이프라인 결과 + fleet_vessels 등록 fishery_code → vessel_type 매핑으로 값을 채운다. + * + * 사용처: RealVesselAnalysis '선박 유형' 컬럼, ChinaFishing 특이운항 리스트, + * VesselDetail / EventList 등 선박 유형 배지. + */ + +import type { BadgeIntent } from '@lib/theme/variants'; + +export type VesselTypeCode = + | 'TRAWL' + | 'PURSE' + | 'LONGLINE' + | 'TRAP' + | 'GILLNET' + | 'CARGO' + | 'UNKNOWN'; + +export interface VesselTypeMeta { + code: VesselTypeCode; + i18nKey: string; + fallback: { ko: string; en: string }; + intent: BadgeIntent; +} + +export const VESSEL_TYPES: Record = { + TRAWL: { code: 'TRAWL', i18nKey: 'vesselType.trawl', fallback: { ko: '저인망', en: 'Trawl' }, intent: 'warning' }, + PURSE: { code: 'PURSE', i18nKey: 'vesselType.purse', fallback: { ko: '선망', en: 'Purse Seine' }, intent: 'warning' }, + GILLNET: { code: 'GILLNET', i18nKey: 'vesselType.gillnet', fallback: { ko: '유자망', en: 'Gillnet' }, intent: 'warning' }, + LONGLINE: { code: 'LONGLINE', i18nKey: 'vesselType.longline', fallback: { ko: '연승', en: 'Longline' }, intent: 'info' }, + TRAP: { code: 'TRAP', i18nKey: 'vesselType.trap', fallback: { ko: '통발', en: 'Trap' }, intent: 'info' }, + CARGO: { code: 'CARGO', i18nKey: 'vesselType.cargo', fallback: { ko: '운반선', en: 'Cargo' }, intent: 'muted' }, + UNKNOWN: { code: 'UNKNOWN', i18nKey: 'vesselType.unknown', fallback: { ko: '미분류', en: 'Unknown' }, intent: 'muted' }, +}; + +export function getVesselTypeLabel( + code: string | null | undefined, + t: (k: string, opts?: { defaultValue?: string }) => string, + lang: 'ko' | 'en' = 'ko', +): string { + if (!code) return t(VESSEL_TYPES.UNKNOWN.i18nKey, { defaultValue: VESSEL_TYPES.UNKNOWN.fallback[lang] }); + const meta = VESSEL_TYPES[code as VesselTypeCode]; + if (!meta) return code; + return t(meta.i18nKey, { defaultValue: meta.fallback[lang] }); +} + +export function getVesselTypeIntent(code: string | null | undefined): BadgeIntent { + if (!code) return VESSEL_TYPES.UNKNOWN.intent; + return VESSEL_TYPES[code as VesselTypeCode]?.intent ?? 'muted'; +} -- 2.45.2 From 3372d0654591caf7205ee9703cae78b437af2c0d Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 16 Apr 2026 15:22:51 +0900 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 7055548..1fd4648 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,15 @@ ## [Unreleased] +### 변경 +- **경량 분석 riskScore 해상도 개선** — `compute_lightweight_risk_score` 에 `dark_suspicion_score`(0~100 패턴 기반 의심도) / `dist_from_baseline_nm`(EEZ 외 기선 근접도 12·24NM 차등) / `dark_history_24h`(반복 이력) 반영. 허가·반복 이중계산 방지 축소 로직. 배포 후 실측: 45점 60.8% 고정 수렴 → **0%** (11~40 구간 고르게 분산) +- **vessel_type 매핑** — fleet_vessels 등록선 `fishery_code` (PT/PT-S/OT/GN/PS/FC) 를 `TRAWL/GILLNET/PURSE/CARGO` 로 매핑하는 `vessel_type_mapping.py` 신설. 경량 경로의 `vessel_type='UNKNOWN'` 하드코딩 제거. 실측: UNKNOWN 98.6% → **89.1%** (886척이 구체 유형으로 전환) +- **VesselType 값 확장** — 기존 TRAWL/PURSE/LONGLINE/TRAP/UNKNOWN 에 `GILLNET`(유자망) / `CARGO`(운반선) 2종 추가 +- **중국 선박 분석 그리드 정합성** — Tab 1 상단 `RealAllVessels` 편의 export 를 `mmsiPrefix='412'` 로 고정 + 제목 "중국 선박 전체 분석 결과 (실시간)" 로 변경. 상단/하단 모두 중국 선박 기준으로 일관 표시 + +### 추가 +- **선박 유형 한글 카탈로그** — `shared/constants/vesselTypes.ts` 신설. 저인망/선망/유자망/연승/통발/운반선/미분류 한글 라벨 + Badge intent. 기존 `alertLevels` 패턴 답습, `catalogRegistry` 등록으로 design-system 쇼케이스 자동 노출 + ## [2026-04-16.6] ### 추가 -- 2.45.2 From 9063095a9b598869e2e5ecc1d619cbb6e9f0d6f8 Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 16 Apr 2026 15:25:36 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=A0=95=EB=A6=AC=20(2026-04-16.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 1fd4648..bbfbed8 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,8 @@ ## [Unreleased] +## [2026-04-16.7] + ### 변경 - **경량 분석 riskScore 해상도 개선** — `compute_lightweight_risk_score` 에 `dark_suspicion_score`(0~100 패턴 기반 의심도) / `dist_from_baseline_nm`(EEZ 외 기선 근접도 12·24NM 차등) / `dark_history_24h`(반복 이력) 반영. 허가·반복 이중계산 방지 축소 로직. 배포 후 실측: 45점 60.8% 고정 수렴 → **0%** (11~40 구간 고르게 분산) - **vessel_type 매핑** — fleet_vessels 등록선 `fishery_code` (PT/PT-S/OT/GN/PS/FC) 를 `TRAWL/GILLNET/PURSE/CARGO` 로 매핑하는 `vessel_type_mapping.py` 신설. 경량 경로의 `vessel_type='UNKNOWN'` 하드코딩 제거. 실측: UNKNOWN 98.6% → **89.1%** (886척이 구체 유형으로 전환) -- 2.45.2