291 lines
9.3 KiB
TypeScript
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,
|
|
"준설결과를 저장하지 못했습니다. 잠시 후 다시 시도해주세요.",
|
|
"준설결과 저장",
|
|
);
|
|
}
|
|
};
|