import { useEffect, useState, useCallback } from 'react'; import { Tag, X, Loader2 } 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 { Input } from '@shared/components/ui/input'; import { Select } from '@shared/components/ui/select'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { useAuth } from '@/app/auth/AuthContext'; import { fetchLabelSessions, createLabelSession, cancelLabelSession, type LabelSession as LabelSessionType, } from '@/services/parentInferenceApi'; import { formatDateTime } from '@shared/utils/dateFormat'; import { getLabelSessionIntent, getLabelSessionLabel } from '@shared/constants/parentResolutionStatuses'; import { useSettingsStore } from '@stores/settingsStore'; import { useTranslation } from 'react-i18next'; /** * 모선 추론 학습 세션 페이지. * 운영자가 정답 라벨링 → prediction 모델 학습 데이터로 활용. * * 권한: parent-inference-workflow:label-session (READ + CREATE + UPDATE) */ export function LabelSession() { const { t: tc } = useTranslation('common'); const lang = useSettingsStore((s) => s.language); const { hasPermission } = useAuth(); const canCreate = hasPermission('parent-inference-workflow:label-session', 'CREATE'); const canUpdate = hasPermission('parent-inference-workflow:label-session', 'UPDATE'); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [filter, setFilter] = useState(''); const [busy, setBusy] = useState(null); // 신규 세션 const [groupKey, setGroupKey] = useState(''); const [subCluster, setSubCluster] = useState('1'); const [labelMmsi, setLabelMmsi] = useState(''); const load = useCallback(async () => { setLoading(true); setError(''); try { const res = await fetchLabelSessions(filter || undefined, 0, 50); setItems(res.content); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'unknown'); } finally { setLoading(false); } }, [filter]); useEffect(() => { load(); }, [load]); const handleCreate = async () => { if (!canCreate || !groupKey || !labelMmsi) return; setBusy(-1); try { await createLabelSession(groupKey, parseInt(subCluster, 10), { labelParentMmsi: labelMmsi, anchorSnapshot: { source: 'manual', timestamp: new Date().toISOString() }, }); setGroupKey(''); setLabelMmsi(''); await load(); } catch (e: unknown) { alert(tc('error.operationFailed', { msg: e instanceof Error ? e.message : 'unknown' })); } finally { setBusy(null); } }; const handleCancel = async (id: number) => { if (!canUpdate) return; if (!confirm(tc('dialog.cancelSession'))) return; setBusy(id); try { await cancelLabelSession(id, '운영자 취소'); await load(); } catch (e: unknown) { alert(tc('error.operationFailed', { msg: e instanceof Error ? e.message : 'unknown' })); } finally { setBusy(null); } }; return ( } />
신규 학습 세션 등록 {!canCreate && 권한 없음}
setGroupKey(e.target.value)} placeholder="group_key" className="flex-1" disabled={!canCreate} /> setSubCluster(e.target.value)} placeholder="sub" className="w-24" disabled={!canCreate} /> setLabelMmsi(e.target.value)} placeholder="정답 parent MMSI" className="w-48" disabled={!canCreate} />
{error &&
{tc('error.errorPrefix', { msg: error })}
} {loading && (
)} {!loading && ( {items.length === 0 && ( )} {items.map((it) => ( ))}
ID Group Key Sub 정답 MMSI 상태 생성자 시작 액션
학습 세션이 없습니다.
{it.id} {it.groupKey} {it.subClusterId} {it.labelParentMmsi} {getLabelSessionLabel(it.status, tc, lang)} {it.createdByAcnt || '-'} {formatDateTime(it.activeFrom)} {it.status === 'ACTIVE' && ( )}
)}
); }