389 lines
14 KiB
TypeScript
389 lines
14 KiB
TypeScript
import type {
|
|
ExmnAttachmentKey,
|
|
ExmnBusiness,
|
|
ExmnBusinessDetail,
|
|
ExmnBusinessSaveRequest,
|
|
ExmnBusinessSearchParams,
|
|
ExmnFileType,
|
|
ExmnImportCommitResult,
|
|
ExmnImportValidationResult,
|
|
ExmnJobType,
|
|
ExmnPage,
|
|
ExmnPipeReportAvailability,
|
|
ExmnSectionSummaryRow,
|
|
ExmnTemplateResult,
|
|
} from "../model/exmn-business-types";
|
|
import {
|
|
type ExmnDefectGroup,
|
|
type ExmnDetailSeverity,
|
|
type ExmnPplUnitDetailRow,
|
|
type ExmnSurveyDetailRow,
|
|
PIPE_DETAIL_DEFECT_COLUMNS,
|
|
} from "../model/exmn-detail-table";
|
|
|
|
import { getBlobData, postMutiPart } from "@/shared/api/file-api";
|
|
import { getData, getRawJson, postRawJson, putData } from "@/shared/api/sewerage-api";
|
|
|
|
/** 조사사업 API의 공통 기준 경로다. */
|
|
const EXMN_BUSINESS_API_PATH = "v1/exmnBusinesses";
|
|
|
|
/** 첨부서비스의 사업보고서 원본 파일명 조회에 필요한 최소 응답 계약이다. */
|
|
interface AttachmentMetadata {
|
|
atflId?: string | null;
|
|
orgnlFileNm?: string | null;
|
|
}
|
|
|
|
/** 공통 응답 원문 또는 data 래퍼 양쪽에서 실제 payload를 해석한다. */
|
|
const unwrapData = <T>(response: T | { data?: T }): T =>
|
|
typeof response === "object" && response !== null && "data" in response
|
|
? (response as { data: T }).data
|
|
: (response as T);
|
|
|
|
/** Spring Page 응답이 비어 있거나 아직 미구현이어도 안전한 빈 Page로 정규화한다. */
|
|
const toPage = <T>(
|
|
value: Partial<ExmnPage<T>> | null | undefined,
|
|
): ExmnPage<T> => ({
|
|
content: Array.isArray(value?.content) ? value.content : [],
|
|
totalElements: Number(value?.totalElements ?? 0),
|
|
number: Number(value?.number ?? 0),
|
|
size: Number(value?.size ?? 20),
|
|
totalPages: Number(value?.totalPages ?? 0),
|
|
});
|
|
|
|
/**
|
|
* 사업보고서 첨부 코드로 원본 파일명을 조회한다.
|
|
* 첨부 메타데이터 장애가 조사사업 목록 전체를 막지 않도록 실패 시 null로 대체한다.
|
|
*/
|
|
const getBusinessReportFileName = async (
|
|
attachmentCode: string,
|
|
attachmentId: string,
|
|
): Promise<string | null> => {
|
|
try {
|
|
const response = (await getRawJson(
|
|
`/attach-service/api/v1/atfl/attachments/${encodeURIComponent(attachmentCode)}`,
|
|
)) as
|
|
| AttachmentMetadata[]
|
|
| { data?: AttachmentMetadata[] | { content?: AttachmentMetadata[] } }
|
|
| { content?: AttachmentMetadata[] }
|
|
| null;
|
|
let payload:
|
|
| AttachmentMetadata[]
|
|
| { content?: AttachmentMetadata[] }
|
|
| null
|
|
| undefined;
|
|
if (Array.isArray(response)) {
|
|
payload = response;
|
|
} else if (response && "data" in response) {
|
|
payload = response.data;
|
|
} else {
|
|
payload = response as { content?: AttachmentMetadata[] } | null;
|
|
}
|
|
const attachments = Array.isArray(payload)
|
|
? payload
|
|
: Array.isArray(payload?.content)
|
|
? payload.content
|
|
: [];
|
|
const attachment =
|
|
attachments.find((item) => item.atflId === attachmentId) ??
|
|
attachments[0];
|
|
return attachment?.orgnlFileNm?.trim() || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/** 조사사업 목록을 조회조건·0-base 페이지·서버 정렬 계약으로 요청한다. */
|
|
export const getExmnBusinesses = async (
|
|
params: ExmnBusinessSearchParams,
|
|
): Promise<ExmnPage<ExmnBusiness>> => {
|
|
const query = new URLSearchParams({
|
|
page: String(params.page),
|
|
size: String(params.size),
|
|
});
|
|
if (params.bizCycl?.trim()) query.set("bizCycl", params.bizCycl.trim());
|
|
if (params.bizYr?.trim()) query.set("bizYr", params.bizYr.trim());
|
|
if (params.ctpvNm?.trim()) query.set("ctpvNm", params.ctpvNm.trim());
|
|
if (params.sggNm?.trim()) query.set("sggNm", params.sggNm.trim());
|
|
if (params.bizNm?.trim()) query.set("bizNm", params.bizNm.trim());
|
|
if (params.sort) query.set("sort", params.sort);
|
|
|
|
const page = toPage<ExmnBusiness>(
|
|
await getData(`${EXMN_BUSINESS_API_PATH}?${query}`),
|
|
);
|
|
const content = await Promise.all(
|
|
page.content.map(async (business) => {
|
|
const attachmentCode = business.bizRptpAtchFileCd?.trim();
|
|
const attachmentId = business.bizRptpAtchFileId?.trim();
|
|
if (!attachmentCode || !attachmentId) return business;
|
|
return {
|
|
...business,
|
|
bizRptpFileName: await getBusinessReportFileName(
|
|
attachmentCode,
|
|
attachmentId,
|
|
),
|
|
};
|
|
}),
|
|
);
|
|
return { ...page, content };
|
|
};
|
|
|
|
/** 선택 사업의 처리분구별 관로·맨홀 조사 상세현황을 조회한다. */
|
|
export const getExmnSectionSummaries = async (
|
|
exmnBizId: string,
|
|
jobType: ExmnJobType,
|
|
): Promise<ExmnSectionSummaryRow[]> => {
|
|
type SectionSummaryPayload =
|
|
| ExmnSectionSummaryRow[]
|
|
| Partial<ExmnPage<ExmnSectionSummaryRow>>;
|
|
try {
|
|
const response = (await getData(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/sections?jobType=${jobType}`,
|
|
)) as SectionSummaryPayload | { data?: SectionSummaryPayload };
|
|
const payload = unwrapData<SectionSummaryPayload>(response);
|
|
if (Array.isArray(payload)) return payload;
|
|
return Array.isArray(payload?.content) ? payload.content : [];
|
|
} catch (error) {
|
|
// 상세현황 API가 아직 없거나 선택 사업에 조사결과가 없는 404는 정상 빈 목록으로 표시한다.
|
|
if ((error as { status?: number } | null)?.status === 404) return [];
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 현재 조사유형의 종합집계 1개와 처리분구별 전체 상세 시트를 하나의 XLSX로 내려받는다.
|
|
* 관로 본단위구간은 이 다운로드 계약에 포함하지 않는다.
|
|
*/
|
|
export const downloadExmnSectionSummary = async (
|
|
exmnBizId: string,
|
|
jobType: ExmnJobType,
|
|
): Promise<Blob> => {
|
|
const query = new URLSearchParams({ jobType });
|
|
return getBlobData(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/sections/download?${query}`,
|
|
);
|
|
};
|
|
|
|
/** 선택 사업·처리분구의 관로·맨홀 전체 상세 집계 행을 조회한다. */
|
|
export const getExmnFacilityDetails = async (params: {
|
|
exmnBizId: string;
|
|
jobType: ExmnJobType;
|
|
trtdstId: string;
|
|
gisManagementNo?: string;
|
|
defectType?: ExmnDefectGroup | "ALL";
|
|
}): Promise<ExmnSurveyDetailRow[]> => {
|
|
const query = new URLSearchParams({
|
|
exmnBizId: params.exmnBizId,
|
|
jobType: params.jobType,
|
|
trtdstId: params.trtdstId,
|
|
});
|
|
if (params.gisManagementNo?.trim()) {
|
|
query.set("gisManagementNo", params.gisManagementNo.trim());
|
|
}
|
|
if (params.defectType && params.defectType !== "ALL") {
|
|
query.set("defectType", params.defectType);
|
|
}
|
|
const response = (await getData(`v1/exmnFacilities?${query}`)) as
|
|
| ExmnSurveyDetailRow[]
|
|
| { data?: ExmnSurveyDetailRow[] };
|
|
const payload = unwrapData<ExmnSurveyDetailRow[]>(response);
|
|
return Array.isArray(payload) ? payload : [];
|
|
};
|
|
|
|
interface ExmnPplUnitDetailPayload {
|
|
pipeNo: string;
|
|
unitSeq: number;
|
|
unitLengthM: number;
|
|
defectPositionM: number | null;
|
|
code: string;
|
|
recordType: "STATUS" | "DEFECT" | null;
|
|
defectType: ExmnDefectGroup | null;
|
|
severity: ExmnDetailSeverity | null;
|
|
occurrencePosition: string;
|
|
infiltrationBonus: number | null;
|
|
continuous: boolean | null;
|
|
structuralScore: number | null;
|
|
operationalScore: number | null;
|
|
surveyStopReason: string | null;
|
|
remark: string | null;
|
|
}
|
|
|
|
/** 조사 상세 표의 0 기반 서버 페이지와 조건 전체 합계를 반환한다. */
|
|
export const getExmnFacilityPage = async (params: {
|
|
exmnBizId: string; jobType: ExmnJobType; trtdstId: string;
|
|
gisManagementNo?: string; defectType: string; defectCodes: string;
|
|
page: number; size: number; sort?: string;
|
|
}): Promise<{ content: ExmnSurveyDetailRow[]; totalElements: number; summary: ExmnSurveyDetailRow }> => {
|
|
const query = new URLSearchParams();
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== "") query.set(key, String(value));
|
|
});
|
|
return unwrapData(await getData(`v1/exmnFacilities/page?${query}`));
|
|
};
|
|
|
|
/** 관로·단위구간 병합을 보존한 서버 그룹 페이지를 화면 표시명으로 변환한다. */
|
|
export const getExmnPplUnitPage = async (params: {
|
|
exmnBizId: string; trtdstId: string; gisManagementNo?: string; page: number; size: number;
|
|
}): Promise<{ content: ExmnPplUnitDetailRow[]; totalElements: number }> => {
|
|
const query = new URLSearchParams();
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== "") query.set(key, String(value));
|
|
});
|
|
const result = unwrapData<{ content: ExmnPplUnitDetailPayload[]; totalElements: number }>(
|
|
await getData(`v1/exmnFacilities/pplUnitSections/page?${query}`),
|
|
);
|
|
return { ...result, content: result.content.map((row) => ({ ...row, defectName: getPplUnitDetailName(row) })) };
|
|
};
|
|
|
|
export const PPL_STATUS_LABELS: Record<string, string> = {
|
|
IS: "조사시작(순주행)",
|
|
ISR: "조사시작(역주행)",
|
|
IE: "조사완료(순주행)",
|
|
IER: "조사완료(역주행)",
|
|
IA: "조사중단",
|
|
JE: "이음부(접합부)존재",
|
|
WD: "이상 없음",
|
|
};
|
|
|
|
const getPplUnitDetailName = (row: ExmnPplUnitDetailPayload) => {
|
|
const code = row.code?.trim();
|
|
if (!code) return "";
|
|
const normalizedCode = code.toUpperCase();
|
|
if (row.recordType === "STATUS") {
|
|
return `(상태)${PPL_STATUS_LABELS[normalizedCode] ?? code}`;
|
|
}
|
|
const defect = PIPE_DETAIL_DEFECT_COLUMNS.find(
|
|
(column) => column.code.toUpperCase() === normalizedCode,
|
|
);
|
|
return `(관로)${defect?.label ?? code}`;
|
|
};
|
|
|
|
/** 선택 처리분구의 PPL_UNIT_V1 본단위구간 상태·결함 행을 조회한다. */
|
|
export const getExmnPplUnitDetails = async (params: {
|
|
exmnBizId: string;
|
|
trtdstId: string;
|
|
gisManagementNo?: string;
|
|
}): Promise<ExmnPplUnitDetailRow[]> => {
|
|
const query = new URLSearchParams({
|
|
exmnBizId: params.exmnBizId,
|
|
trtdstId: params.trtdstId,
|
|
});
|
|
if (params.gisManagementNo?.trim()) {
|
|
query.set("gisManagementNo", params.gisManagementNo.trim());
|
|
}
|
|
const response = (await getData(
|
|
`v1/exmnFacilities/pplUnitSections?${query}`,
|
|
)) as ExmnPplUnitDetailPayload[] | { data?: ExmnPplUnitDetailPayload[] };
|
|
const payload = unwrapData<ExmnPplUnitDetailPayload[]>(response);
|
|
if (!Array.isArray(payload)) return [];
|
|
return payload.map((row) => ({
|
|
...row,
|
|
defectName: getPplUnitDetailName(row),
|
|
}));
|
|
};
|
|
|
|
/** 선택한 조사사업의 처리분구·첨부를 포함한 상세정보를 조회한다. */
|
|
export const getExmnBusiness = async (
|
|
exmnBizId: string,
|
|
): Promise<ExmnBusinessDetail> =>
|
|
getData(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}`,
|
|
) as Promise<ExmnBusinessDetail>;
|
|
|
|
/** 조사사업과 선택 처리분구별 기본 요약행을 신규 저장한다. */
|
|
export const createExmnBusiness = async (
|
|
request: ExmnBusinessSaveRequest,
|
|
): Promise<ExmnBusinessDetail> =>
|
|
unwrapData<ExmnBusinessDetail>(
|
|
await postRawJson(EXMN_BUSINESS_API_PATH, request),
|
|
);
|
|
|
|
/** 기존 조사사업 기본정보와 연결 처리분구를 같은 저장 계약으로 갱신한다. */
|
|
export const updateExmnBusiness = async (
|
|
exmnBizId: string,
|
|
request: ExmnBusinessSaveRequest,
|
|
): Promise<ExmnBusinessDetail> =>
|
|
putData(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}`,
|
|
request,
|
|
) as Promise<ExmnBusinessDetail>;
|
|
|
|
/** 저장된 사업의 처리분구별 PPL/MNHL 엑셀 시트명을 생성한다. */
|
|
export const createExmnTemplates = async (
|
|
exmnBizId: string,
|
|
trtdstIds: string[],
|
|
): Promise<ExmnTemplateResult> =>
|
|
unwrapData<ExmnTemplateResult>(
|
|
await postRawJson(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/templates`,
|
|
{ trtdstIds },
|
|
),
|
|
);
|
|
|
|
/** 생성된 조사사업 입력 양식 파일을 내려받는다. */
|
|
export const downloadExmnTemplates = async (exmnBizId: string): Promise<Blob> =>
|
|
getBlobData(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/templates/download`,
|
|
);
|
|
|
|
/** 하수관로·맨홀 현황조사 보고서 기준 XLSX를 내려받는다. */
|
|
export const downloadExmnReportTemplate = async (): Promise<Blob> =>
|
|
getBlobData(`${EXMN_BUSINESS_API_PATH}/templates/report/download`);
|
|
|
|
/** 사업보고서 첨부파일 ID로 기존 첨부서비스의 원본 파일을 내려받는다. */
|
|
export const downloadExmnBusinessReport = async (
|
|
attachmentId: string,
|
|
): Promise<Blob> =>
|
|
getBlobData(
|
|
`/attach-service/api/v1/download/${encodeURIComponent(attachmentId)}`,
|
|
);
|
|
|
|
/** 저장된 페이지 범위를 이용해 선택 관로의 조사보고서 구간만 PDF로 조회한다. */
|
|
export const downloadExmnPipeReport = async (
|
|
exmnBizId: string,
|
|
pipeNumber: string,
|
|
jobType: "PPL" | "MNHL" = "PPL",
|
|
): Promise<Blob> =>
|
|
getBlobData(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/${jobType === "MNHL" ? "manholeReports" : "pipeReports"}/${encodeURIComponent(pipeNumber)}`,
|
|
);
|
|
|
|
/** 사업 파일별 첨부서비스 키 쌍을 조사사업에 연결한다. */
|
|
/** 팝업 생성 전에 선택 관로번호가 등록된 관로 PDF에 포함되는지 확인한다. */
|
|
export const getExmnPipeReportAvailability = async (
|
|
exmnBizId: string,
|
|
pipeNumber: string,
|
|
jobType: "PPL" | "MNHL" = "PPL",
|
|
): Promise<ExmnPipeReportAvailability> =>
|
|
getData(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/${jobType === "MNHL" ? "manholeReports" : "pipeReports"}/${encodeURIComponent(pipeNumber)}/availability`,
|
|
) as Promise<ExmnPipeReportAvailability>;
|
|
|
|
export const updateExmnAttachments = async (
|
|
exmnBizId: string,
|
|
attachments: Partial<Record<ExmnFileType, ExmnAttachmentKey>>,
|
|
): Promise<Partial<Record<ExmnFileType, ExmnAttachmentKey>>> =>
|
|
putData(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/attachments`,
|
|
{ attachments },
|
|
) as Promise<Partial<Record<ExmnFileType, ExmnAttachmentKey>>>;
|
|
|
|
/** 원본 PPL/MNHL 파일의 업무·JSON 정합성을 서버에서 검토한다. */
|
|
export const validateExmnImport = async (
|
|
exmnBizId: string,
|
|
formData: FormData,
|
|
): Promise<ExmnImportValidationResult> =>
|
|
postMutiPart(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/imports/validate`,
|
|
formData,
|
|
) as Promise<ExmnImportValidationResult>;
|
|
|
|
/** 검토했던 동일 파일과 첨부키를 다시 보내 시설 저장·요약 집계를 확정한다. */
|
|
export const commitExmnImport = async (
|
|
exmnBizId: string,
|
|
formData: FormData,
|
|
): Promise<ExmnImportCommitResult> =>
|
|
postMutiPart(
|
|
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/imports/commit`,
|
|
formData,
|
|
) as Promise<ExmnImportCommitResult>;
|