자동 갱신 (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>
105 lines
2.9 KiB
TypeScript
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);
|
|
},
|
|
}));
|