Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev

This commit is contained in:
2026-10-22 09:44:24 +09:00
9 changed files with 144 additions and 37 deletions
@@ -341,9 +341,10 @@ export const downloadExmnBusinessReport = async (
export const downloadExmnPipeReport = async (
exmnBizId: string,
pipeNumber: string,
jobType: "PPL" | "MNHL" = "PPL",
): Promise<Blob> =>
getBlobData(
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/pipeReports/${encodeURIComponent(pipeNumber)}`,
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/${jobType === "MNHL" ? "manholeReports" : "pipeReports"}/${encodeURIComponent(pipeNumber)}`,
);
/** 사업 파일별 첨부서비스 키 쌍을 조사사업에 연결한다. */
@@ -351,9 +352,10 @@ export const downloadExmnPipeReport = async (
export const getExmnPipeReportAvailability = async (
exmnBizId: string,
pipeNumber: string,
jobType: "PPL" | "MNHL" = "PPL",
): Promise<ExmnPipeReportAvailability> =>
getData(
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/pipeReports/${encodeURIComponent(pipeNumber)}/availability`,
`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/${jobType === "MNHL" ? "manholeReports" : "pipeReports"}/${encodeURIComponent(pipeNumber)}/availability`,
) as Promise<ExmnPipeReportAvailability>;
export const updateExmnAttachments = async (
@@ -385,7 +385,7 @@ export function ExmnRegistrationPage() {
if (refreshed)
setNotice({
type: "success",
text: "조사결과 최종 저장과 PPL/MNHL 집계를 완료했습니다.",
text: "조사결과 저장을 완료했습니다.",
});
});
}
@@ -71,7 +71,7 @@ const VALIDATION_CHECK_GUIDES: Partial<
},
MNHL_EXMN_PDF: {
checkedItems: ["PDF 파일 형식", "조사결과 Excel 구조 및 저장 가능 여부"],
pendingNote: "PDF 내부 맨홀번호 대조는 검토 기준 협의 후 적용 예정입니다.",
pendingNote: "맨홀보고서의 맨홀번호에는 실제 GIS 맨홀번호를 입력해주세요. 보고서 열람 시 번호를 대조하며, 업로드 검토에는 PDF 내부 번호 대조가 포함되지 않습니다.",
},
};
@@ -179,6 +179,9 @@ export function ExmnResultRegisterPopupPage({
const [isLoading, setIsLoading] = useState(false);
const [notice, setNotice] = useState<Notice>(null);
/** 맨홀 PDF 하나만 선택한 경우 기존 조사자료의 첨부 교체로 처리한다. */
const isManholeOnly = Boolean(files.MNHL_EXMN_PDF) && Object.keys(files).length === 1;
/** popup layout 상위 html/body에서 생기는 문서 스크롤바를 팝업이 열린 동안 잠근다. */
useEffect(() => {
const previousHtmlOverflow = document.documentElement.style.overflow;
@@ -310,7 +313,9 @@ export function ExmnResultRegisterPopupPage({
const fileMap = Object.fromEntries(
Object.entries(files).map(([type, selected]) => [type, selected?.file]),
) as Partial<Record<ExmnFileType, File>>;
const validationMessage = validateExmnFiles(fileMap);
const validationMessage = isManholeOnly
? validateExmnFile("MNHL_EXMN_PDF", files.MNHL_EXMN_PDF!.file)
: validateExmnFiles(fileMap);
if (validationMessage) {
setFileValidationStatuses((current) => ({
...current,
@@ -373,22 +378,22 @@ export function ExmnResultRegisterPopupPage({
/** 정상 검토된 조사결과 Excel을 저장하고 부모 화면에 완료 메시지를 보낸 뒤 팝업을 닫는다. */
const handleCommit = async () => {
const excelValidation = validationResults.EXMN_EXCEL;
if (
!excelValidation?.valid ||
fileValidationStatuses.EXMN_EXCEL !== "success" ||
fileValidationStatuses.PPL_EXMN_PDF !== "success"
)
return;
if (!canCommit) return;
setIsLoading(true);
setNotice(null);
try {
if (!(await ensureTemplateReady())) return;
if (!isManholeOnly && !(await ensureTemplateReady())) return;
// 로딩 화면이 저장 확인창을 가리지 않도록 사전 확인과 업로드 상태를 나눈다.
setIsLoading(false);
if (!(await confirm("조사결과를 저장하시겠습니까?"))) return;
setIsLoading(true);
if (isManholeOnly) {
// 검토 이후 기존 조사자료가 변경됐을 수 있어 첨부 업로드 직전에 다시 대조한다.
const result = await validateExmnImport(exmnBizId, buildImportFormData(files, null, "MNHL_EXMN_PDF"));
if (!result.valid) throw new Error(result.errors.map((item) => item.message).join("\n"));
}
await uploadAndLinkAttachments();
await commitExmnImport(
if (!isManholeOnly) await commitExmnImport(
exmnBizId,
buildImportFormData(files, excelValidation),
);
@@ -418,6 +423,11 @@ export function ExmnResultRegisterPopupPage({
(fileType) => fileValidationStatuses[fileType],
);
const excelValidation = validationResults.EXMN_EXCEL;
/** 단독 첨부 교체와 신규 Excel 결과 저장의 필수 검토 조건을 분리한다. */
const canCommit = isManholeOnly
? fileValidationStatuses.MNHL_EXMN_PDF === "success" && Boolean(validationResults.MNHL_EXMN_PDF?.valid)
: Boolean(excelValidation?.valid) && fileValidationStatuses.EXMN_EXCEL === "success"
&& fileValidationStatuses.PPL_EXMN_PDF === "success";
return (
<StandardPopupLayout
@@ -429,10 +439,7 @@ export function ExmnResultRegisterPopupPage({
type="button"
color="primary"
disabled={
isLoading ||
!excelValidation?.valid ||
fileValidationStatuses.EXMN_EXCEL !== "success" ||
fileValidationStatuses.PPL_EXMN_PDF !== "success"
isLoading || !canCommit
}
onClick={() => void handleCommit()}
>
@@ -614,7 +621,9 @@ export function ExmnResultRegisterPopupPage({
const status = fileValidationStatuses[fileType]!;
const result = validationResults[fileType];
const messages = fileValidationMessages[fileType] ?? [];
const guide = VALIDATION_CHECK_GUIDES[fileType];
const guide = fileType === "MNHL_EXMN_PDF" && isManholeOnly
? { checkedItems: ["PDF 맨홀번호 판독", "기존 조사자료에 연결된 실제 GIS 맨홀번호와 대조"], pendingNote: "저장 시 기존 조사자료는 유지하고 맨홀 보고서만 교체합니다." }
: VALIDATION_CHECK_GUIDES[fileType];
const countText =
fileType === "EXMN_EXCEL"
? `관로 ${result?.summary.pplCount ?? 0}건 · 맨홀 ${result?.summary.mnhlCount ?? 0}건`
@@ -52,21 +52,22 @@ export default function ExmnSurveyResultSection({
const alert = useAlertStore((state) => state.setMessage);
const exmnBizId = readFeatureProperty(featureData, ["exmnBizId"]);
const jobType = readFeatureProperty(featureData, ["jobType"]);
const pipeNumber = readFeatureProperty(featureData, [
"gisManagementNo",
"fclt_mng_no",
"fcltMngNo",
]);
// 맨홀은 조사 관로번호가 아니라 지도 원천의 실제 시설관리번호로 보고서를 조회한다.
const pipeNumber = readFeatureProperty(featureData, jobType === "MNHL"
? ["fclt_mng_no", "fcltMngNo"]
: ["gisManagementNo", "fclt_mng_no", "fcltMngNo"]);
const reportAvailable = readBooleanFeatureProperty(featureData, [
"reportAvailable",
]);
/** 지도는 상류맨홀 위치를 유지하고 맨홀 보고서는 실제 GIS 맨홀번호로 연결한다. */
const reportLabel = jobType === "MNHL" ? "맨홀" : "관로";
const canOpenPipeReport =
jobType === "PPL" && Boolean(exmnBizId && pipeNumber);
(jobType === "PPL" || jobType === "MNHL") && Boolean(exmnBizId && pipeNumber);
const openPipeReport = async () => {
if (!canOpenPipeReport) return;
if (!reportAvailable) {
alert("등록된 관로 조사보고서가 없습니다.");
alert(`등록된 ${reportLabel} 조사보고서가 없습니다.`);
return;
}
@@ -74,11 +75,12 @@ export default function ExmnSurveyResultSection({
const availability = await getExmnPipeReportAvailability(
exmnBizId,
pipeNumber,
jobType === "MNHL" ? "MNHL" : "PPL",
);
if (!availability.available) {
alert(
availability.message?.trim() ||
`선택한 관로번호의 조사보고서가 등록된 PDF에 없습니다: ${pipeNumber}`,
`선택한 GIS 번호의 조사보고서가 등록된 PDF에 없습니다: ${pipeNumber}`,
);
return;
}
@@ -86,6 +88,7 @@ export default function ExmnSurveyResultSection({
const reportWindow = openWindowPopup("exmnPipeReport", {
exmnBizId,
pipeNumber,
jobType,
reportNo: readFeatureProperty(featureData, ["reportNo"]),
});
if (!reportWindow) {
@@ -97,7 +100,7 @@ export default function ExmnSurveyResultSection({
alert(
error instanceof Error && error.message
? error.message
: "관로 조사보고서 열람 가능 여부를 확인하지 못했습니다.",
: `${reportLabel} 조사보고서 열람 가능 여부를 확인하지 못했습니다.`,
);
}
};
@@ -107,6 +110,7 @@ export default function ExmnSurveyResultSection({
["사업차수", readFeatureProperty(featureData, ["bizCycl"])],
["처리분구", readFeatureProperty(featureData, ["trtdstNm"])],
["조사유형", jobType === "MNHL" ? "맨홀조사" : "관로조사"],
[jobType === "MNHL" ? "GIS 맨홀번호" : "GIS 관로번호", pipeNumber],
["보고서번호", readFeatureProperty(featureData, ["reportNo"])],
["구조적결함 등급", readFeatureProperty(featureData, ["structuralGrade"])],
["운영적결함 등급", readFeatureProperty(featureData, ["operationalGrade"])],
@@ -116,7 +120,7 @@ export default function ExmnSurveyResultSection({
<section className="border-b border-border bg-background">
<div className="card-header flex items-center justify-between">
<h3 className="mx-3 text-2xl font-medium text-label">조사 결과</h3>
{jobType === "PPL" && (
{(jobType === "PPL" || jobType === "MNHL") && (
<Button
onClick={openPipeReport}
disabled={!canOpenPipeReport}
@@ -14,7 +14,7 @@ const getParam = (value: string | string[] | undefined) =>
/** 공통 Blob API 오류 문자열에서 백엔드의 사용자 안내 메시지만 추출한다. */
const getReportErrorMessage = (error: unknown) => {
if (!(error instanceof Error)) {
return "관로 조사보고서를 불러오지 못했습니다.";
return "조사보고서를 불러오지 못했습니다.";
}
const backendMessage = error.message.match(
@@ -35,6 +35,9 @@ export default function ExmnPipeReportPopupContent({
}: WindowPopupContentProps) {
const exmnBizId = getParam(searchParams.exmnBizId);
const pipeNumber = getParam(searchParams.pipeNumber);
/** 조사유형별 첨부 API와 표시 제목을 선택한다. */
const jobType = getParam(searchParams.jobType) === "MNHL" ? "MNHL" : "PPL";
const reportLabel = jobType === "MNHL" ? "맨홀" : "관로";
const reportNo = getParam(searchParams.reportNo);
const [reportUrl, setReportUrl] = useState("");
const [errorMessage, setErrorMessage] = useState("");
@@ -46,7 +49,7 @@ export default function ExmnPipeReportPopupContent({
setReportUrl("");
setErrorMessage("");
void downloadExmnPipeReport(exmnBizId, pipeNumber)
void downloadExmnPipeReport(exmnBizId, pipeNumber, jobType)
.then((blob) => {
if (!active) return;
@@ -59,7 +62,7 @@ export default function ExmnPipeReportPopupContent({
})
.catch((error) => {
if (!active) return;
console.warn("관로 조사보고서 조회 실패:", error);
console.warn("조사보고서 조회 실패:", error);
setErrorMessage(getReportErrorMessage(error));
});
@@ -67,16 +70,16 @@ export default function ExmnPipeReportPopupContent({
active = false;
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [exmnBizId, pipeNumber]);
}, [exmnBizId, pipeNumber, jobType]);
return (
<StandardPopupLayout
title="관로 조사보고서"
title={`${reportLabel} 조사보고서`}
showDefaultCancelAction={false}
actions={
<>
<span className="text-[1.05rem] text-label">
관로번호 {pipeNumber}
{jobType === "MNHL" ? "GIS 번호" : "관로번호"} {pipeNumber}
{reportNo ? ` · 보고서번호 ${reportNo}` : ""}
</span>
<Button type="button" color="white" onClick={() => window.close()}>
@@ -93,7 +96,7 @@ export default function ExmnPipeReportPopupContent({
</div>
) : reportUrl ? (
<iframe
title={`${pipeNumber} 관로 조사보고서`}
title={`${pipeNumber} ${reportLabel} 조사보고서`}
src={reportUrl}
className="h-full w-full border border-border bg-background"
/>