feat: 중국어선감시 KoreaFilters 통합 + 필터 배지 클릭 선박목록/CSV 다운로드
- cnFishing을 KoreaFilters 인터페이스에 통합 (koreaLayers → koreaFilters) → 다른 필터 탭과 동일한 선박 비활성화/상단 배지/카운트 동작 - 상단 필터 배지 클릭 → 대상 선박 목록 패널 (MMSI/이름/국적/유형/속도) → 선박 클릭 시 flyTo, 200척까지 표시 - CSV 다운로드: BOM 포함 UTF-8, 필터별 파일명 (e.g. cnFishing_2026-03-23.csv) - cnFishingSuspects Set 추가 (useKoreaFilters 반환값) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
18b827ced0
커밋
0da477c53c
@ -159,7 +159,6 @@ export const KoreaDashboard = ({
|
|||||||
koreaData.visibleShips,
|
koreaData.visibleShips,
|
||||||
currentTime,
|
currentTime,
|
||||||
vesselAnalysis.analysisMap,
|
vesselAnalysis.analysisMap,
|
||||||
koreaLayers.cnFishing,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTabChange = useCallback((_tab: DashboardTab) => {
|
const handleTabChange = useCallback((_tab: DashboardTab) => {
|
||||||
@ -198,8 +197,8 @@ export const KoreaDashboard = ({
|
|||||||
onClick={() => koreaFiltersResult.setFilter('ferryWatch', !koreaFiltersResult.filters.ferryWatch)} title={t('filters.ferryWatch')}>
|
onClick={() => koreaFiltersResult.setFilter('ferryWatch', !koreaFiltersResult.filters.ferryWatch)} title={t('filters.ferryWatch')}>
|
||||||
<span className="text-[11px]">🚢</span>{t('filters.ferryWatch')}
|
<span className="text-[11px]">🚢</span>{t('filters.ferryWatch')}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={`mode-btn ${koreaLayers.cnFishing ? 'active live' : ''}`}
|
<button type="button" className={`mode-btn ${koreaFiltersResult.filters.cnFishing ? 'active live' : ''}`}
|
||||||
onClick={() => toggleKoreaLayer('cnFishing')} title="중국어선감시">
|
onClick={() => koreaFiltersResult.setFilter('cnFishing', !koreaFiltersResult.filters.cnFishing)} title="중국어선감시">
|
||||||
<span className="text-[11px]">🎣</span>중국어선감시
|
<span className="text-[11px]">🎣</span>중국어선감시
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={`mode-btn ${showFieldAnalysis ? 'active' : ''}`}
|
<button type="button" className={`mode-btn ${showFieldAnalysis ? 'active' : ''}`}
|
||||||
@ -239,6 +238,7 @@ export const KoreaDashboard = ({
|
|||||||
transshipSuspects={koreaFiltersResult.transshipSuspects}
|
transshipSuspects={koreaFiltersResult.transshipSuspects}
|
||||||
cableWatchSuspects={koreaFiltersResult.cableWatchSuspects}
|
cableWatchSuspects={koreaFiltersResult.cableWatchSuspects}
|
||||||
dokdoWatchSuspects={koreaFiltersResult.dokdoWatchSuspects}
|
dokdoWatchSuspects={koreaFiltersResult.dokdoWatchSuspects}
|
||||||
|
cnFishingSuspects={koreaFiltersResult.cnFishingSuspects}
|
||||||
dokdoAlerts={koreaFiltersResult.dokdoAlerts}
|
dokdoAlerts={koreaFiltersResult.dokdoAlerts}
|
||||||
vesselAnalysis={vesselAnalysis}
|
vesselAnalysis={vesselAnalysis}
|
||||||
hiddenShipCategories={hiddenShipCategories}
|
hiddenShipCategories={hiddenShipCategories}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export interface KoreaFiltersState {
|
|||||||
cableWatch: boolean;
|
cableWatch: boolean;
|
||||||
dokdoWatch: boolean;
|
dokdoWatch: boolean;
|
||||||
ferryWatch: boolean;
|
ferryWatch: boolean;
|
||||||
|
cnFishing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -59,6 +60,7 @@ interface Props {
|
|||||||
transshipSuspects: Set<string>;
|
transshipSuspects: Set<string>;
|
||||||
cableWatchSuspects: Set<string>;
|
cableWatchSuspects: Set<string>;
|
||||||
dokdoWatchSuspects: Set<string>;
|
dokdoWatchSuspects: Set<string>;
|
||||||
|
cnFishingSuspects: Set<string>;
|
||||||
dokdoAlerts: { mmsi: string; name: string; dist: number; time: number }[];
|
dokdoAlerts: { mmsi: string; name: string; dist: number; time: number }[];
|
||||||
vesselAnalysis?: UseVesselAnalysisResult;
|
vesselAnalysis?: UseVesselAnalysisResult;
|
||||||
hiddenShipCategories?: Set<string>;
|
hiddenShipCategories?: Set<string>;
|
||||||
@ -115,6 +117,7 @@ const FILTER_ICON: Record<string, string> = {
|
|||||||
cableWatch: '\u{1F50C}',
|
cableWatch: '\u{1F50C}',
|
||||||
dokdoWatch: '\u{1F3DD}\uFE0F',
|
dokdoWatch: '\u{1F3DD}\uFE0F',
|
||||||
ferryWatch: '\u{1F6A2}',
|
ferryWatch: '\u{1F6A2}',
|
||||||
|
cnFishing: '\u{1F3A3}',
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILTER_COLOR: Record<string, string> = {
|
const FILTER_COLOR: Record<string, string> = {
|
||||||
@ -124,6 +127,7 @@ const FILTER_COLOR: Record<string, string> = {
|
|||||||
cableWatch: '#00e5ff',
|
cableWatch: '#00e5ff',
|
||||||
dokdoWatch: '#22c55e',
|
dokdoWatch: '#22c55e',
|
||||||
ferryWatch: '#2196f3',
|
ferryWatch: '#2196f3',
|
||||||
|
cnFishing: '#f59e0b',
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILTER_I18N_KEY: Record<string, string> = {
|
const FILTER_I18N_KEY: Record<string, string> = {
|
||||||
@ -133,6 +137,7 @@ const FILTER_I18N_KEY: Record<string, string> = {
|
|||||||
cableWatch: 'filters.cableWatchMonitor',
|
cableWatch: 'filters.cableWatchMonitor',
|
||||||
dokdoWatch: 'filters.dokdoWatchMonitor',
|
dokdoWatch: 'filters.dokdoWatchMonitor',
|
||||||
ferryWatch: 'filters.ferryWatchMonitor',
|
ferryWatch: 'filters.ferryWatchMonitor',
|
||||||
|
cnFishing: 'filters.cnFishingMonitor',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts, vesselAnalysis, hiddenShipCategories, hiddenNationalities }: Props) {
|
export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts, vesselAnalysis, hiddenShipCategories, hiddenNationalities }: Props) {
|
||||||
@ -156,6 +161,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
const [staticPickInfo, setStaticPickInfo] = useState<StaticPickInfo | null>(null);
|
const [staticPickInfo, setStaticPickInfo] = useState<StaticPickInfo | null>(null);
|
||||||
const handleStaticPick = useCallback((info: StaticPickInfo) => setStaticPickInfo(info), []);
|
const handleStaticPick = useCallback((info: StaticPickInfo) => setStaticPickInfo(info), []);
|
||||||
const [analysisPanelOpen, setAnalysisPanelOpen] = useLocalStorage('analysisPanelOpen', false);
|
const [analysisPanelOpen, setAnalysisPanelOpen] = useLocalStorage('analysisPanelOpen', false);
|
||||||
|
const [activeBadgeFilter, setActiveBadgeFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchKoreaInfra().then(setInfra).catch(() => {});
|
fetchKoreaInfra().then(setInfra).catch(() => {});
|
||||||
@ -189,7 +195,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const anyKoreaFilterOn = koreaFilters.illegalFishing || koreaFilters.illegalTransship || koreaFilters.darkVessel || koreaFilters.cableWatch || koreaFilters.dokdoWatch || koreaFilters.ferryWatch;
|
const anyKoreaFilterOn = koreaFilters.illegalFishing || koreaFilters.illegalTransship || koreaFilters.darkVessel || koreaFilters.cableWatch || koreaFilters.dokdoWatch || koreaFilters.ferryWatch || koreaFilters.cnFishing;
|
||||||
|
|
||||||
// 줌 레벨별 아이콘/심볼 스케일 배율 — z4=0.8x, z6=1.0x 시작, 2단계씩 상향
|
// 줌 레벨별 아이콘/심볼 스케일 배율 — z4=0.8x, z6=1.0x 시작, 2단계씩 상향
|
||||||
const zoomScale = useMemo(() => {
|
const zoomScale = useMemo(() => {
|
||||||
@ -488,7 +494,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
// 분석 결과 deck.gl 레이어
|
// 분석 결과 deck.gl 레이어
|
||||||
const analysisActiveFilter = koreaFilters.illegalFishing ? 'illegalFishing'
|
const analysisActiveFilter = koreaFilters.illegalFishing ? 'illegalFishing'
|
||||||
: koreaFilters.darkVessel ? 'darkVessel'
|
: koreaFilters.darkVessel ? 'darkVessel'
|
||||||
: layers.cnFishing ? 'cnFishing'
|
: koreaFilters.cnFishing ? 'cnFishing'
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const analysisDeckLayers = useAnalysisDeckLayers(
|
const analysisDeckLayers = useAnalysisDeckLayers(
|
||||||
@ -638,10 +644,10 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
{layers.cables && <SubmarineCableLayer />}
|
{layers.cables && <SubmarineCableLayer />}
|
||||||
{layers.cctv && <CctvLayer />}
|
{layers.cctv && <CctvLayer />}
|
||||||
{/* 정적 레이어들은 deck.gl DeckGLOverlay로 전환됨 — 아래 DOM 레이어 제거 */}
|
{/* 정적 레이어들은 deck.gl DeckGLOverlay로 전환됨 — 아래 DOM 레이어 제거 */}
|
||||||
{(koreaFilters.illegalFishing || layers.cnFishing) && <FishingZoneLayer />}
|
{(koreaFilters.illegalFishing || koreaFilters.cnFishing) && <FishingZoneLayer />}
|
||||||
{layers.cnFishing && <ChineseFishingOverlay ships={ships} analysisMap={vesselAnalysis?.analysisMap} />}
|
{koreaFilters.cnFishing && <ChineseFishingOverlay ships={ships} analysisMap={vesselAnalysis?.analysisMap} />}
|
||||||
{/* HazardFacility, CnFacility, JpFacility → useStaticDeckLayers (deck.gl GPU) 전환 완료 */}
|
{/* HazardFacility, CnFacility, JpFacility → useStaticDeckLayers (deck.gl GPU) 전환 완료 */}
|
||||||
{layers.cnFishing && (
|
{koreaFilters.cnFishing && (
|
||||||
<FleetClusterLayer
|
<FleetClusterLayer
|
||||||
ships={allShips ?? ships}
|
ships={allShips ?? ships}
|
||||||
analysisMap={vesselAnalysis ? vesselAnalysis.analysisMap : undefined}
|
analysisMap={vesselAnalysis ? vesselAnalysis.analysisMap : undefined}
|
||||||
@ -681,42 +687,106 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
{/* Filter Status Banner — 필터별 개별 탐지 카운트 */}
|
{/* Filter Status Banner — 필터별 개별 탐지 카운트 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const active = (Object.keys(koreaFilters) as (keyof KoreaFiltersState)[]).filter(k => koreaFilters[k]);
|
const active = (Object.keys(koreaFilters) as (keyof KoreaFiltersState)[]).filter(k => koreaFilters[k]);
|
||||||
if (active.length === 0) return null;
|
if (active.length === 0) { if (activeBadgeFilter) setActiveBadgeFilter(null); return null; }
|
||||||
const filterCount: Record<string, number> = {
|
const gearPattern = /^.+?_\d+_\d+_?$/;
|
||||||
illegalFishing: (allShips ?? ships).filter(s => {
|
const all = allShips ?? ships;
|
||||||
if (s.mtCategory !== 'fishing' || s.flag === 'KR') return false;
|
const getShipsForFilter = (k: string): Ship[] => {
|
||||||
return classifyFishingZone(s.lat, s.lng).zone !== 'OUTSIDE';
|
switch (k) {
|
||||||
}).length,
|
case 'illegalFishing': return all.filter(s => s.mtCategory === 'fishing' && s.flag !== 'KR' && classifyFishingZone(s.lat, s.lng).zone !== 'OUTSIDE');
|
||||||
illegalTransship: transshipSuspects.size,
|
case 'illegalTransship': return all.filter(s => transshipSuspects.has(s.mmsi));
|
||||||
darkVessel: ships.filter(s => {
|
case 'darkVessel': return all.filter(s => { const dto = vesselAnalysis?.analysisMap.get(s.mmsi); return !!(dto?.algorithms.darkVessel.isDark) || (s.lastSeen != null && currentTime - s.lastSeen > 3600000); });
|
||||||
const dto = vesselAnalysis?.analysisMap.get(s.mmsi);
|
case 'cableWatch': return all.filter(s => cableWatchSuspects.has(s.mmsi));
|
||||||
return dto?.algorithms.darkVessel.isDark || (s.lastSeen && currentTime - s.lastSeen > 3600000);
|
case 'dokdoWatch': return all.filter(s => dokdoWatchSuspects.has(s.mmsi));
|
||||||
}).length,
|
case 'ferryWatch': return all.filter(s => s.mtCategory === 'passenger');
|
||||||
cableWatch: cableWatchSuspects.size,
|
case 'cnFishing': return all.filter(s => (s.flag === 'CN' && s.mtCategory === 'fishing') || gearPattern.test(s.name || ''));
|
||||||
dokdoWatch: dokdoWatchSuspects.size,
|
default: return [];
|
||||||
ferryWatch: (allShips ?? ships).filter(s => s.mtCategory === 'passenger').length,
|
}
|
||||||
};
|
};
|
||||||
|
const downloadCsv = (k: string) => {
|
||||||
|
const data = getShipsForFilter(k);
|
||||||
|
const bom = '\uFEFF';
|
||||||
|
const header = 'MMSI,Name,Flag,Category,Lat,Lng,Speed,Heading';
|
||||||
|
const rows = data.map(s => `${s.mmsi},"${(s.name || '').replace(/"/g, '""')}",${s.flag || ''},${s.mtCategory || ''},${s.lat.toFixed(4)},${s.lng.toFixed(4)},${s.speed},${s.heading}`);
|
||||||
|
const blob = new Blob([bom + header + '\n' + rows.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${k}_${new Date().toISOString().slice(0, 10)}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
const badgeShips = activeBadgeFilter ? getShipsForFilter(activeBadgeFilter) : [];
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-2.5 left-1/2 -translate-x-1/2 z-20 flex gap-1.5 backdrop-blur-lg">
|
<>
|
||||||
{active.map(k => {
|
<div className="absolute top-2.5 left-1/2 -translate-x-1/2 z-20 flex gap-1.5 backdrop-blur-lg">
|
||||||
const color = FILTER_COLOR[k];
|
{active.map(k => {
|
||||||
const count = filterCount[k] ?? 0;
|
const color = FILTER_COLOR[k];
|
||||||
return (
|
const count = getShipsForFilter(k).length;
|
||||||
<div
|
const isOpen = activeBadgeFilter === k;
|
||||||
key={k}
|
return (
|
||||||
className="rounded-lg px-3 py-1.5 font-mono text-[11px] font-bold flex items-center gap-1.5 animate-pulse"
|
<div
|
||||||
style={{
|
key={k}
|
||||||
background: `${color}22`, border: `1px solid ${color}88`,
|
className={`rounded-lg px-3 py-1.5 font-mono text-[11px] font-bold flex items-center gap-1.5 cursor-pointer select-none ${isOpen ? '' : 'animate-pulse'}`}
|
||||||
color,
|
style={{
|
||||||
}}
|
background: isOpen ? `${color}44` : `${color}22`,
|
||||||
>
|
border: `1px solid ${isOpen ? color : color + '88'}`,
|
||||||
<span className="text-[13px]">{FILTER_ICON[k]}</span>
|
color,
|
||||||
{t(FILTER_I18N_KEY[k])}
|
}}
|
||||||
<span className="ml-0.5 text-white/80">{count}척</span>
|
onClick={() => setActiveBadgeFilter(prev => prev === k ? null : k)}
|
||||||
|
>
|
||||||
|
<span className="text-[13px]">{FILTER_ICON[k]}</span>
|
||||||
|
{FILTER_I18N_KEY[k] ? t(FILTER_I18N_KEY[k]) : k}
|
||||||
|
<span className="ml-0.5 text-white/80">{count}척</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{activeBadgeFilter && badgeShips.length > 0 && (
|
||||||
|
<div
|
||||||
|
className="absolute top-12 left-1/2 -translate-x-1/2 z-30 rounded-lg px-3 py-2 font-mono text-[11px] w-[440px] max-h-[320px] bg-kcg-overlay backdrop-blur-lg shadow-lg"
|
||||||
|
style={{ borderWidth: 1, borderStyle: 'solid', borderColor: (FILTER_COLOR[activeBadgeFilter] ?? '#888') + '88' }}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="font-bold text-white">
|
||||||
|
{FILTER_ICON[activeBadgeFilter]} {FILTER_I18N_KEY[activeBadgeFilter] ? t(FILTER_I18N_KEY[activeBadgeFilter]) : activeBadgeFilter} — {badgeShips.length}척
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<button type="button" onClick={() => downloadCsv(activeBadgeFilter)} className="text-[10px] px-2 py-0.5 rounded bg-white/10 hover:bg-white/20 text-white transition-colors">CSV</button>
|
||||||
|
<button type="button" onClick={() => setActiveBadgeFilter(null)} className="text-white/60 hover:text-white transition-colors">✕</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="overflow-y-auto max-h-[260px]">
|
||||||
})}
|
<table className="w-full text-[10px]">
|
||||||
</div>
|
<thead className="sticky top-0 bg-kcg-overlay">
|
||||||
|
<tr className="text-white/50 border-b border-white/10">
|
||||||
|
<th className="text-left py-1 px-1">MMSI</th>
|
||||||
|
<th className="text-left py-1 px-1">Name</th>
|
||||||
|
<th className="text-center py-1 px-1">Flag</th>
|
||||||
|
<th className="text-left py-1 px-1">Type</th>
|
||||||
|
<th className="text-right py-1 px-1">Speed</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{badgeShips.slice(0, 200).map(s => (
|
||||||
|
<tr
|
||||||
|
key={s.mmsi}
|
||||||
|
className="hover:bg-white/5 cursor-pointer border-b border-white/5"
|
||||||
|
onClick={() => setFlyToTarget({ lng: s.lng, lat: s.lat, zoom: 12 })}
|
||||||
|
>
|
||||||
|
<td className="py-0.5 px-1 text-white/60">{s.mmsi}</td>
|
||||||
|
<td className="py-0.5 px-1 text-white/90 truncate max-w-[140px]">{s.name || '-'}</td>
|
||||||
|
<td className="py-0.5 px-1 text-center text-white/60">{s.flag || '??'}</td>
|
||||||
|
<td className="py-0.5 px-1 text-white/50">{s.mtCategory || '-'}</td>
|
||||||
|
<td className="py-0.5 px-1 text-right text-white/50">{s.speed?.toFixed(1)}kn</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{badgeShips.length > 200 && <div className="text-center text-white/40 text-[10px] py-1">...외 {badgeShips.length - 200}척</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ interface KoreaFilters {
|
|||||||
cableWatch: boolean;
|
cableWatch: boolean;
|
||||||
dokdoWatch: boolean;
|
dokdoWatch: boolean;
|
||||||
ferryWatch: boolean;
|
ferryWatch: boolean;
|
||||||
|
cnFishing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DokdoAlert {
|
interface DokdoAlert {
|
||||||
@ -28,6 +29,7 @@ interface UseKoreaFiltersResult {
|
|||||||
transshipSuspects: Set<string>;
|
transshipSuspects: Set<string>;
|
||||||
cableWatchSuspects: Set<string>;
|
cableWatchSuspects: Set<string>;
|
||||||
dokdoWatchSuspects: Set<string>;
|
dokdoWatchSuspects: Set<string>;
|
||||||
|
cnFishingSuspects: Set<string>;
|
||||||
dokdoAlerts: DokdoAlert[];
|
dokdoAlerts: DokdoAlert[];
|
||||||
anyFilterOn: boolean;
|
anyFilterOn: boolean;
|
||||||
}
|
}
|
||||||
@ -43,7 +45,6 @@ export function useKoreaFilters(
|
|||||||
visibleShips: Ship[],
|
visibleShips: Ship[],
|
||||||
currentTime: number,
|
currentTime: number,
|
||||||
analysisMap?: Map<string, VesselAnalysisDto>,
|
analysisMap?: Map<string, VesselAnalysisDto>,
|
||||||
cnFishingOn = false,
|
|
||||||
): UseKoreaFiltersResult {
|
): UseKoreaFiltersResult {
|
||||||
const [filters, setFilters] = useLocalStorage<KoreaFilters>('koreaFilters', {
|
const [filters, setFilters] = useLocalStorage<KoreaFilters>('koreaFilters', {
|
||||||
illegalFishing: false,
|
illegalFishing: false,
|
||||||
@ -52,6 +53,7 @@ export function useKoreaFilters(
|
|||||||
cableWatch: false,
|
cableWatch: false,
|
||||||
dokdoWatch: false,
|
dokdoWatch: false,
|
||||||
ferryWatch: false,
|
ferryWatch: false,
|
||||||
|
cnFishing: false,
|
||||||
});
|
});
|
||||||
const [dokdoAlerts, setDokdoAlerts] = useState<DokdoAlert[]>([]);
|
const [dokdoAlerts, setDokdoAlerts] = useState<DokdoAlert[]>([]);
|
||||||
|
|
||||||
@ -70,7 +72,7 @@ export function useKoreaFilters(
|
|||||||
filters.cableWatch ||
|
filters.cableWatch ||
|
||||||
filters.dokdoWatch ||
|
filters.dokdoWatch ||
|
||||||
filters.ferryWatch ||
|
filters.ferryWatch ||
|
||||||
cnFishingOn;
|
filters.cnFishing;
|
||||||
|
|
||||||
// 불법환적 의심 선박 탐지 (Python 분석 결과 소비)
|
// 불법환적 의심 선박 탐지 (Python 분석 결과 소비)
|
||||||
const transshipSuspects = useMemo(() => {
|
const transshipSuspects = useMemo(() => {
|
||||||
@ -250,6 +252,18 @@ export function useKoreaFilters(
|
|||||||
return result;
|
return result;
|
||||||
}, [koreaShips, filters.dokdoWatch, currentTime]);
|
}, [koreaShips, filters.dokdoWatch, currentTime]);
|
||||||
|
|
||||||
|
// 중국어선 의심 선박 Set
|
||||||
|
const cnFishingSuspects = useMemo(() => {
|
||||||
|
if (!filters.cnFishing) return new Set<string>();
|
||||||
|
const result = new Set<string>();
|
||||||
|
for (const s of koreaShips) {
|
||||||
|
const isCnFishing = s.flag === 'CN' && s.mtCategory === 'fishing';
|
||||||
|
const isGearPattern = /^.+?_\d+_\d+_?$/.test(s.name || '');
|
||||||
|
if (isCnFishing || isGearPattern) result.add(s.mmsi);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [filters.cnFishing, koreaShips]);
|
||||||
|
|
||||||
// 필터링된 선박 목록
|
// 필터링된 선박 목록
|
||||||
const filteredShips = useMemo(() => {
|
const filteredShips = useMemo(() => {
|
||||||
if (!anyFilterOn) return visibleShips;
|
if (!anyFilterOn) return visibleShips;
|
||||||
@ -272,14 +286,10 @@ export function useKoreaFilters(
|
|||||||
if (filters.cableWatch && cableWatchSet.has(s.mmsi)) return true;
|
if (filters.cableWatch && cableWatchSet.has(s.mmsi)) return true;
|
||||||
if (filters.dokdoWatch && dokdoWatchSet.has(s.mmsi)) return true;
|
if (filters.dokdoWatch && dokdoWatchSet.has(s.mmsi)) return true;
|
||||||
if (filters.ferryWatch && s.mtCategory === 'passenger') return true;
|
if (filters.ferryWatch && s.mtCategory === 'passenger') return true;
|
||||||
if (cnFishingOn) {
|
if (filters.cnFishing && cnFishingSuspects.has(s.mmsi)) return true;
|
||||||
const isCnFishing = s.flag === 'CN' && s.mtCategory === 'fishing';
|
|
||||||
const isGearPattern = /^.+?_\d+_\d+_?$/.test(s.name || '');
|
|
||||||
if (isCnFishing || isGearPattern) return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}, [visibleShips, filters, anyFilterOn, transshipSuspects, darkVesselSet, cableWatchSet, dokdoWatchSet, analysisMap, cnFishingOn]);
|
}, [visibleShips, filters, anyFilterOn, transshipSuspects, darkVesselSet, cableWatchSet, dokdoWatchSet, analysisMap, cnFishingSuspects]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filters,
|
filters,
|
||||||
@ -288,6 +298,7 @@ export function useKoreaFilters(
|
|||||||
transshipSuspects,
|
transshipSuspects,
|
||||||
cableWatchSuspects: cableWatchSet,
|
cableWatchSuspects: cableWatchSet,
|
||||||
dokdoWatchSuspects: dokdoWatchSet,
|
dokdoWatchSuspects: dokdoWatchSet,
|
||||||
|
cnFishingSuspects,
|
||||||
dokdoAlerts,
|
dokdoAlerts,
|
||||||
anyFilterOn,
|
anyFilterOn,
|
||||||
};
|
};
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user