kcg-ai-monitoring/frontend/src/stores/eventStore.ts
htlee e401c07dd3 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>
2026-04-09 11:43:18 +09:00

105 lines
2.9 KiB
TypeScript

import { create } from 'zustand';
import {
getEvents,
getEventStats,
toLegacyEvent,
type PredictionEvent,
type EventStats,
type LegacyEventRecord,
} from '@/services/event';
/** @deprecated LegacyEventRecord 대신 PredictionEvent 사용 권장 */
export type { LegacyEventRecord as EventRecord } from '@/services/event';
interface EventStore {
/** 원본 API 이벤트 목록 */
rawEvents: PredictionEvent[];
/** 하위 호환용 레거시 형식 이벤트 */
events: LegacyEventRecord[];
/** 상태별 통계 */
stats: EventStats;
/** 페이지네이션 */
totalElements: number;
totalPages: number;
currentPage: number;
pageSize: number;
/** 로딩/에러 */
loading: boolean;
error: string | null;
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[];
}
export const useEventStore = create<EventStore>((set, get) => ({
rawEvents: [],
events: [],
stats: {},
totalElements: 0,
totalPages: 0,
currentPage: 0,
pageSize: 20,
loading: false,
error: null,
loaded: false,
load: async (params) => {
// 중복 호출 방지 (파라미터 없는 기본 호출은 loaded 체크)
if (!params && get().loaded && !get().error) return;
set({ loading: true, error: null });
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,
loaded: true,
loading: false,
});
} catch (err) {
set({ error: err instanceof Error ? err.message : String(err), loading: false });
}
},
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();
set({ stats });
} catch {
// stats 로딩 실패는 무시 (KPI 카드만 빈 값)
}
},
filterByLevel: (level) => {
const { events } = get();
if (!level) return events;
return events.filter((e) => e.level === level);
},
}));