Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 4m17s

This commit is contained in:
2026-08-07 17:03:11 +09:00
24 changed files with 513 additions and 207 deletions
+226 -34
View File
@@ -1,4 +1,5 @@
import { useEffect, useLayoutEffect, useRef, type MutableRefObject } from "react"; import { useEffect, useLayoutEffect, useRef, useState, type MutableRefObject } from "react";
import { createPortal } from "react-dom";
import JSZip from "jszip"; import JSZip from "jszip";
import { ValueType } from "realgrid"; import { ValueType } from "realgrid";
@@ -385,6 +386,20 @@ function toNum(v: unknown): number {
return parseFloat(String(v).replace(/,/g, "")) || NaN; return parseFloat(String(v).replace(/,/g, "")) || NaN;
} }
/** RealGrid 기본 hover가 안 되는 빈 셀 여부 */
function isEmptyGridCellValue(value: unknown): boolean {
if (value == null) return true;
if (typeof value === "number") return Number.isNaN(value);
return String(value).trim() === "";
}
type EmptyCellHoverTooltip = {
message: string;
x: number;
y: number;
key: string;
};
/** onSorting: mergeCells: true인 컬럼이 있으면 그룹 내 순서 유지 정렬, 없으면 RealGrid 기본 정렬 */ /** onSorting: mergeCells: true인 컬럼이 있으면 그룹 내 순서 유지 정렬, 없으면 RealGrid 기본 정렬 */
function createOnSortingHandler( function createOnSortingHandler(
columns: { columns: {
@@ -761,6 +776,7 @@ const DataGrid = ({
}: DataGridProps) => { }: DataGridProps) => {
const { columns, columnLayout } = columnInfo; const { columns, columnLayout } = columnInfo;
const gridRef = useRef<RealGridReact>(null); const gridRef = useRef<RealGridReact>(null);
const gridHostRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const columnsRef = useRef(columns); const columnsRef = useRef(columns);
const sortDirRef = useRef<Record<string, "asc" | "desc" | "none">>({}); const sortDirRef = useRef<Record<string, "asc" | "desc" | "none">>({});
@@ -784,6 +800,10 @@ const DataGrid = ({
useRef<typeof onItemAllChecked>(onItemAllChecked); useRef<typeof onItemAllChecked>(onItemAllChecked);
const isSyncingChecksFromRowDataRef = useRef(false); const isSyncingChecksFromRowDataRef = useRef(false);
const columnsFieldsSigRef = useRef(""); const columnsFieldsSigRef = useRef("");
const emptyCellHoverKeyRef = useRef("");
const emptyCellHoverRafRef = useRef<number | null>(null);
const [emptyCellHoverTooltip, setEmptyCellHoverTooltip] =
useState<EmptyCellHoverTooltip | null>(null);
columnsRef.current = columns; columnsRef.current = columns;
rowDataRef.current = rowData; rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback; cellStyleCallbackRef.current = cellStyleCallback;
@@ -1758,34 +1778,43 @@ useEffect(() => {
const fieldName = const fieldName =
cellIndex?.fieldName ?? cellIndex?.column ?? ""; cellIndex?.fieldName ?? cellIndex?.column ?? "";
// cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex) // cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex)
// 빈 셀은 hover 오버레이로만 표시하므로 클릭 showMessage는 생략 (이중 표출 방지)
if (cellTooltipCallbackRef.current && row && cellIndex) { if (cellTooltipCallbackRef.current && row && cellIndex) {
const msg = cellTooltipCallbackRef.current({ if (!isEmptyGridCellValue(row[fieldName])) {
row: row as Record<string, string | number>, const msg = cellTooltipCallbackRef.current({
fieldName, row: row as Record<string, string | number>,
}); fieldName,
if (msg) { });
const colName = if (msg) {
columns.find( const colName =
(c) => columns.find(
c.fieldName === fieldName || (c) =>
c.name === cellIndex.column, c.fieldName === fieldName ||
)?.name ?? c.name === cellIndex.column,
cellIndex.column ?? )?.name ??
fieldName; cellIndex.column ??
( fieldName;
gridView as { (
showMessage?: ( gridView as {
opts: { showMessage?: (
message: string; opts: {
hideDuration?: number; message: string;
}, hideDuration?: number;
idx: { itemIndex: number; column: string }, },
) => void; idx: {
} itemIndex: number;
).showMessage?.( column: string;
{ message: msg, hideDuration: 5000 }, },
{ itemIndex: cellIndex.itemIndex, column: colName }, ) => void;
); }
).showMessage?.(
{ message: msg, hideDuration: 5000 },
{
itemIndex: cellIndex.itemIndex,
column: colName,
},
);
}
} }
} }
if (onDataCellClickRef.current && row !== undefined) { if (onDataCellClickRef.current && row !== undefined) {
@@ -1807,6 +1836,144 @@ useEffect(() => {
}; };
}, [onRowClick, onDataCellClick, cellTooltipCallback, columns]); }, [onRowClick, onDataCellClick, cellTooltipCallback, columns]);
// 빈 셀은 RealGrid hover 툴팁이 안 뜸 → React 오버레이로만 보완 (값 있는 셀은 기존 onShowTooltip)
useEffect(() => {
if (!cellTooltipCallback) {
emptyCellHoverKeyRef.current = "";
setEmptyCellHoverTooltip(null);
return;
}
const host = gridHostRef.current;
if (!host) return;
const clearEmptyCellHoverTooltip = () => {
if (emptyCellHoverKeyRef.current === "") return;
emptyCellHoverKeyRef.current = "";
setEmptyCellHoverTooltip(null);
};
const handleMouseMove = (ev: MouseEvent) => {
if (emptyCellHoverRafRef.current != null) {
cancelAnimationFrame(emptyCellHoverRafRef.current);
}
emptyCellHoverRafRef.current = requestAnimationFrame(() => {
const gridView = gridRef.current?.gridView as
| {
mouseToIndex?: (
x: number,
y: number,
) => {
cellType?: string;
itemIndex?: number;
column?: string;
fieldName?: string;
} | null;
}
| undefined;
if (!gridView?.mouseToIndex) {
clearEmptyCellHoverTooltip();
return;
}
const rect = host.getBoundingClientRect();
let index: {
cellType?: string;
itemIndex?: number;
column?: string;
fieldName?: string;
} | null = null;
try {
index = gridView.mouseToIndex(
ev.clientX - rect.left,
ev.clientY - rect.top,
);
} catch {
clearEmptyCellHoverTooltip();
return;
}
if (
!index ||
(index.cellType != null && index.cellType !== "data") ||
index.itemIndex == null ||
index.itemIndex < 0
) {
clearEmptyCellHoverTooltip();
return;
}
const row = rowDataRef.current[index.itemIndex];
if (!row) {
clearEmptyCellHoverTooltip();
return;
}
const col = columnsRef.current.find(
(c) =>
c.name === index.column ||
c.fieldName === index.column ||
c.name === index.fieldName ||
c.fieldName === index.fieldName,
);
const fieldName =
col?.fieldName ?? index.fieldName ?? index.column ?? "";
if (!fieldName) {
clearEmptyCellHoverTooltip();
return;
}
// 값이 있으면 RealGrid 기본 hover에 맡김
if (!isEmptyGridCellValue(row[fieldName])) {
clearEmptyCellHoverTooltip();
return;
}
const msg = cellTooltipCallbackRef.current?.({
row: row as Record<string, string | number>,
fieldName,
});
if (!msg) {
clearEmptyCellHoverTooltip();
return;
}
// 값 있는 셀 RealGrid 툴팁과 같이: 본문 다음 줄에 (검증 기준값...)이 오도록
// 템플릿 리터럴 들여쓰기 공백은 제거
const normalizedMessage = msg.replace(/\n[ \t]+/g, "\n");
const key = `${index.itemIndex}:${fieldName}`;
if (emptyCellHoverKeyRef.current === key) return;
emptyCellHoverKeyRef.current = key;
setEmptyCellHoverTooltip({
message: normalizedMessage,
x: ev.clientX + 12,
y: ev.clientY + 16,
key,
});
});
};
const handleMouseLeave = () => {
if (emptyCellHoverRafRef.current != null) {
cancelAnimationFrame(emptyCellHoverRafRef.current);
emptyCellHoverRafRef.current = null;
}
clearEmptyCellHoverTooltip();
};
host.addEventListener("mousemove", handleMouseMove);
host.addEventListener("mouseleave", handleMouseLeave);
return () => {
host.removeEventListener("mousemove", handleMouseMove);
host.removeEventListener("mouseleave", handleMouseLeave);
if (emptyCellHoverRafRef.current != null) {
cancelAnimationFrame(emptyCellHoverRafRef.current);
emptyCellHoverRafRef.current = null;
}
clearEmptyCellHoverTooltip();
};
}, [cellTooltipCallback]);
useEffect(() => { useEffect(() => {
const gridView = gridRef.current?.gridView; const gridView = gridRef.current?.gridView;
if (!gridView || !onScrollToBottom) return; if (!gridView || !onScrollToBottom) return;
@@ -1996,7 +2163,7 @@ useEffect(() => {
)} )}
</div> </div>
</div> </div>
<div style={{ height: `${gridHeight}vh` }}> <div ref={gridHostRef} className="relative" style={{ height: `${gridHeight}vh` }}>
<RealGridReact <RealGridReact
ref={gridRef} ref={gridRef}
autoGenerateField={true} autoGenerateField={true}
@@ -2206,6 +2373,18 @@ useEffect(() => {
> >
{columns.map((column) => { {columns.map((column) => {
const baseStyleName = column.styleName; const baseStyleName = column.styleName;
const numberFormat =
column.dataFormat && column.dataFormat.trim() !== ""
? column.dataFormat
: "#,##0.000";
const resolvedEditor =
column.editor ??
(column.valueType === ValueType.NUMBER
? {
type: "number" as const,
editFormat: numberFormat,
}
: undefined);
return ( return (
<RGDataColumn <RGDataColumn
@@ -2218,17 +2397,15 @@ useEffect(() => {
: ValueType.TEXT : ValueType.TEXT
} }
{...(column.valueType === ValueType.NUMBER && { {...(column.valueType === ValueType.NUMBER && {
numberFormat: column.dataFormat numberFormat,
? column.dataFormat
: "#,##0.000",
})} })}
header={realGridHeaderWithTooltip({ header={realGridHeaderWithTooltip({
...column.header, ...column.header,
tooltip: resolveColumnTooltip(column), tooltip: resolveColumnTooltip(column),
})} })}
editable={column.editable} editable={column.editable}
{...(column.editor != null && { {...(resolvedEditor != null && {
editor: column.editor, editor: resolvedEditor,
})} })}
{...(column.values != null && { {...(column.values != null && {
values: column.values, values: column.values,
@@ -2287,6 +2464,21 @@ useEffect(() => {
); );
})} })}
</RealGridReact> </RealGridReact>
{emptyCellHoverTooltip &&
createPortal(
<div
className="rg-tooltip pointer-events-none !visible fixed z-[9999]"
style={{
left: emptyCellHoverTooltip.x,
top: emptyCellHoverTooltip.y,
whiteSpace: "pre-wrap",
}}
role="tooltip"
>
{emptyCellHoverTooltip.message}
</div>,
document.body,
)}
</div> </div>
</div> </div>
); );
@@ -56,8 +56,8 @@ type ExceptionHistoryTabKey = "NV" | "IMR" | "HB";
const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [ const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [
{ value: "NV", label: "일반검증 내역" }, { value: "NV", label: "일반검증 내역" },
{ value: "IMR", label: "I-MR 검증내역" }, { value: "IMR", label: "시계열 검증내역" },
{ value: "HB", label: "HB 검증내역" }, { value: "HB", label: "모집단 검증내역" },
]; ];
const TITLE = "예외처리 내역 조회"; const TITLE = "예외처리 내역 조회";
@@ -274,7 +274,7 @@ export function ExceptionProcessingHistoryDialog({
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1); setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
setImrPageIndex(pageIndex); setImrPageIndex(pageIndex);
} catch { } catch {
setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다."); setErrorMessage("시계열 검증내역 조회 중 오류가 발생했습니다.");
} finally { } finally {
setImrLoading(false); setImrLoading(false);
} }
@@ -293,7 +293,7 @@ export function ExceptionProcessingHistoryDialog({
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1); setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
setHbPageIndex(pageIndex); setHbPageIndex(pageIndex);
} catch { } catch {
setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다."); setErrorMessage("모집단 검증내역 조회 중 오류가 발생했습니다.");
} finally { } finally {
setHbLoading(false); setHbLoading(false);
} }
@@ -16,7 +16,6 @@ import {
LoadingWithAnimation, LoadingWithAnimation,
} from "@common/components"; } from "@common/components";
import CompactDataGrid from "./CompactDataGrid"; import CompactDataGrid from "./CompactDataGrid";
import SaveButton from "../button/SaveButton";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -182,6 +181,7 @@ const SidoSggFacilityNameSearchForm = ({
Record<string, string | number> | null Record<string, string | number> | null
>(null); >(null);
const didAutoFetchRef = useRef(false); const didAutoFetchRef = useRef(false);
const isSavingConnectionRef = useRef(false);
const sggKey: [string, string] | null = const sggKey: [string, string] | null =
sido && sido !== "ALL" ? ["sgg-all", sido] : null; sido && sido !== "ALL" ? ["sgg-all", sido] : null;
@@ -329,119 +329,213 @@ const SidoSggFacilityNameSearchForm = ({
} }
}; };
const handleSaveAdminRegions = async () => { const canEditFactConnection = async (): Promise<boolean> => {
const statusCodeResponse = await statisticsApi.CheckStatusCode({ const statusCodeResponse = await statisticsApi.CheckStatusCode({
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "", sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
statsCd: 'ST00020002', statsCd: "ST00020002",
statsYr: statsYr?.trim() ?? '' statsYr: statsYr?.trim() ?? "",
}); });
if (statusCodeResponse.status === 'success') { if (statusCodeResponse.status === "success") {
if (!['0', '1', '4'].includes(statusCodeResponse.data)) { if (!["0", "1", "4"].includes(statusCodeResponse.data)) {
const codeName = statusCodeResponse.data === '2' ? '제출' : (statusCodeResponse.data === '3' ? '승인' : '검증중'); const codeName =
alert(`시설별 설치 현황 데이터가 ${codeName} 상태일 때는 처리 시설 정보를 변경할 수 없습니다.`); statusCodeResponse.data === "2"
return; ? "제출"
} : statusCodeResponse.data === "3"
} else { ? "승인"
alert('오류가 발생했습니다.\n관리자에게 문의하세요') : "검증중";
alert(
`시설별 설치 현황 데이터가 ${codeName} 상태일 때는 처리 시설 정보를 변경할 수 없습니다.`,
);
return false;
}
return true;
} }
alert("오류가 발생했습니다.\n관리자에게 문의하세요");
return false;
};
const persistFactConnection = async (
saveList: SeweragePipeFactConnectionSaveItem[],
deleteList: string[],
): Promise<boolean> => {
if (!statsYr?.trim()) { if (!statsYr?.trim()) {
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요."); alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
return; return false;
} }
if (!selectedFacilityRow) { if (!selectedFacilityRow) {
alert("처리시설을 먼저 선택해주세요."); alert("처리시설을 먼저 선택해주세요.");
return; return false;
}
if (saveList.length === 0 && deleteList.length === 0) {
return true;
}
if (!(await canEditFactConnection())) {
return false;
} }
const hasChanged = adminRegionRows.some( try {
(row) => setIsLoading(true);
(row.__checked === "1" && row.isConnection !== "1") || const userInfo = await getUserInfo();
(row.__checked !== "1" && row.isConnection === "1"), const request: SeweragePipeFactConnectionSaveRequest = {
); ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""),
sggNm: String(selectedFacilityRow.sggNm ?? ""),
fcltSn: String(selectedFacilityRow.fcltSn ?? ""),
statsYr: statsYr.trim(),
swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""),
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
userId: userInfo.userUnqId,
saveList,
deleteList,
};
const saveList: SeweragePipeFactConnectionSaveItem[] = adminRegionRows const response =
.filter((row) => row.__checked === "1") await statisticsInputSewPipeApiService.saveFactConnection(
.map((row) => ({ request,
admdstCd: String(row.admdstCd ?? ""),
admdstNm: String(row.emdNm ?? ""),
}))
.filter((row) => row.admdstCd && row.admdstNm);
const deleteList = adminRegionRows
.filter((row) => row.__checked !== "1" && row.isConnection === "1")
.map((row) => String(row.admdstCd ?? ""))
.filter(Boolean);
if (!hasChanged) {
alert("변경된 연결 정보가 없습니다.");
return;
}
const run = async () => {
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const request: SeweragePipeFactConnectionSaveRequest = {
ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""),
sggNm: String(selectedFacilityRow.sggNm ?? ""),
fcltSn: String(selectedFacilityRow.fcltSn ?? ""),
statsYr: statsYr.trim(),
swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""),
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
userId: userInfo.userUnqId,
saveList,
deleteList,
};
const response =
await statisticsInputSewPipeApiService.saveFactConnection(
request,
);
if (response.status !== "success") {
alert(response.message ?? "연결관계 저장에 실패했습니다.");
return;
}
setAdminRegionRows((prev) =>
prev.map((row) => ({
...row,
isConnection: row.__checked === "1" ? "1" : "0",
})),
); );
await onSaveSuccess?.();
alert("저장되었습니다.");
} catch {
alert("연결관계 저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
void run(); if (response.status !== "success") {
alert(response.message ?? "연결관계 저장에 실패했습니다.");
return false;
}
await onSaveSuccess?.();
return true;
} catch {
alert("연결관계 저장 중 오류가 발생했습니다.");
return false;
} finally {
setIsLoading(false);
}
}; };
const handleAdminRegionItemChecked = ( const syncConnectionFlags = (
rows: Record<string, string | number>[],
): Record<string, string | number>[] =>
rows.map((row) => ({
...row,
isConnection: row.__checked === "1" ? "1" : "0",
}));
const revertCheckedToConnection = (
rows: Record<string, string | number>[],
): Record<string, string | number>[] =>
rows.map((row) => ({
...row,
__checked: row.isConnection === "1" ? "1" : "0",
}));
const handleAdminRegionItemChecked = async (
itemIndex: number, itemIndex: number,
checked: boolean, checked: boolean,
) => { ) => {
if (isSavingConnectionRef.current) {
// RealGrid가 이미 토글했을 수 있어 __checked 기준으로 재동기화
setAdminRegionRows((prev) => prev.map((row) => ({ ...row })));
return;
}
const target = adminRegionRows[itemIndex];
if (!target) return;
const nextChecked = checked ? "1" : "0";
const prevChecked = target.isConnection === "1" ? "1" : "0";
setAdminRegionRows((prev) => setAdminRegionRows((prev) =>
prev.map((row, idx) => prev.map((row, idx) =>
idx === itemIndex idx === itemIndex
? { ...row, __checked: checked ? "1" : "0" } ? { ...row, __checked: nextChecked }
: row, : row,
), ),
); );
// 서버 상태와 동일하면 API 호출 없이 UI만 반영
if (nextChecked === prevChecked) {
return;
}
const saveList: SeweragePipeFactConnectionSaveItem[] =
checked
? [
{
admdstCd: String(target.admdstCd ?? ""),
admdstNm: String(target.emdNm ?? ""),
},
].filter((row) => row.admdstCd && row.admdstNm)
: [];
const deleteList =
!checked && String(target.admdstCd ?? "")
? [String(target.admdstCd)]
: [];
if (saveList.length === 0 && deleteList.length === 0) {
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
alert("행정구역 코드가 없어 저장할 수 없습니다.");
return;
}
isSavingConnectionRef.current = true;
const ok = await persistFactConnection(saveList, deleteList);
isSavingConnectionRef.current = false;
if (!ok) {
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
return;
}
setAdminRegionRows((prev) => syncConnectionFlags(prev));
}; };
const handleAdminRegionItemAllChecked = (checked: boolean) => { const handleAdminRegionItemAllChecked = async (checked: boolean) => {
if (isSavingConnectionRef.current) {
setAdminRegionRows((prev) => prev.map((row) => ({ ...row })));
return;
}
const snapshot = adminRegionRows;
if (snapshot.length === 0) return;
setAdminRegionRows((prev) => setAdminRegionRows((prev) =>
prev.map((row) => ({ ...row, __checked: checked ? "1" : "0" })), prev.map((row) => ({
...row,
__checked: checked ? "1" : "0",
})),
); );
const saveList: SeweragePipeFactConnectionSaveItem[] = checked
? snapshot
.filter((row) => row.isConnection !== "1")
.map((row) => ({
admdstCd: String(row.admdstCd ?? ""),
admdstNm: String(row.emdNm ?? ""),
}))
.filter((row) => row.admdstCd && row.admdstNm)
: [];
const deleteList = !checked
? snapshot
.filter((row) => row.isConnection === "1")
.map((row) => String(row.admdstCd ?? ""))
.filter(Boolean)
: [];
if (saveList.length === 0 && deleteList.length === 0) {
setAdminRegionRows((prev) => syncConnectionFlags(prev));
return;
}
isSavingConnectionRef.current = true;
const ok = await persistFactConnection(saveList, deleteList);
isSavingConnectionRef.current = false;
if (!ok) {
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
return;
}
setAdminRegionRows((prev) => syncConnectionFlags(prev));
}; };
useEffect(() => { useEffect(() => {
@@ -575,8 +669,10 @@ const SidoSggFacilityNameSearchForm = ({
조회 조회
</Button> </Button>
</div> </div>
<div className="ml-auto"> <div>
<SaveButton saveFunc={handleSaveAdminRegions} /> <span className="text-lg text-gray-500 float-end">
* 체크 박스를 선택 / 해제 시 반영됩니다.
</span>
</div> </div>
</form> </form>
{isLoading && ( {isLoading && (
@@ -41,8 +41,8 @@ const VERIFICATION_METHOD_CODES: VerificationMethodCd[] = ["NV", "IMR", "HB"];
const VRFC_MTHD_NAME_MAP: Record<VerificationMethodCd, string> = { const VRFC_MTHD_NAME_MAP: Record<VerificationMethodCd, string> = {
NV: "일반", NV: "일반",
IMR: "I-MR", IMR: "시계열",
HB: "HB", HB: "모집단",
}; };
export type VerificationExceptionSubmitItem = { export type VerificationExceptionSubmitItem = {
@@ -40,8 +40,8 @@ type VerificationHistoryTabKey = "NV" | "IMR" | "HB";
const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [ const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
{ value: "NV", label: "일반검증 내역" }, { value: "NV", label: "일반검증 내역" },
{ value: "IMR", label: "I-MR 검증내역" }, { value: "IMR", label: "시계열 검증내역" },
{ value: "HB", label: "HB 검증내역" }, { value: "HB", label: "모집단 검증내역" },
]; ];
const PAGE_SIZE = 30; const PAGE_SIZE = 30;
@@ -40,8 +40,8 @@ type VerificationReportTabKey = "NV" | "IMR" | "HB";
const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [ const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [
{ value: "NV", label: "일반" }, { value: "NV", label: "일반" },
{ value: "IMR", label: "I-MR" }, { value: "IMR", label: "시계열" },
{ value: "HB", label: "HB" }, { value: "HB", label: "모집단" },
]; ];
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = { const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
@@ -290,11 +290,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function createEmptyExcretaTreatmentFacilityRow( function createEmptyExcretaTreatmentFacilityRow(
@@ -786,7 +786,7 @@ const ExcretaTreatmentFacilityInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -941,10 +941,10 @@ const ExcretaTreatmentFacilityInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 -"; : "모집단 검증 -";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -305,11 +305,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function resolvePrivateSewageFacilityTab(tabKey: string) { function resolvePrivateSewageFacilityTab(tabKey: string) {
@@ -871,7 +871,7 @@ const PrivateSewageFacilityInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -1146,10 +1146,10 @@ const PrivateSewageFacilityInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -122,11 +122,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === 'COMPLETED' && Number(res.job_prog) === 100) return; if (res.status === 'COMPLETED' && Number(res.job_prog) === 100) return;
if (res.status === 'FAILED' || res.status === 'ERROR' || res.status === 'CANCELLED') { if (res.status === 'FAILED' || res.status === 'ERROR' || res.status === 'CANCELLED') {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error('IMR 검증 완료 대기 시간이 초과되었습니다.'); throw new Error('시계열 검증 완료 대기 시간이 초과되었습니다.');
} }
type MainFacilityMappingSaveDraft = { type MainFacilityMappingSaveDraft = {
@@ -385,7 +385,7 @@ const PublicSewageFacilityInp = () => {
if (finalStatus === '5') { if (finalStatus === '5') {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel('IMR 검증 진행 중...'); setGlobalProgressLabel('시계열 검증 진행 중...');
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? '', sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? '',
@@ -1416,7 +1416,7 @@ const PublicSewageFacilityInp = () => {
return errors return errors
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" ? "IMR검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "HB검증 - "); e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
const comp = e.compVlCn ?? ""; const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? ""; const now = e.nowVlCn ?? "";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
@@ -285,11 +285,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function createEmptyPumpingStationRow( function createEmptyPumpingStationRow(
@@ -770,7 +770,7 @@ const PumpingStationInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -925,10 +925,10 @@ const PumpingStationInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 -"; : "모집단 검증 -";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -521,10 +521,10 @@ function verificationCellTooltip({
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -577,11 +577,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 12. 하수 및 분뇨관련산업 // 하수도 통계 관리 > 12. 하수 및 분뇨관련산업
@@ -1431,7 +1431,7 @@ const SewageExcretaRelatedInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -136,10 +136,10 @@ function verificationCellTooltip({
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -630,11 +630,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 11. 하수 및 분뇨찌꺼기(슬러지)처리 // 하수도 통계 관리 > 11. 하수 및 분뇨찌꺼기(슬러지)처리
@@ -1437,7 +1437,7 @@ const SewageExcretaSlurgeInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -1898,7 +1898,7 @@ const SewageExcretaSlurgeInp = () => {
col.fieldName === "sggNm" col.fieldName === "sggNm"
) { ) {
next = buildSggNmColumn(col, canEdit, sggOptions); next = buildSggNmColumn(col, canEdit, sggOptions);
} else if (col.fieldName === "sggNm" || col.fieldName === "swtrmtFcltNm") { } else if (col.fieldName === "sggNm") {
next = { ...col, editable: false as const, styleName: "rg-cell-readonly" }; next = { ...col, editable: false as const, styleName: "rg-cell-readonly" };
} }
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
@@ -324,11 +324,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function buildRegionalFactSubmitListFromGrid( function buildRegionalFactSubmitListFromGrid(
@@ -1428,7 +1428,7 @@ const SewagePipeInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: values.bsnmCd ?? "", sewer_bzmn_cd: values.bsnmCd ?? "",
@@ -2244,7 +2244,7 @@ const SewagePipeInp = () => {
return errors return errors
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" ? "IMR검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "HB검증 - "); e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
const comp = e.compVlCn ?? ""; const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? ""; const now = e.nowVlCn ?? "";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
@@ -183,11 +183,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function buildColumnFormatNumber( function buildColumnFormatNumber(
@@ -655,7 +655,7 @@ const SewagePipeMaintainInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -1099,7 +1099,7 @@ const SewagePipeMaintainInp = () => {
return errors return errors
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" ? "IMR검증 - " : e.vrfcCd === "NV" ? "일반검증 - " : "HB검증 - "; e.vrfcCd === "IMR" ? "시계열 검증 - " : e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -446,11 +446,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장 // 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장
@@ -852,7 +852,7 @@ const SewageReservoirPumpInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -1500,10 +1500,10 @@ const SewageReservoirPumpInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 -"; : "모집단 검증 -";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -341,11 +341,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 09. 하수도재정 // 하수도 통계 관리 > 09. 하수도재정
@@ -918,7 +918,7 @@ const SewerageFinanceInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -1212,10 +1212,10 @@ const SewerageFinanceInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -87,11 +87,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function getSewerageSupplyRateParams( function getSewerageSupplyRateParams(
@@ -507,7 +507,7 @@ const SeweragePenetrationRateInp = () => {
setGlobalProgress(50); setGlobalProgress(50);
if (response.data.finalStatus === '5') { if (response.data.finalStatus === '5') {
setGlobalProgressLabel('IMR 검증 진행 중...'); setGlobalProgressLabel('시계열 검증 진행 중...');
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: searchValues?.bsnmCd ?? '', sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
@@ -833,7 +833,7 @@ const SeweragePenetrationRateInp = () => {
if (Array.isArray(errors) && errors.length > 0) { if (Array.isArray(errors) && errors.length > 0) {
return errors return errors
.map((e) => { .map((e) => {
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : (e.vrfcCd === 'NV' ? '일반검증 - ' : 'HB검증 -'); const prefix = e.vrfcCd === 'IMR' ? '시계열 검증 - ' : (e.vrfcCd === 'NV' ? '일반검증 - ' : '모집단 검증 -');
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -271,11 +271,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 13. 하수도 서비스 수준 // 하수도 통계 관리 > 13. 하수도 서비스 수준
@@ -791,7 +791,7 @@ const SewerageServiceLevelInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -1110,10 +1110,10 @@ const SewerageServiceLevelInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -508,11 +508,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 10. 물 재이용 // 하수도 통계 관리 > 10. 물 재이용
@@ -1318,7 +1318,7 @@ const WaterReuseInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
@@ -2053,10 +2053,10 @@ const WaterReuseInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -115,6 +115,14 @@ const StatisticsInputManagement = () => {
setRowData([]); setRowData([]);
}; };
useEffect(() => {
const defaultYear = String(new Date().getFullYear() - 1);
void handleSearch({
year: defaultYear,
clsfCd: "ALL",
});
}, []);
const handleRegisterClick = () => { const handleRegisterClick = () => {
const yr = const yr =
searchFormValues?.year ?? lastSearchRef.current?.year ?? ""; searchFormValues?.year ?? lastSearchRef.current?.year ?? "";
@@ -48,15 +48,21 @@ const GeneralVerificationManagement = () => {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const handleSearch = async () => { const handleSearch = async (
values: {
statsCd: string;
useYn: string;
vrfcClsfCd: string;
} = searchValues,
) => {
try { try {
setIsLoading(true); setIsLoading(true);
const params = Object.fromEntries( const params = Object.fromEntries(
Object.entries({ Object.entries({
trgtStatsCd: searchValues.statsCd, trgtStatsCd: values.statsCd,
vrfcClsfCd: searchValues.vrfcClsfCd, vrfcClsfCd: values.vrfcClsfCd,
useYn: searchValues.useYn, useYn: values.useYn,
}).filter(([, value]) => value !== "" && value !== "ALL"), }).filter(([, value]) => value !== "" && value !== "ALL"),
); );
@@ -83,7 +89,7 @@ const GeneralVerificationManagement = () => {
}; };
const columns = useMemo( const columns = useMemo(
() => getGeneralVerificationColumns(handleSearch), () => getGeneralVerificationColumns(() => handleSearch()),
[searchValues.statsCd, searchValues.vrfcClsfCd, searchValues.useYn], [searchValues.statsCd, searchValues.vrfcClsfCd, searchValues.useYn],
); );
@@ -103,14 +109,16 @@ const GeneralVerificationManagement = () => {
}; };
const handleReset = () => { const handleReset = () => {
const resetValues = { statsCd: "", useYn: "", vrfcClsfCd: "" };
setSearchValues(resetValues);
setRowSelection({}); setRowSelection({});
setSelectedVrfcRow(null); setSelectedVrfcRow(null);
handleSearch();
setShowRegistForm(false); setShowRegistForm(false);
void handleSearch(resetValues);
}; };
useEffect(() => { useEffect(() => {
handleSearch(); void handleSearch();
}, []); }, []);
return ( return (
@@ -182,7 +190,7 @@ const GeneralVerificationManagement = () => {
<Button <Button
color="green" color="green"
className="h-[3.2rem]" className="h-[3.2rem]"
onClick={handleSearch} onClick={() => void handleSearch()}
> >
조회 조회
</Button> </Button>
@@ -205,7 +213,7 @@ const GeneralVerificationManagement = () => {
<RegistDialog <RegistDialog
open={showRegistForm} open={showRegistForm}
onOpenChange={setShowRegistForm} onOpenChange={setShowRegistForm}
onSuccess={handleSearch} onSuccess={() => void handleSearch()}
> >
<Button>등록</Button> <Button>등록</Button>
</RegistDialog> </RegistDialog>
+3 -1
View File
@@ -233,13 +233,15 @@ export type ColumnConfig = {
datetimeFormat?: string; datetimeFormat?: string;
/** RealGrid 셀 편집기 */ /** RealGrid 셀 편집기 */
editor?: { editor?: {
type?: "dropdown" | "list" | "date" | "text" | "line"; type?: "dropdown" | "list" | "date" | "text" | "line" | "number";
domainOnly?: boolean; domainOnly?: boolean;
textReadOnly?: boolean; textReadOnly?: boolean;
commitOnSelect?: boolean; commitOnSelect?: boolean;
dropDownWhenClick?: boolean; dropDownWhenClick?: boolean;
showToday?: boolean; showToday?: boolean;
datetimeFormat?: string; datetimeFormat?: string;
/** NUMBER 편집 시 천단위 구분·소수점 형식 (표시용 numberFormat과 동일하게 맞추는 것을 권장) */
editFormat?: string;
values?: string[]; values?: string[];
labels?: string[]; labels?: string[];
/** RealGrid 입력 마스크 (예: 위탁계약기간 `yyyymmdd~yyyymmdd`) */ /** RealGrid 입력 마스크 (예: 위탁계약기간 `yyyymmdd~yyyymmdd`) */
@@ -34,9 +34,9 @@ export const PublicSewageFactColumn = {
}, },
// (m³/일) // (m³/일)
{ name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', editable: false, styleName: 'rg-cell-readonly'},
{ name: 'prcsFcltBoilCpct', fieldName: 'prcsFcltBoilCpct', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'prcsFcltBoilCpct', fieldName: 'prcsFcltBoilCpct', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', editable: false, styleName: 'rg-cell-readonly'},
{ name: 'prcsFcltAltdCpct', fieldName: 'prcsFcltAltdCpct', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'prcsFcltAltdCpct', fieldName: 'prcsFcltAltdCpct', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', editable: false, styleName: 'rg-cell-readonly'},
// 유입하수량 // 유입하수량
{ name: 'inflwSwageQnt', fieldName: 'inflwSwageQnt', header: { text: '유입하수량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, { name: 'inflwSwageQnt', fieldName: 'inflwSwageQnt', header: { text: '유입하수량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
@@ -199,10 +199,10 @@ export const RegionalFactStatusInpColumn = {
editable: false, editable: false,
styleName: 'rg-cell-total', styleName: 'rg-cell-total',
valueType: ValueType.NUMBER, valueType: ValueType.NUMBER,
dataFormat: '#,##0.0', dataFormat: '#,##0.00',
computed: { computed: {
numeratorFields: ['sewerLdgrEdpPrlg'], numeratorFields: ['sewerLdgrEdpPrlg'],
denominatorFields: ['cbsysPplMtncPlanPrlg', 'drtwPplMtncPlanPrlg', 'exlnPplMtncPlanPrlg'], denominatorFields: ['cbsysSqrePrlg', 'cbsysCiclPrlg', 'cbsysOpnPrlg', 'cbsysGttrPrlg', 'drtwPplSqrePrlg', 'drtwPplCiclPrlg', 'exlnPplSqrePrlg', 'exlnPplCiclPrlg', 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'],
type: 'pblswrgPopltnSuprtRate' as const, type: 'pblswrgPopltnSuprtRate' as const,
}, },
}, },