import { useEffect, useState, useCallback } from 'react'; import { CheckCircle, XCircle, RotateCcw, Loader2, GitMerge } 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 { PageContainer, PageHeader } from '@shared/components/layout'; import { useAuth } from '@/app/auth/AuthContext'; import { fetchReviewList, reviewParent, type ParentResolution, } from '@/services/parentInferenceApi'; import { formatDateTime } from '@shared/utils/dateFormat'; import { getParentResolutionIntent, getParentResolutionLabel } from '@shared/constants/parentResolutionStatuses'; import { useSettingsStore } from '@stores/settingsStore'; import { useTranslation } from 'react-i18next'; /** * 모선 확정/거부/리셋 페이지. * - 운영자가 prediction이 추론한 모선 후보를 확정/거부. * - 권한: parent-inference-workflow:parent-review (READ + UPDATE) * - 모든 액션은 백엔드에서 audit_log + review_log에 기록 */ export function ParentReview() { const { t: tc } = useTranslation('common'); const lang = useSettingsStore((s) => s.language); const { hasPermission } = useAuth(); const canUpdate = hasPermission('parent-inference-workflow:parent-review', 'UPDATE'); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [actionLoading, setActionLoading] = useState(null); const [filter, setFilter] = useState(''); // 새 그룹 입력 폼 (테스트용) const [newGroupKey, setNewGroupKey] = useState(''); const [newSubCluster, setNewSubCluster] = useState('1'); const [newMmsi, setNewMmsi] = useState(''); const load = useCallback(async () => { setLoading(true); setError(''); try { const res = await fetchReviewList(filter || undefined, 0, 50); setItems(res.content); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'unknown'; setError(msg); } finally { setLoading(false); } }, [filter]); useEffect(() => { load(); }, [load]); const handleAction = async ( item: ParentResolution, action: 'CONFIRM' | 'REJECT' | 'RESET', selectedMmsi?: string, ) => { if (!canUpdate) return; setActionLoading(item.id); try { await reviewParent(item.groupKey, item.subClusterId, { action, selectedParentMmsi: selectedMmsi || item.selectedParentMmsi || undefined, comment: `${action} via UI`, }); await load(); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'unknown'; alert('처리 실패: ' + msg); } finally { setActionLoading(null); } }; const handleCreate = async () => { if (!canUpdate || !newGroupKey || !newMmsi) return; setActionLoading(-1); try { await reviewParent(newGroupKey, parseInt(newSubCluster, 10), { action: 'CONFIRM', selectedParentMmsi: newMmsi, comment: '운영자 직접 등록', }); setNewGroupKey(''); setNewMmsi(''); await load(); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'unknown'; alert('등록 실패: ' + msg); } finally { setActionLoading(null); } }; return ( } /> {/* 신규 등록 폼 (테스트용) */} {canUpdate && (
신규 모선 확정 등록 (테스트)
setNewGroupKey(e.target.value)} placeholder="group_key (예: 渔船A)" className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" /> setNewSubCluster(e.target.value)} placeholder="sub_cluster_id" className="w-32 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" /> setNewMmsi(e.target.value)} placeholder="parent MMSI" className="w-40 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" />
)} {!canUpdate && (
조회 전용 모드 (UPDATE 권한 없음). 확정/거부/리셋 액션이 비활성화됩니다.
)} {error && (
에러: {error}
)} {loading && (
)} {!loading && items.length === 0 && ( 등록된 모선 결정이 없습니다. 위의 폼으로 테스트 등록하거나, prediction 백엔드 연결 후 데이터가 채워집니다. )} {!loading && items.length > 0 && ( {items.map((it) => ( ))}
ID Group Key Sub 상태 선택 MMSI 갱신 시각 액션
{it.id} {it.groupKey} {it.subClusterId} {getParentResolutionLabel(it.status, tc, lang)} {it.selectedParentMmsi || '-'} {formatDateTime(it.updatedAt)}
)}
); }