369 lines
11 KiB
TypeScript
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);
|
|
}
|