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

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>;