import { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; import { Select } from '@shared/components/ui/select'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { FileUpload } from '@shared/components/common/FileUpload'; import { AlertTriangle, Eye, Anchor, Radar, Crosshair, Filter, Upload, X, Loader2, } from 'lucide-react'; import { useEventStore } from '@stores/eventStore'; import { formatDateTime } from '@shared/utils/dateFormat'; import { type AlertLevel as AlertLevelType, getAlertLevelLabel, getAlertLevelIntent } from '@shared/constants/alertLevels'; import { getEventStatusIntent, getEventStatusLabel } from '@shared/constants/eventStatuses'; import { getViolationLabel, getViolationIntent } from '@shared/constants/violationTypes'; import { useSettingsStore } from '@stores/settingsStore'; /* * 이벤트 목록 — SFR-02 공통컴포넌트 적용 * DataTable(검색+정렬+페이징+엑셀내보내기+출력), FileUpload * 실제 백엔드 API 연동 */ type AlertLevel = AlertLevelType; interface EventRow { id: string; time: string; level: AlertLevel; type: string; vesselName: string; mmsi: string; area: string; lat: string; lng: string; speed: string; status: string; assignee: string; [key: string]: unknown; } export function EventList() { const { t } = useTranslation('enforcement'); const { t: tc } = useTranslation('common'); const lang = useSettingsStore((s) => s.language); const { events: storeEvents, stats, loading, error, load, loadStats, } = useEventStore(); const columns: DataColumn[] = useMemo(() => [ { key: 'level', label: '등급', minWidth: '64px', maxWidth: '110px', sortable: true, render: (val) => { const lv = val as AlertLevel; return ( {getAlertLevelLabel(lv, tc, lang)} ); }, }, { key: 'time', label: '발생시간', minWidth: '140px', maxWidth: '170px', sortable: true, render: (val) => {formatDateTime(val as string)}, }, { key: 'type', label: '유형', minWidth: '90px', maxWidth: '160px', sortable: true, render: (val) => { const code = val as string; return ( {getViolationLabel(code, tc, lang)} ); }, }, { key: 'vesselName', label: '선박명', minWidth: '100px', maxWidth: '220px', sortable: true, render: (val) => {val as string}, }, { key: 'mmsi', label: 'MMSI', minWidth: '90px', maxWidth: '120px', render: (val) => {val as string}, }, { key: 'area', label: '해역', minWidth: '80px', maxWidth: '140px', sortable: true }, { key: 'speed', label: '속력', minWidth: '56px', maxWidth: '80px', align: 'right' }, { key: 'status', label: '처리상태', minWidth: '80px', maxWidth: '120px', sortable: true, render: (val) => { const s = val as string; return ( {getEventStatusLabel(s, tc, lang)} ); }, }, { key: 'assignee', label: '담당', minWidth: '60px', maxWidth: '100px' }, ], [tc, lang]); const [levelFilter, setLevelFilter] = useState(''); const [showUpload, setShowUpload] = useState(false); const fetchData = useCallback(() => { const params = levelFilter ? { level: levelFilter } : undefined; load(params); loadStats(); }, [levelFilter, load, loadStats]); useEffect(() => { fetchData(); }, [fetchData]); // store events -> EventRow 변환 const EVENTS: EventRow[] = storeEvents.map((e) => ({ id: e.id, time: e.time, level: e.level as AlertLevel, type: e.type, vesselName: e.vesselName ?? '-', mmsi: e.mmsi ?? '-', area: e.area ?? '-', lat: e.lat != null ? String(e.lat) : '-', lng: e.lng != null ? String(e.lng) : '-', speed: e.speed != null ? `${e.speed}kt` : '미상', status: e.status ?? '-', assignee: e.assignee ?? '-', })); // KPI 카운트: stats API가 있으면 사용, 없으면 클라이언트 계산 const kpiCritical = stats['CRITICAL'] ?? EVENTS.filter((e) => e.level === 'CRITICAL').length; const kpiHigh = stats['HIGH'] ?? EVENTS.filter((e) => e.level === 'HIGH').length; const kpiMedium = stats['MEDIUM'] ?? EVENTS.filter((e) => e.level === 'MEDIUM').length; const kpiLow = stats['LOW'] ?? EVENTS.filter((e) => e.level === 'LOW').length; const kpiTotal = (stats['TOTAL'] as number | undefined) ?? EVENTS.length; return (
} /> {/* KPI 요약 */}
{[ { label: '전체', count: kpiTotal, icon: Radar, color: 'text-label', bg: 'bg-muted', filterVal: '' }, { label: 'CRITICAL', count: kpiCritical, icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-500/10', filterVal: 'CRITICAL' }, { label: 'HIGH', count: kpiHigh, icon: Eye, color: 'text-orange-400', bg: 'bg-orange-500/10', filterVal: 'HIGH' }, { label: 'MEDIUM', count: kpiMedium, icon: Anchor, color: 'text-yellow-400', bg: 'bg-yellow-500/10', filterVal: 'MEDIUM' }, { label: 'LOW', count: kpiLow, icon: Crosshair, color: 'text-blue-400', bg: 'bg-blue-500/10', filterVal: 'LOW' }, ].map((kpi) => (
setLevelFilter(kpi.filterVal)} className={`flex items-center gap-2.5 p-3 rounded-xl border cursor-pointer transition-colors ${ (kpi.filterVal === '' && !levelFilter) || kpi.filterVal === levelFilter ? 'bg-card border-blue-500/30' : 'bg-card border-border hover:border-border' }`} >
{kpi.count}
{kpi.label}
))}
{/* 에러 표시 */} {error && (
데이터 로딩 실패: {error}
)} {/* 파일 업로드 영역 */} {showUpload && (
이벤트 데이터 업로드
console.log('Uploaded:', files)} />
)} {/* 로딩 인디케이터 */} {loading && (
로딩 중...
)} {/* DataTable — 검색+정렬+페이징+엑셀+출력 */} {!loading && ( )}
); }