wing-ops/frontend/src/tabs/reports/services/reportsApi.ts

369 lines
11 KiB
TypeScript

import { api } from '@common/services/api';
import type { OilSpillReportData, ReportType, Jurisdiction, ReportStatus, AnalysisCategory } from '../components/OilSpillReportTemplate';
// ============================================================
// API 응답 타입
// ============================================================
export interface ApiTemplateSection {
sectCd: string;
sectNm: string;
fieldDef: { key: string; label: string; type: string; options?: string[] }[];
sortOrd: number;
}
export interface ApiTemplate {
tmplSn: number;
tmplCd: string;
tmplNm: string;
icon: string;
tmplDc: string;
sections: ApiTemplateSection[];
}
export interface ApiCategoryTemplate {
icon: string;
label: string;
}
export interface ApiCategorySection {
sectCd: string;
sectNm: string;
icon: string;
sectDc: string;
dfltYn: string;
sortOrd: number;
}
export interface ApiCategory {
ctgrSn: number;
ctgrCd: string;
ctgrNm: string;
icon: string;
ctgrDc: string;
colorCd: string;
borderColor: string;
bgActive: string;
reportNm: string;
templates: ApiCategoryTemplate[];
sections: ApiCategorySection[];
}
export interface ApiReportListItem {
reportSn: number;
tmplCd: string | null;
tmplNm: string | null;
ctgrCd: string | null;
ctgrNm: string | null;
title: string;
jrsdCd: string | null;
sttsCd: string;
authorId: string;
authorName: string;
regDtm: string;
mdfcnDtm: string | null;
hasMapCapture?: boolean;
}
export interface ApiReportSectionData {
sectCd: string;
includeYn: string;
sectData: unknown;
sortOrd: number;
}
export interface ApiReportDetail extends ApiReportListItem {
acdntSn: number | null;
sections: ApiReportSectionData[];
mapCaptureImg?: string | null;
}
export interface ApiReportListResponse {
items: ApiReportListItem[];
totalCount: number;
page: number;
size: number;
}
// ============================================================
// 코드 매핑
// ============================================================
const STATUS_TO_CODE: Record<ReportStatus, string> = {
'완료': 'COMPLETED',
'수행중': 'IN_PROGRESS',
'테스트': 'DRAFT',
};
const CODE_TO_STATUS: Record<string, ReportStatus> = {
COMPLETED: '완료',
IN_PROGRESS: '수행중',
DRAFT: '테스트',
};
const TMPL_CODE_TO_TYPE: Record<string, ReportType> = {
INITIAL: '초기보고서',
COMMAND: '지휘부 보고',
FORECAST: '예측보고서',
COMPREHENSIVE: '종합보고서',
SPILL: '유출유 보고',
};
const TYPE_TO_TMPL_CODE: Record<ReportType, string> = {
'초기보고서': 'INITIAL',
'지휘부 보고': 'COMMAND',
'예측보고서': 'FORECAST',
'종합보고서': 'COMPREHENSIVE',
'유출유 보고': 'SPILL',
};
const CTGR_CODE_TO_CAT: Record<string, AnalysisCategory> = {
OIL: '유출유 확산예측',
HNS: 'HNS 대기확산',
RESCUE: '긴급구난',
};
const CAT_TO_CTGR_CODE: Record<string, string> = {
'유출유 확산예측': 'OIL',
'HNS 대기확산': 'HNS',
'긴급구난': 'RESCUE',
};
// ============================================================
// 캐시 (템플릿/카테고리 — 변경 빈도 낮음)
// ============================================================
let templatesCache: ApiTemplate[] | null = null;
let categoriesCache: ApiCategory[] | null = null;
// ============================================================
// API 함수
// ============================================================
export async function fetchTemplates(): Promise<ApiTemplate[]> {
if (templatesCache) return templatesCache;
const res = await api.get<ApiTemplate[]>('/reports/templates');
templatesCache = res.data;
return res.data;
}
export async function fetchCategories(): Promise<ApiCategory[]> {
if (categoriesCache) return categoriesCache;
const res = await api.get<ApiCategory[]>('/reports/categories');
categoriesCache = res.data;
return res.data;
}
export async function fetchReports(params?: {
jrsdCd?: string;
tmplCd?: string;
sttsCd?: string;
search?: string;
page?: number;
size?: number;
}): Promise<ApiReportListResponse> {
const res = await api.get<ApiReportListResponse>('/reports', { params });
return res.data;
}
export async function fetchReport(sn: number): Promise<ApiReportDetail> {
const res = await api.get<ApiReportDetail>(`/reports/${sn}`);
return res.data;
}
export async function createReportApi(input: {
tmplSn?: number;
ctgrSn?: number;
acdntSn?: number;
title: string;
jrsdCd?: string;
sttsCd?: string;
mapCaptureImg?: string;
sections?: { sectCd: string; includeYn?: string; sectData: unknown; sortOrd?: number }[];
}): Promise<{ sn: number }> {
const res = await api.post<{ sn: number }>('/reports', input);
return res.data;
}
export async function updateReportApi(sn: number, input: {
title?: string;
jrsdCd?: string;
sttsCd?: string;
acdntSn?: number | null;
mapCaptureImg?: string | null;
sections?: { sectCd: string; includeYn?: string; sectData: unknown; sortOrd?: number }[];
}): Promise<void> {
await api.post(`/reports/${sn}/update`, input);
}
export async function deleteReportApi(sn: number): Promise<void> {
await api.post(`/reports/${sn}/delete`);
}
// ============================================================
// OilSpillReportData ↔ API 변환
// ============================================================
const SECTION_KEYS = [
'incident', 'tide', 'weather', 'spread', 'aquaculture',
'beaches', 'markets', 'esi', 'species', 'habitat',
'sensitivity', 'vessels', 'recovery', 'result',
] as const;
async function findTmplSn(reportType: ReportType): Promise<number | undefined> {
const tmpls = await fetchTemplates();
const code = TYPE_TO_TMPL_CODE[reportType];
return tmpls.find(t => t.tmplCd === code)?.tmplSn;
}
async function findCtgrSn(category: AnalysisCategory): Promise<number | undefined> {
if (!category) return undefined;
const cats = await fetchCategories();
const code = CAT_TO_CTGR_CODE[category];
return cats.find(c => c.ctgrCd === code)?.ctgrSn;
}
export async function saveReport(data: OilSpillReportData): Promise<number> {
const tmplSn = await findTmplSn(data.reportType);
const ctgrSn = data.analysisCategory ? await findCtgrSn(data.analysisCategory) : undefined;
const sttsCd = STATUS_TO_CODE[data.status] || 'DRAFT';
const sections: { sectCd: string; sectData: unknown; sortOrd: number }[] = [];
let sortOrd = 0;
for (const key of SECTION_KEYS) {
sections.push({ sectCd: key, sectData: data[key], sortOrd: sortOrd++ });
}
// analysis + etcEquipment 합산
sections.push({ sectCd: 'analysis', sectData: { analysis: data.analysis, etcEquipment: data.etcEquipment }, sortOrd: sortOrd++ });
// reportSn이 있으면 update, 없으면 create
const existingSn = (data as OilSpillReportData & { reportSn?: number }).reportSn;
if (existingSn) {
await updateReportApi(existingSn, {
title: data.title || data.incident.name || '보고서',
jrsdCd: data.jurisdiction,
sttsCd,
mapCaptureImg: data.capturedMapImage !== undefined ? (data.capturedMapImage || null) : undefined,
sections,
});
return existingSn;
}
const result = await createReportApi({
tmplSn,
ctgrSn,
title: data.title || data.incident.name || '보고서',
jrsdCd: data.jurisdiction,
sttsCd,
mapCaptureImg: data.capturedMapImage || undefined,
sections,
});
return result.sn;
}
export function apiListItemToReportData(item: ApiReportListItem): OilSpillReportData {
return {
id: String(item.reportSn),
title: item.title,
createdAt: item.regDtm,
updatedAt: item.mdfcnDtm || item.regDtm,
author: item.authorName || '',
reportType: (item.tmplCd ? TMPL_CODE_TO_TYPE[item.tmplCd] : '초기보고서') || '초기보고서',
analysisCategory: (item.ctgrCd ? CTGR_CODE_TO_CAT[item.ctgrCd] : '') || '',
jurisdiction: (item.jrsdCd as Jurisdiction) || '남해청',
status: CODE_TO_STATUS[item.sttsCd] || '테스트',
hasMapCapture: item.hasMapCapture,
// 목록에서는 섹션 데이터 없음 — 빈 기본값
incident: { name: '', writeTime: '', shipName: '', agent: '', location: '', lat: '', lon: '', occurTime: '', accidentType: '', pollutant: '', spillAmount: '', depth: '', seabed: '' },
tide: [], weather: [], spread: [],
analysis: '', aquaculture: [], beaches: [], markets: [],
esi: [], species: [], habitat: [], sensitivity: [],
vessels: [], etcEquipment: '', recovery: [],
result: { spillTotal: '', weatheredTotal: '', recoveredTotal: '', seaRemainTotal: '', coastAttachTotal: '' },
};
}
export function apiDetailToReportData(detail: ApiReportDetail): OilSpillReportData & { reportSn: number } {
const base = apiListItemToReportData(detail);
const reportData = { ...base, reportSn: detail.reportSn } as OilSpillReportData & { reportSn: number };
// 섹션 데이터 복원
for (const sect of detail.sections) {
const d = sect.sectData as Record<string, unknown>;
if (!d) continue;
switch (sect.sectCd) {
case 'incident':
reportData.incident = d as OilSpillReportData['incident'];
break;
case 'tide':
reportData.tide = d as OilSpillReportData['tide'];
break;
case 'weather':
reportData.weather = d as OilSpillReportData['weather'];
break;
case 'spread':
reportData.spread = d as OilSpillReportData['spread'];
break;
case 'analysis':
if (typeof d === 'object' && d !== null) {
reportData.analysis = (d as { analysis?: string }).analysis || '';
reportData.etcEquipment = (d as { etcEquipment?: string }).etcEquipment || '';
}
break;
case 'aquaculture':
reportData.aquaculture = d as OilSpillReportData['aquaculture'];
break;
case 'beaches':
reportData.beaches = d as OilSpillReportData['beaches'];
break;
case 'markets':
reportData.markets = d as OilSpillReportData['markets'];
break;
case 'esi':
reportData.esi = d as OilSpillReportData['esi'];
break;
case 'species':
reportData.species = d as OilSpillReportData['species'];
break;
case 'habitat':
reportData.habitat = d as OilSpillReportData['habitat'];
break;
case 'sensitivity':
reportData.sensitivity = d as OilSpillReportData['sensitivity'];
break;
case 'vessels':
reportData.vessels = d as OilSpillReportData['vessels'];
break;
case 'recovery':
reportData.recovery = d as OilSpillReportData['recovery'];
break;
case 'result':
reportData.result = d as OilSpillReportData['result'];
break;
}
}
// location이 비어있고 좌표가 있으면 좌표 문자열로 대체 (기존 보고서 대응)
if (!reportData.incident.location && reportData.incident.lat && reportData.incident.lon) {
reportData.incident.location =
`위도 ${parseFloat(reportData.incident.lat).toFixed(4)}, 경도 ${parseFloat(reportData.incident.lon).toFixed(4)}`;
}
if (detail.mapCaptureImg) {
reportData.capturedMapImage = detail.mapCaptureImg;
}
return reportData;
}
export async function loadReportsFromApi(): Promise<OilSpillReportData[]> {
const res = await fetchReports({ size: 100 });
return res.items.map(apiListItemToReportData);
}
export async function loadReportDetail(sn: number): Promise<OilSpillReportData & { reportSn: number }> {
const detail = await fetchReport(sn);
return apiDetailToReportData(detail);
}