Files
frontend/apps/sn/src/features/dredging/api/dredging-api.ts
T

291 lines
9.3 KiB
TypeScript

import { DREDGING_EXTERNAL_MODE } from "../model/disclosure";
/** 준설 조회·등록·수정·삭제와 최근접 관로 HTTP 계약을 담당한다. */
import {
deleteData,
getRawJson,
postRawJson,
putData,
} from "@/shared/api/sewerage-api";
import { getBlobData } from "@/shared/api/file-api";
import { requestGridExcel } from "@/shared/api/grid-export";
import { isDredgingBackendEnabled } from "./backend-mode";
import { toDredgingApiError } from "./dredging-error";
import {
toDredgingRecordPage,
toDredgingPhotos,
toNearestPipeMatches,
type DredgingRecordListResponse,
} from "./response-adapters";
import type {
DredgingRecordUpdate,
DredgingPhoto,
GeoPoint,
PipeMatch,
} from "../model/types";
import type {
AttachmentReference,
DredgingRegistrationRequest,
} from "../model/registration-request";
export type { AttachmentReference } from "../model/registration-request";
/** 그리드는 0 기반 서버 페이지 하나만 요청한다. 지도·전체 다운로드는 별도 계약이다. */
export type DredgingGridQuery = {
viewType: string;
fromDate: string;
toDate: string;
ctpv?: string;
sgg?: string;
page: number;
size: number;
sort?: string;
};
export const fetchDredgingGridPage = async (
query: DredgingGridQuery,
): Promise<{
content: Array<Record<string, unknown> & { recordIds: string[] }>;
totalElements: number;
number: number;
size: number;
}> => {
const params = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== "") params.set(key, String(value));
});
const body = await getRawJson(`v1/dredging/grid?${params}`);
return body?.data ?? body;
};
/** 적용 조건 전체의 서버 XLSX를 받는다. page/size는 다운로드에 전달하지 않는다. */
export const downloadDredgingGridExcel = (
query: Omit<DredgingGridQuery, "page" | "size">,
): Promise<void> => {
const params = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== "") params.set(key, String(value));
});
return requestGridExcel(
`v1/dredging/grid/exportJobs?${params}`,
{},
`준설관리_${query.viewType}_${query.fromDate}_${query.toDate}.xlsx`,
);
};
type LocationRequest = GeoPoint & {
locationKey: string;
};
/** 실제 준설관리 API에서 기간 내 등록 기록을 조회한다. */
export const fetchDredgingRecords = async (params: {
fromDate: string;
toDate: string;
ctpv?: string;
sgg?: string;
page?: number;
size?: number;
}): Promise<DredgingRecordListResponse> => {
if (!isDredgingBackendEnabled) {
return { items: [], total: 0, page: 0, size: params.size ?? 2000 };
}
const search = new URLSearchParams({
fromDate: params.fromDate,
toDate: params.toDate,
page: String(params.page ?? 0),
size: String(params.size ?? 2000),
});
if (params.ctpv?.trim()) search.set("ctpv", params.ctpv.trim());
if (params.sgg?.trim()) search.set("sgg", params.sgg.trim());
try {
const body = await getRawJson(`v1/dredging/records?${search.toString()}`);
return toDredgingRecordPage(body);
} catch (error) {
throw toDredgingApiError(
error,
"준설현황을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.",
"준설현황 조회",
);
}
};
/** 등록된 준설사진의 공통 첨부파일 참조를 조회한다. */
export const fetchDredgingPhotos = async (
dredgingJobSn: string | number,
pipeFid: string | null,
targetSn?: number,
): Promise<DredgingPhoto[]> => {
if (!isDredgingBackendEnabled) return [];
try {
const body = await getRawJson(
`v1/dredging/records/${encodeURIComponent(dredgingJobSn)}/photos?${targetSn ? `targetSn=${targetSn}` : `fid=${encodeURIComponent(pipeFid ?? "")}`}`,
);
return toDredgingPhotos(body);
} catch (error) {
throw toDredgingApiError(
error,
"등록된 준설사진을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.",
"준설사진 목록 조회",
);
}
};
/** 상세 기반 수정 팝업에서 선택 관로의 일자·위치·사진 구성을 함께 수정한다. */
export const updateDredgingRecord = async (
dredgingJobSn: string | number,
update: DredgingRecordUpdate,
attachments: Map<string, AttachmentReference>,
) => {
if (!isDredgingBackendEnabled) return true;
try {
await putData(`v1/dredging/records/${encodeURIComponent(dredgingJobSn)}`, {
dredgingDate: update.dredgingDate,
targetSn: update.targetSn,
registrationMode: update.registrationMode,
location: {
source: update.locationSource,
longitude: update.location.longitude,
latitude: update.location.latitude,
},
expectedPipe: update.pipe && !DREDGING_EXTERNAL_MODE ? {
fid: update.pipe.fid,
mngInstCd: update.pipe.mngInstCd,
fcltMngNo: update.pipe.fcltMngNo,
} : null,
retainedPhotoSns: update.retainedPhotoSns,
newPhotos: update.newPhotos.map((photo) => {
const attachment = attachments.get(photo.key);
if (!attachment) {
throw new Error(`${photo.file.name}의 첨부 식별자가 없습니다.`);
}
return {
...attachment,
originalFileName: photo.file.name,
clientCapturedAt: photo.metadata.capturedAt,
clientLongitude: photo.metadata.location?.longitude ?? null,
clientLatitude: photo.metadata.location?.latitude ?? null,
};
}),
});
return true;
} catch (error) {
throw toDredgingApiError(
error,
"준설정보를 수정하지 못했습니다. 잠시 후 다시 시도해주세요.",
"준설작업 수정",
);
}
};
/** 목록에서 선택한 준설작업과 관로·사진 참조를 모두 물리 삭제한다. */
export const deleteDredgingRecord = async (dredgingJobSn: string | number) => {
if (!isDredgingBackendEnabled) return true;
try {
const response = await deleteData(
`v1/dredging/records/${encodeURIComponent(dredgingJobSn)}`,
);
if (response instanceof Response && !response.ok) {
throw new Error(`준설작업 삭제 응답 오류: ${response.status}`);
}
return true;
} catch (error) {
throw toDredgingApiError(
error,
"준설정보를 삭제하지 못했습니다. 잠시 후 다시 시도해주세요.",
"준설작업 삭제",
);
}
};
/** 공통 첨부서비스에서 준설사진 원본을 Blob으로 내려받는다. */
export const fetchDredgingPhotoBlob = async (atflId: string) => {
try {
return await getBlobData(
`/attach-service/api/v1/download/${encodeURIComponent(atflId)}`,
);
} catch (error) {
throw toDredgingApiError(
error,
"사진 원본을 불러오지 못했습니다.",
"준설사진 원본 조회",
);
}
};
const mockPipeForLocations = (locations: LocationRequest[]): PipeMatch => {
const first = locations[0];
const longitude = first?.longitude ?? 127.5;
const latitude = first?.latitude ?? 36.5;
const seed = Math.abs(
Math.round(longitude * 100_000) + Math.round(latitude * 100_000),
);
const distanceMeters = Number(((seed % 850) / 100 + 1.2).toFixed(2));
return {
fid: `MOCK-${String(seed % 1_000_000).padStart(6, "0")}`,
mngInstCd: "4173000",
fcltMngNo: `PPL-${String(seed % 1_000_000).padStart(6, "0")}`,
pipeDiameterMm: [450, 600, 700, 900][seed % 4] ?? 450,
pipeLengthM: Number((((seed % 8_000) + 2_000) / 100).toFixed(2)),
pipeMaterial: ["콘크리트", "PVC", "PE"][seed % 3] ?? "콘크리트",
installationYear: 1995 + (seed % 25),
swtrmtZoneFid: "MOCK-ZONE",
swtrmtZoneNm: "모의처리구역",
distanceMeters,
snappedLongitude: longitude + 0.00001,
snappedLatitude: latitude - 0.00001,
isMock: true,
};
};
/**
* 위치 목록을 한 번에 보내고 사진별 최근접 관로를 받는다.
* FE 선행 모드에서는 같은 그룹의 상태 전이를 검증할 수 있는 명시적 모의 관로를 반환한다.
*/
export const findNearestPipeMatches = async (
locations: LocationRequest[],
): Promise<Map<string, PipeMatch>> => {
if (DREDGING_EXTERNAL_MODE || locations.length === 0) return new Map();
if (!isDredgingBackendEnabled) {
const mockPipe = mockPipeForLocations(locations);
return new Map(
locations.map((location) => [location.locationKey, mockPipe]),
);
}
let body: any;
try {
body = await postRawJson("v1/dredging/nearestPipes", { locations });
} catch (error) {
throw toDredgingApiError(
error,
"최근접 관로를 조회하지 못했습니다. 잠시 후 다시 시도해주세요.",
"최근접 관로 조회",
);
}
return toNearestPipeMatches(body);
};
/** UI 세션이 확정한 요청을 전송하며 transport 내부에서는 ID를 재생성하지 않는다. */
export const saveDredgingRegistrations = async (
request: DredgingRegistrationRequest,
) => {
if (!isDredgingBackendEnabled) {
return {
mock: true,
registeredCount: 1,
clientRequestId: request.clientRequestId,
};
}
try {
return await postRawJson("v1/dredging/registrations", request);
} catch (error) {
throw toDredgingApiError(
error,
"준설결과를 저장하지 못했습니다. 잠시 후 다시 시도해주세요.",
"준설결과 저장",
);
}
};