feat(frontend): 워크플로우 연결 Step 5 — 자동갱신 + 모선추론 연결 + i18n
자동 갱신 (30초, 깜박임 없음): - eventStore: silentRefresh() 메서드 추가 (loading 상태 미변경, 데이터만 교체) - EventList: 30초 인터벌로 silentRefresh + loadStats 호출 - DarkVesselDetection: 30초 인터벌로 getDarkVessels silent 갱신 모선추론 자동 연결: - ParentReview CONFIRM → createLabelSession 자동 호출 (학습 데이터 수집 시작) - ParentReview REJECT → excludeForGroup 자동 호출 (잘못된 후보 재추론 방지) - 자동 연결 실패 시 리뷰 자체는 유지 (catch 무시) i18n (ko/en): - darkTier: CRITICAL/HIGH/WATCH/NONE 라벨 - transshipTier: CRITICAL/HIGH/WATCH 라벨 - adminSubGroup: AI 플랫폼/시스템 운영/사용자 관리/감사·보안 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
6887a2b4fc
커밋
e401c07dd3
@ -136,6 +136,17 @@ export function DarkVesselDetection() {
|
||||
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
// 30초 자동 갱신 (깜박임 없음 — loading 상태 변경 없이 데이터만 교체)
|
||||
useEffect(() => {
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const res = await getDarkVessels({ hours: 1, size: 500 });
|
||||
setRawData(res.content);
|
||||
} catch { /* silent */ }
|
||||
}, 30_000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const DATA: Suspect[] = useMemo(() => {
|
||||
let items = rawData.map((v, i) => mapToSuspect(v, i));
|
||||
if (tierFilter) {
|
||||
|
||||
@ -58,6 +58,7 @@ export function EventList() {
|
||||
loading,
|
||||
error,
|
||||
load,
|
||||
silentRefresh,
|
||||
loadStats,
|
||||
} = useEventStore();
|
||||
const [actionLoading, setActionLoading] = useState<number | null>(null);
|
||||
@ -210,6 +211,16 @@ export function EventList() {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
// 30초 자동 갱신 (깜박임 없음 — silentRefresh 사용)
|
||||
useEffect(() => {
|
||||
const params = levelFilter ? { level: levelFilter } : undefined;
|
||||
const timer = setInterval(() => {
|
||||
silentRefresh(params);
|
||||
loadStats();
|
||||
}, 30_000);
|
||||
return () => clearInterval(timer);
|
||||
}, [levelFilter, silentRefresh, loadStats]);
|
||||
|
||||
// store events -> EventRow 변환 (rawEvents에서 numeric id 참조)
|
||||
const EVENTS: EventRow[] = storeEvents.map((e, idx) => ({
|
||||
id: e.id,
|
||||
|
||||
@ -9,6 +9,8 @@ import { useAuth } from '@/app/auth/AuthContext';
|
||||
import {
|
||||
fetchReviewList,
|
||||
reviewParent,
|
||||
createLabelSession,
|
||||
excludeForGroup,
|
||||
type ParentResolution,
|
||||
} from '@/services/parentInferenceApi';
|
||||
import { formatDateTime } from '@shared/utils/dateFormat';
|
||||
@ -70,6 +72,28 @@ export function ParentReview() {
|
||||
selectedParentMmsi: selectedMmsi || item.selectedParentMmsi || undefined,
|
||||
comment: `${action} via UI`,
|
||||
});
|
||||
|
||||
// CONFIRM → LabelSession 자동 생성 (학습 데이터 수집 시작)
|
||||
if (action === 'CONFIRM') {
|
||||
const mmsi = selectedMmsi || item.selectedParentMmsi;
|
||||
if (mmsi) {
|
||||
await createLabelSession(item.groupKey, item.subClusterId, {
|
||||
labelParentMmsi: mmsi,
|
||||
}).catch(() => { /* LabelSession 실패는 무시 — 리뷰 자체는 성공 */ });
|
||||
}
|
||||
}
|
||||
|
||||
// REJECT → Exclusion 자동 등록 (잘못된 후보 재추론 방지)
|
||||
if (action === 'REJECT') {
|
||||
const mmsi = item.selectedParentMmsi;
|
||||
if (mmsi) {
|
||||
await excludeForGroup(item.groupKey, item.subClusterId, {
|
||||
excludedMmsi: mmsi,
|
||||
reason: '운영자 거부',
|
||||
}).catch(() => { /* Exclusion 실패는 무시 */ });
|
||||
}
|
||||
}
|
||||
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : 'unknown';
|
||||
|
||||
@ -132,6 +132,23 @@
|
||||
"INTERMITTENT": "Intermittent",
|
||||
"SPEED_ANOMALY": "Speed Anomaly"
|
||||
},
|
||||
"darkTier": {
|
||||
"CRITICAL": "Intentional Loss (Critical)",
|
||||
"HIGH": "Suspicious Loss",
|
||||
"WATCH": "Under Watch",
|
||||
"NONE": "Normal"
|
||||
},
|
||||
"transshipTier": {
|
||||
"CRITICAL": "Confirmed Transship",
|
||||
"HIGH": "Suspected Transship",
|
||||
"WATCH": "Under Watch"
|
||||
},
|
||||
"adminSubGroup": {
|
||||
"aiPlatform": "AI Platform",
|
||||
"systemOps": "System Operations",
|
||||
"userMgmt": "User Management",
|
||||
"auditSecurity": "Audit & Security"
|
||||
},
|
||||
"userAccountStatus": {
|
||||
"ACTIVE": "Active",
|
||||
"PENDING": "Pending",
|
||||
|
||||
@ -132,6 +132,23 @@
|
||||
"INTERMITTENT": "신호 간헐송출",
|
||||
"SPEED_ANOMALY": "속도 이상"
|
||||
},
|
||||
"darkTier": {
|
||||
"CRITICAL": "고의 소실 (위험)",
|
||||
"HIGH": "의심 소실",
|
||||
"WATCH": "관찰 대상",
|
||||
"NONE": "정상"
|
||||
},
|
||||
"transshipTier": {
|
||||
"CRITICAL": "환적 확실",
|
||||
"HIGH": "환적 의심",
|
||||
"WATCH": "관찰 대상"
|
||||
},
|
||||
"adminSubGroup": {
|
||||
"aiPlatform": "AI 플랫폼",
|
||||
"systemOps": "시스템 운영",
|
||||
"userMgmt": "사용자 관리",
|
||||
"auditSecurity": "감사·보안"
|
||||
},
|
||||
"userAccountStatus": {
|
||||
"ACTIVE": "활성",
|
||||
"PENDING": "승인 대기",
|
||||
|
||||
@ -29,6 +29,8 @@ interface EventStore {
|
||||
loaded: boolean;
|
||||
/** API 호출 */
|
||||
load: (params?: { level?: string; status?: string; category?: string; page?: number; size?: number }) => Promise<void>;
|
||||
/** 화면 깜박임 없는 백그라운드 갱신 (loading 상태 변경 없음) */
|
||||
silentRefresh: (params?: { level?: string; status?: string; category?: string; page?: number; size?: number }) => Promise<void>;
|
||||
loadStats: () => Promise<void>;
|
||||
filterByLevel: (level: string | null) => LegacyEventRecord[];
|
||||
}
|
||||
@ -68,6 +70,23 @@ export const useEventStore = create<EventStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
silentRefresh: async (params) => {
|
||||
try {
|
||||
const res = await getEvents(params);
|
||||
const legacy = res.content.map(toLegacyEvent);
|
||||
set({
|
||||
rawEvents: res.content,
|
||||
events: legacy,
|
||||
totalElements: res.totalElements,
|
||||
totalPages: res.totalPages,
|
||||
currentPage: res.number,
|
||||
pageSize: res.size,
|
||||
});
|
||||
} catch {
|
||||
// silent: 에러 무시 — 다음 갱신에서 재시도
|
||||
}
|
||||
},
|
||||
|
||||
loadStats: async () => {
|
||||
try {
|
||||
const stats = await getEventStats();
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user