kcg-ai-monitoring/frontend/src/services/parentInferenceApi.ts
htlee 9251d7593c refactor: 프로젝트 뼈대 정리 — iran 잔재 제거 + 백엔드 계층 분리 + 카탈로그 등록
iran 백엔드 프록시 잔재 제거:
- IranBackendClient dead class 삭제, AppProperties/application.yml iran-backend 블록 제거
- Frontend UI 라벨/주석/system-flow manifest deprecated 마킹
- CLAUDE.md 시스템 구성 다이어그램 최신화

백엔드 계층 분리:
- AlertController/MasterDataController/AdminStatsController 에서 repository/JdbcTemplate 직접 주입 제거
- AlertService/MasterDataService/AdminStatsService 신규 계층 도입 + @Transactional(readOnly=true)
- Proxy controller 의 @PostConstruct RestClient 생성 → RestClientConfig @Bean 으로 통합

감사 로그 보강:
- EnforcementService createRecord/updateRecord/createPlan 에 @Auditable 추가
- VesselAnalysisGroupService.resolveParent 에 PARENT_RESOLVE 액션 기록

카탈로그 정합성:
- performanceStatus 를 catalogRegistry 에 등록 (쇼케이스 자동 노출)
- alertLevels 확장: isValidAlertLevel / isHighSeverity / getAlertLevelOrder
- LiveMapView/DarkVesselDetection 시각 매핑(opacity/radius/tier score) 상수로 추출
- GearIdentification/vesselAnomaly 직접 분기를 타입 가드/헬퍼로 치환
2026-04-16 16:18:18 +09:00

185 lines
5.7 KiB
TypeScript

/**
* 모선 워크플로우 API 클라이언트.
* - 후보/리뷰/운영자 결정 모두 자체 백엔드 + 자체 DB(gear_group_parent_resolution) 경유.
*/
const API_BASE = import.meta.env.VITE_API_URL ?? '/api';
export interface ParentResolution {
id: number;
groupKey: string;
subClusterId: number;
status: 'UNRESOLVED' | 'MANUAL_CONFIRMED' | 'REVIEW_REQUIRED';
selectedParentMmsi: string | null;
rejectedCandidateMmsi: string | null;
approvedBy: string | null;
approvedAt: string | null;
rejectedAt: string | null;
manualComment: string | null;
createdAt: string;
updatedAt: string;
}
export interface CandidateExclusion {
id: number;
scopeType: 'GROUP' | 'GLOBAL';
groupKey: string | null;
subClusterId: number | null;
excludedMmsi: string;
reason: string | null;
actor: string | null;
actorAcnt: string | null;
createdAt: string;
releasedAt: string | null;
releasedByAcnt: string | null;
}
export interface LabelSession {
id: number;
groupKey: string;
subClusterId: number;
labelParentMmsi: string;
status: 'ACTIVE' | 'CANCELLED' | 'COMPLETED';
activeFrom: string;
activeUntil: string | null;
createdByAcnt: string | null;
cancelledAt: string | null;
cancelReason: string | null;
createdAt: string;
}
export interface ReviewLog {
id: number;
groupKey: string;
subClusterId: number | null;
action: string;
selectedParentMmsi: string | null;
actorAcnt: string | null;
comment: string | null;
createdAt: string;
}
export interface PageResponse<T> {
content: T[];
totalElements: number;
totalPages: number;
number: number;
size: number;
}
async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
...init,
});
if (!res.ok) {
let errBody = '';
try { errBody = await res.text(); } catch { /* ignore */ }
throw new Error(`API ${res.status}: ${path} ${errBody}`);
}
return res.json();
}
// ============================================================================
// Resolution
// ============================================================================
export function fetchReviewList(status?: string, page = 0, size = 20) {
const qs = new URLSearchParams();
if (status) qs.set('status', status);
qs.set('page', String(page));
qs.set('size', String(size));
return apiRequest<PageResponse<ParentResolution>>(`/parent-inference/review?${qs}`);
}
export function reviewParent(
groupKey: string,
subClusterId: number,
payload: { action: 'CONFIRM' | 'REJECT' | 'RESET'; selectedParentMmsi?: string; comment?: string },
) {
return apiRequest<ParentResolution>(
`/parent-inference/groups/${encodeURIComponent(groupKey)}/${subClusterId}/review`,
{ method: 'POST', body: JSON.stringify(payload) },
);
}
// ============================================================================
// Exclusions
// ============================================================================
export function fetchExclusions(scopeType?: 'GROUP' | 'GLOBAL', page = 0, size = 20) {
const qs = new URLSearchParams();
if (scopeType) qs.set('scopeType', scopeType);
qs.set('page', String(page));
qs.set('size', String(size));
return apiRequest<PageResponse<CandidateExclusion>>(`/parent-inference/exclusions?${qs}`);
}
export function excludeForGroup(
groupKey: string,
subClusterId: number,
payload: { excludedMmsi: string; reason?: string },
) {
return apiRequest<CandidateExclusion>(
`/parent-inference/groups/${encodeURIComponent(groupKey)}/${subClusterId}/exclusions`,
{ method: 'POST', body: JSON.stringify(payload) },
);
}
export function excludeGlobal(payload: { excludedMmsi: string; reason?: string }) {
return apiRequest<CandidateExclusion>(`/parent-inference/exclusions/global`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export function releaseExclusion(exclusionId: number, reason?: string) {
return apiRequest<CandidateExclusion>(`/parent-inference/exclusions/${exclusionId}/release`, {
method: 'POST',
body: JSON.stringify({ reason }),
});
}
// ============================================================================
// Label Sessions
// ============================================================================
export function fetchLabelSessions(status?: string, page = 0, size = 20) {
const qs = new URLSearchParams();
if (status) qs.set('status', status);
qs.set('page', String(page));
qs.set('size', String(size));
return apiRequest<PageResponse<LabelSession>>(`/parent-inference/label-sessions?${qs}`);
}
export function createLabelSession(
groupKey: string,
subClusterId: number,
payload: { labelParentMmsi: string; anchorSnapshot?: Record<string, unknown> },
) {
return apiRequest<LabelSession>(
`/parent-inference/groups/${encodeURIComponent(groupKey)}/${subClusterId}/label-sessions`,
{ method: 'POST', body: JSON.stringify(payload) },
);
}
export function cancelLabelSession(sessionId: number, reason?: string) {
return apiRequest<LabelSession>(`/parent-inference/label-sessions/${sessionId}/cancel`, {
method: 'POST',
body: JSON.stringify({ reason }),
});
}
// ============================================================================
// Review Logs (도메인 액션 이력)
// ============================================================================
export function fetchReviewLogs(groupKey?: string, page = 0, size = 50) {
const qs = new URLSearchParams();
if (groupKey) qs.set('groupKey', groupKey);
qs.set('page', String(page));
qs.set('size', String(size));
return apiRequest<PageResponse<ReviewLog>>(`/parent-inference/review-logs?${qs}`);
}