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'; import { useSettingsStore } from '@stores/settingsStore'; import { useTranslation } from 'react-i18next'; /** * prediction 분석 엔진이 산출한 실시간 어구/선단 그룹을 표시. * - GET /api/vessel-analysis/groups * - 자체 DB의 ParentResolution 운영자 결정이 합성되어 있음 */ export function RealGearGroups() { const { t: tc } = useTranslation('common'); const lang = useSettingsStore((s) => s.language); const [items, setItems] = useState([]); const [available, setAvailable] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [filterType, setFilterType] = useState(''); const load = useCallback(async () => { setLoading(true); setError(''); try { const res = await fetchGroups(); setItems(res.items); setAvailable(res.serviceAvailable); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'unknown'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const filtered = filterType ? items.filter((i) => i.groupType === filterType) : items; const stats = { total: items.length, fleet: items.filter((i) => i.groupType === 'FLEET').length, gearInZone: items.filter((i) => i.groupType === 'GEAR_IN_ZONE').length, gearOutZone: items.filter((i) => i.groupType === 'GEAR_OUT_ZONE').length, confirmed: items.filter((i) => i.resolution?.status === 'MANUAL_CONFIRMED').length, }; return (
실시간 어구/선단 그룹 {!available && 미연결}
GET /api/vessel-analysis/groups · 자체 DB의 운영자 결정(resolution) 합성됨
{/* 통계 */}
{error &&
{tc('error.errorPrefix', { msg: error })}
} {loading &&
} {!loading && (
{filtered.length === 0 && ( )} {filtered.slice(0, 100).map((g) => ( ))}
유형 그룹 키 서브 멤버 면적(NM²) 중심 좌표 운영자 결정 스냅샷 시각
데이터가 없습니다.
{getGearGroupTypeLabel(g.groupType, tc, lang)} {g.groupKey} {g.subClusterId} {g.memberCount} {g.areaSqNm?.toFixed(2)} {g.centerLat?.toFixed(3)}, {g.centerLon?.toFixed(3)} {g.resolution ? ( {getParentResolutionLabel(g.resolution.status, tc, lang)} ) : -} {g.snapshotTime ? new Date(g.snapshotTime).toLocaleTimeString('ko-KR') : '-'}
{filtered.length > 100 && (
상위 100건만 표시 (전체 {filtered.length}건)
)}
)}
); } 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}
); }