docs/prediction-analysis.md §7 P1 권고의 "UI 미노출 탐지" 해소. event_generator 가 생산하는 카테고리 중 불법 조업 관련 3종을 READ 전용 대시보드로 통합. 대상 카테고리: - GEAR_ILLEGAL — G-01 수역·어구 / G-05 고정어구 drift / G-06 쌍끌이 - EEZ_INTRUSION — 영해 침범 / 접속수역 + 고위험 - ZONE_DEPARTURE — 특정수역 진입 (risk ≥ 40) ### 변경 - frontend/src/services/illegalFishingPatternApi.ts 신설 - 기존 /api/events 를 category 다중 병렬 조회 후 머지 (backend 변경 없음) - category '' 이면 3 카테고리 통합, 지정 시 단일 카테고리만 - size 기본 200 × 3 categories = 최대 600건, occurredAt desc 정렬 - byCategory / byLevel 집계 포함 - frontend/src/features/detection/IllegalFishingPattern.tsx 신설 (391 라인) - PageContainer + PageHeader(Ban 아이콘) + Section + KPI 5장 + 카테고리별 3장 - DataTable (occurredAt/level/category/title/mmsi/zone/status 7컬럼) - 필터: category / level / mmsi (최근 DEFAULT_SIZE 건 범위) - 상세 패널: JSON features 포함, EventList 로 네비게이션 링크 - design-system SSOT 준수: Badge intent, Select aria-label, text-* 시맨틱 토큰 - index.ts + componentRegistry.ts export/lazy 등록 - detection.json (ko/en) illegalPattern.* 네임스페이스 추가 (각 60키) - common.json (ko/en) nav.illegalFishing 추가 - V032__menu_illegal_fishing_pattern.sql - auth_perm_tree 엔트리 (rsrc_cd=detection:illegal-fishing, nav_sort=920) - ADMIN 5 ops + OPERATOR/ANALYST/FIELD/VIEWER READ - READ 전용 페이지 (처리 액션은 EventList 경유) ### 검증 - npx tsc --noEmit 통과 (0 에러) - 백엔드 변경 없음 (기존 /api/events category 필터 재사용) - Flyway V032 자동 적용 (백엔드 재배포 필요)
78 lines
2.5 KiB
TypeScript
78 lines
2.5 KiB
TypeScript
/**
|
|
* 불법 조업 이벤트 전용 서비스 — 기존 /api/events 를 category 다중 조회로 래핑.
|
|
*
|
|
* category 는 event_generator 의 rule 에서 오는 단일 값이지만, UI 관점에서 "불법 조업"
|
|
* 은 여러 카테고리의 합집합이다:
|
|
* - GEAR_ILLEGAL : G-01 수역-어구 / G-05 고정어구 drift / G-06 쌍끌이
|
|
* - EEZ_INTRUSION : 영해 침범 / 접속수역 + 고위험
|
|
* - ZONE_DEPARTURE : 특정수역 진입 (관심구역 모니터링)
|
|
*
|
|
* backend 변경 없이 클라이언트에서 병렬 조회 후 머지한다.
|
|
*/
|
|
import { getEvents, type EventPageResponse, type PredictionEvent } from './event';
|
|
|
|
export const ILLEGAL_FISHING_CATEGORIES = [
|
|
'GEAR_ILLEGAL',
|
|
'EEZ_INTRUSION',
|
|
'ZONE_DEPARTURE',
|
|
] as const;
|
|
|
|
export type IllegalFishingCategory = (typeof ILLEGAL_FISHING_CATEGORIES)[number];
|
|
|
|
export interface ListParams {
|
|
/** 단일 카테고리를 지정하면 해당 카테고리만, '' 이면 3개 모두 병합 조회 */
|
|
category?: IllegalFishingCategory | '';
|
|
level?: string;
|
|
status?: string;
|
|
vesselMmsi?: string;
|
|
size?: number;
|
|
}
|
|
|
|
export interface IllegalFishingPatternPage {
|
|
content: PredictionEvent[];
|
|
totalElements: number;
|
|
byCategory: Record<string, number>;
|
|
byLevel: Record<string, number>;
|
|
}
|
|
|
|
/**
|
|
* 병합 조회 — category 미지정 시 3개 병렬 조회 후 occurredAt desc 정렬로 머지.
|
|
* 각 카테고리 최대 size 건씩 수집하므로, 기본 200 * 3 = 600 건이 상한.
|
|
*/
|
|
export async function listIllegalFishingEvents(params?: ListParams): Promise<IllegalFishingPatternPage> {
|
|
const size = params?.size ?? 200;
|
|
const targetCategories: IllegalFishingCategory[] = params?.category
|
|
? [params.category]
|
|
: [...ILLEGAL_FISHING_CATEGORIES];
|
|
|
|
const pages: EventPageResponse[] = await Promise.all(
|
|
targetCategories.map((category) =>
|
|
getEvents({
|
|
category,
|
|
level: params?.level,
|
|
status: params?.status,
|
|
vesselMmsi: params?.vesselMmsi,
|
|
page: 0,
|
|
size,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const allEvents: PredictionEvent[] = pages.flatMap((p) => p.content);
|
|
allEvents.sort((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
|
|
const byCategory: Record<string, number> = {};
|
|
const byLevel: Record<string, number> = {};
|
|
for (const e of allEvents) {
|
|
byCategory[e.category] = (byCategory[e.category] ?? 0) + 1;
|
|
byLevel[e.level] = (byLevel[e.level] ?? 0) + 1;
|
|
}
|
|
|
|
return {
|
|
content: allEvents,
|
|
totalElements: pages.reduce((acc, p) => acc + p.totalElements, 0),
|
|
byCategory,
|
|
byLevel,
|
|
};
|
|
}
|