From b508568e86852a1cc92b00383d172b80c19bd2d1 Mon Sep 17 00:00:00 2001
From: rsone001
Date: Mon, 18 May 2026 11:03:43 +0900
Subject: [PATCH] =?UTF-8?q?[st]=20-=20fix:=20=EC=98=A4=EB=A5=98=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=88=84=EB=9D=BD=20=EA=B8=B0?=
=?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/st/src/components/css/DataGrid.css | 22 +
apps/st/src/components/module/DataGrid.tsx | 78 ++-
apps/st/src/components/module/TabDataGrid.tsx | 15 +-
.../module/VerificationResultDialog.tsx | 435 ++++++--------
.../VerificationResultHistoryDialog.tsx | 559 +++++++++++++-----
.../post-submit-verification-results.ts | 85 +++
.../VerificationSelectionReport.tsx | 196 +++---
.../input/PublicSewageFacilityInp.tsx | 39 +-
.../statistics/input/SewagePipeInp.tsx | 47 +-
.../input/SeweragePenetrationRateInp.tsx | 55 +-
apps/st/src/components/type/DataGrid.type.ts | 2 +
.../sewagePipe/ManholeStatusInpColumn.ts | 10 +-
.../sewagePipe/PipeTypeStatusInpColumn.ts | 3 +-
.../util/column/total/SewagePipeTotColumn.ts | 1 +
.../total/SeweragePentrationRateTotColumn.ts | 1 +
.../st/src/components/util/gridExcelExport.ts | 45 ++
.../statistics/api/statistics-service.ts | 12 +-
...-public-sewage-facility-input-interface.ts | 2 +-
.../statistics-sewage-pipe-input-interface.ts | 10 +-
.../statistics/model/statistics-interface.ts | 14 +
.../shared/lib/stats-cd-input-navigation.ts | 8 +
21 files changed, 1121 insertions(+), 518 deletions(-)
create mode 100644 apps/st/src/components/module/post-submit-verification-results.ts
create mode 100644 apps/st/src/components/util/gridExcelExport.ts
diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css
index a79e3ebd4..76bebc387 100644
--- a/apps/st/src/components/css/DataGrid.css
+++ b/apps/st/src/components/css/DataGrid.css
@@ -21,6 +21,16 @@
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok {
@apply !font-semibold !text-[#3372cf];
}
+ /* 검증결과 다이얼로그: 구분(NV·IMR·HB) */
+ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-nv {
+ @apply !bg-[#f3f4f6] !font-semibold !text-[#4b5563];
+ }
+ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-imr {
+ @apply !bg-[#fef2f2] !font-semibold !text-[#dc2626];
+ }
+ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-hb {
+ @apply !bg-[#eff6ff] !font-semibold !text-[#2563eb];
+ }
/* 개별 반려: 높이/레이아웃은 RealGrid 행에 그대로 두고, 색·테두리·정렬만 버튼 느낌 */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn {
text-align: center !important;
@@ -252,6 +262,18 @@
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok {
color: #3372cf !important;
}
+[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-nv {
+ background-color: #4b5563 !important;
+ color: #f9fafb !important;
+}
+[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-imr {
+ background-color: rgb(127 29 29 / 0.55) !important;
+ color: #fecaca !important;
+}
+[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-hb {
+ background-color: rgb(30 58 138 / 0.55) !important;
+ color: #bfdbfe !important;
+}
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn {
background-color: rgb(127 29 29 / 0.42) !important;
border-color: rgb(248 113 113 / 0.5) !important;
diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx
index 0ae40300a..f32d3d72d 100644
--- a/apps/st/src/components/module/DataGrid.tsx
+++ b/apps/st/src/components/module/DataGrid.tsx
@@ -20,6 +20,11 @@ import type {
} from "../type/DataGrid.type";
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
+import {
+ prepareGridColumnsForExcelExport,
+ restoreGridColumnsAfterExcelExport,
+ shouldPrepareColumnsForExcelExport,
+} from "@/components/util/gridExcelExport";
import { useTabStore } from "@/store/tab-store";
// RealGrid 타입 확장
@@ -47,6 +52,23 @@ if (typeof window !== "undefined") {
RealGrid.setLicenseKey(process.env.NEXT_PUBLIC_ST_REALGRID_KEY || "");
+type GridViewWithCheckApi = {
+ checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
+ checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
+ checkAll?: (checked: boolean) => void;
+};
+
+function isRowCheckedFlag(value: unknown): boolean {
+ return (
+ value === true ||
+ value === 1 ||
+ value === "1" ||
+ value === "Y" ||
+ value === "y" ||
+ value === "true"
+ );
+}
+
/** setDataComparer(field, comparer) - setFields 없이 숫자 정렬용. comparer(field, row1, row2) => -1|0|1 */
interface RealGridDataProviderComparer {
setDataComparer?(
@@ -411,6 +433,7 @@ const DataGrid = ({
const onItemCheckedRef = useRef(onItemChecked);
const onItemAllCheckedRef =
useRef(onItemAllChecked);
+ const isSyncingChecksFromRowDataRef = useRef(false);
columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
@@ -547,6 +570,12 @@ const DataGrid = ({
gridView.setColumnLayout?.(normalizedLayout);
}
+ const useExcelColumnVisibilityPrep =
+ shouldPrepareColumnsForExcelExport(columns);
+ const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep
+ ? prepareGridColumnsForExcelExport(gridView, columns)
+ : undefined;
+
const exportBaseName = excelExportFileSuffix
? `${menuName}_${excelExportFileSuffix}`
: menuName;
@@ -556,8 +585,14 @@ const DataGrid = ({
target: "local",
fileName: `${exportBaseName}.xlsx`,
footer: "hidden",
- allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
+ allColumns: !useExcelColumnVisibilityPrep,
done: () => {
+ if (excelExportVisibilitySnapshot) {
+ restoreGridColumnsAfterExcelExport(
+ gridView,
+ excelExportVisibilitySnapshot,
+ );
+ }
// 헤더 원복
columns.forEach((col) => {
if (col.fieldName && gridView.columnByName) {
@@ -1278,6 +1313,7 @@ const DataGrid = ({
if (onItemChecked) {
gridView.onItemChecked = (_grid, itemIndex, checked) => {
+ if (isSyncingChecksFromRowDataRef.current) return;
onItemCheckedRef.current?.(itemIndex, checked);
};
} else {
@@ -1286,6 +1322,7 @@ const DataGrid = ({
if (onItemAllChecked) {
gridView.onItemAllChecked = (_grid, checked) => {
+ if (isSyncingChecksFromRowDataRef.current) return;
onItemAllCheckedRef.current?.(checked);
};
} else {
@@ -1299,6 +1336,45 @@ const DataGrid = ({
};
}, [onItemChecked, onItemAllChecked]);
+ useEffect(() => {
+ if (!gridRef.current?.gridView || !showHeaderCheckbox) return;
+
+ const gridView = gridRef.current.gridView as GridViewWithCheckApi;
+ const dataProvider = gridRef.current.dataProvider as
+ | { setRows?: (rows: Record[]) => void }
+ | undefined;
+
+ if (dataProvider?.setRows) {
+ dataProvider.setRows(rowData);
+ }
+
+ const hasCheckedColumn = rowData.some((row) =>
+ Object.prototype.hasOwnProperty.call(row, "__checked"),
+ );
+ if (!hasCheckedColumn) return;
+
+ const timer = window.setTimeout(() => {
+ isSyncingChecksFromRowDataRef.current = true;
+ try {
+ gridView.checkAll?.(false);
+ rowData.forEach((row, itemIndex) => {
+ if (!isRowCheckedFlag((row as Record).__checked)) return;
+ if (gridView.checkItem) {
+ gridView.checkItem(itemIndex, true, false);
+ return;
+ }
+ gridView.checkRow?.(itemIndex, true, false);
+ });
+ } catch (e) {
+ console.warn("check sync:", e);
+ } finally {
+ isSyncingChecksFromRowDataRef.current = false;
+ }
+ }, 0);
+
+ return () => window.clearTimeout(timer);
+ }, [rowData, showHeaderCheckbox]);
+
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView) return;
diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx
index a2327c11b..e4c23eb83 100644
--- a/apps/st/src/components/module/TabDataGrid.tsx
+++ b/apps/st/src/components/module/TabDataGrid.tsx
@@ -16,6 +16,11 @@ import GridTopActionButton from "./GridTopActionButton";
import SplitDataGrid from "./SplitDataGrid";
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
+import {
+ prepareGridColumnsForExcelExport,
+ restoreGridColumnsAfterExcelExport,
+ shouldPrepareColumnsForExcelExport,
+} from "@/components/util/gridExcelExport"
import { useTabStore } from "@/store/tab-store"
// RealGrid 타입 확장
@@ -432,13 +437,21 @@ const TabDataGrid = ({
gridView.setColumnLayout?.(normalizedLayout);
}
+ const useExcelColumnVisibilityPrep = shouldPrepareColumnsForExcelExport(columns);
+ const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep
+ ? prepareGridColumnsForExcelExport(gridView, columns)
+ : undefined;
+
gridView.exportGrid({
type: 'excel',
target: 'local',
fileName: `${menuName}_${tabLabel}.xlsx`,
footer: 'hidden',
- allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
+ allColumns: !useExcelColumnVisibilityPrep,
done: () => {
+ if (excelExportVisibilitySnapshot) {
+ restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot);
+ }
// 헤더 원복
columns.forEach((col) => {
if (col.fieldName && gridView.columnByName) {
diff --git a/apps/st/src/components/module/VerificationResultDialog.tsx b/apps/st/src/components/module/VerificationResultDialog.tsx
index acd460f87..7311e184d 100644
--- a/apps/st/src/components/module/VerificationResultDialog.tsx
+++ b/apps/st/src/components/module/VerificationResultDialog.tsx
@@ -23,7 +23,11 @@ import {
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
-import type { CellEditedCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
+import type {
+ CellEditedCallbackParams,
+ CellStyleCallbackParams,
+ ColumnInfo,
+} from "@/components/type/DataGrid.type";
import { getUserInfo } from "../../../../../packages/common/components/src/api";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
@@ -31,6 +35,16 @@ import { useAlertStore } from "@/shared/lib";
type VerificationResultGridRow = Record;
+type VerificationMethodCd = "NV" | "IMR" | "HB";
+
+const VERIFICATION_METHOD_CODES: VerificationMethodCd[] = ["NV", "IMR", "HB"];
+
+const VRFC_MTHD_NAME_MAP: Record = {
+ NV: "일반",
+ IMR: "I-MR",
+ HB: "HB",
+};
+
export type VerificationExceptionSubmitItem = {
vrfcSn: number;
prcsOpnnExpln: string;
@@ -60,9 +74,23 @@ const PRCS_YN_LABEL: Record = {
"3": "반려",
};
-/** History와 동일 + 편집 가능한 처리의견(예외사유, API 필드명 `prcsOpnnExpln`과 동일) */
+function verificationMthdCellStyle({
+ fieldName,
+ row,
+}: CellStyleCallbackParams): string | undefined {
+ if (fieldName !== "vrfcMthdNm") return undefined;
+ const code = String(row.vrfcMthdCd ?? "");
+ if (code === "IMR") return "rg-cell-vrf-mthd-imr";
+ if (code === "HB") return "rg-cell-vrf-mthd-hb";
+ if (code === "NV") return "rg-cell-vrf-mthd-nv";
+ return undefined;
+}
+
+/** History와 동일 + 구분(검증유형) + 편집 가능한 처리의견(예외사유) */
const verificationSubmitResultColumnInfo: ColumnInfo = {
columns: [
+ { name: "vrfcMthdNm", fieldName: "vrfcMthdNm", header: { text: "구분" }, editable: false },
+ { name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
@@ -81,6 +109,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
},
],
columnLayout: [
+ "vrfcMthdNm",
"ctpvNm",
"sggNm",
"vrfcTrgtNm",
@@ -121,7 +150,6 @@ export function VerificationResultDialog({
emptyMessage = "표시할 검증 결과가 없습니다.",
onExceptionSubmitted,
}: VerificationResultDialogProps) {
- /** 부모가 매 렌더마다 새 객체를 넘기면 queryContext 참조만 바뀌어 loadInitial이 재실행되므로 필드 값 기준으로 안정화 */
const stableQueryContext = useMemo((): VerificationResultDialogQueryContext | null => {
if (!queryContext) return null;
return queryContext;
@@ -133,47 +161,24 @@ export function VerificationResultDialog({
queryContext?.statsCd,
]);
- const [normalPageIndex, setNormalPageIndex] = useState(0);
- const [imrPageIndex, setImrPageIndex] = useState(0);
- const [normalPageCount, setNormalPageCount] = useState(1);
- const [imrPageCount, setImrPageCount] = useState(1);
- const [normalLoading, setNormalLoading] = useState(false);
- const [imrLoading, setImrLoading] = useState(false);
+ const [pageIndex, setPageIndex] = useState(0);
+ const [pageCount, setPageCount] = useState(1);
+ const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
- const [normalRows, setNormalRows] = useState([]);
- const [imrRows, setImrRows] = useState([]);
- const [normalCheckedIndices, setNormalCheckedIndices] = useState>(
- () => new Set(),
- );
- const [imrCheckedIndices, setImrCheckedIndices] = useState>(() => new Set());
+ const [rows, setRows] = useState([]);
+ const [checkedIndices, setCheckedIndices] = useState>(() => new Set());
- const normalGridRef = useRef(null);
- const imrGridRef = useRef(null);
- /** 메인 입력 화면처럼 편집 값은 그리드에 두고, 제출 시에만 스냅샷 조회 → Enter 시 rows 재주입 충돌 방지 */
- const getNormalGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null);
- const getImrGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null);
+ const gridRef = useRef(null);
+ const getGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null);
const alert = useAlertStore((state) => state.setMessage);
- const onNormalPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => {
+ const onPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => {
if (params.fieldName !== "prcsOpnnExpln") return;
const text = String(params.newValue ?? "");
const hasValue = text.trim().length > 0;
- setRowCheckByItemIndex(normalGridRef, params.itemIndex, hasValue);
- setNormalCheckedIndices((prev) => {
- const next = new Set(prev);
- if (hasValue) next.add(params.itemIndex);
- else next.delete(params.itemIndex);
- return next;
- });
- }, []);
-
- const onImrPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => {
- if (params.fieldName !== "prcsOpnnExpln") return;
- const text = String(params.newValue ?? "");
- const hasValue = text.trim().length > 0;
- setRowCheckByItemIndex(imrGridRef, params.itemIndex, hasValue);
- setImrCheckedIndices((prev) => {
+ setRowCheckByItemIndex(gridRef, params.itemIndex, hasValue);
+ setCheckedIndices((prev) => {
const next = new Set(prev);
if (hasValue) next.add(params.itemIndex);
else next.delete(params.itemIndex);
@@ -182,38 +187,38 @@ export function VerificationResultDialog({
}, []);
const resetState = useCallback(() => {
- setNormalPageIndex(0);
- setImrPageIndex(0);
- setNormalPageCount(1);
- setImrPageCount(1);
- setNormalLoading(false);
- setImrLoading(false);
+ setPageIndex(0);
+ setPageCount(1);
+ setLoading(false);
setErrorMessage(null);
- setNormalRows([]);
- setImrRows([]);
- setNormalCheckedIndices(new Set());
- setImrCheckedIndices(new Set());
+ setRows([]);
+ setCheckedIndices(new Set());
}, []);
- const mapItemToRow = useCallback((item: VerificationResultReportItem): VerificationResultGridRow => {
- return {
- vrfcSn: item.vrfcSn,
- ctpvNm: item.ctpvNm,
- sggNm: item.sggNm,
- vrfcTrgtNm: item.vrfcTrgtNm,
- rsnExpln: item.rsnExpln,
- nowVlCn: item.nowVlCn ?? "-",
- compVlCn: item.compVlCn ?? "-",
- prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
- pbcpAprvYn: item.pbcpAprvYn,
- regDt: (item.regDt ?? "").slice(0, 16),
- mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
- prcsOpnnExpln: item.prcsOpnnExpln ?? "",
- };
- }, []);
+ const mapItemToRow = useCallback(
+ (item: VerificationResultReportItem, vrfcMthdCd: VerificationMethodCd): VerificationResultGridRow => {
+ return {
+ vrfcSn: item.vrfcSn,
+ vrfcMthdCd,
+ vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
+ ctpvNm: item.ctpvNm,
+ sggNm: item.sggNm,
+ vrfcTrgtNm: item.vrfcTrgtNm,
+ rsnExpln: item.rsnExpln,
+ nowVlCn: item.nowVlCn ?? "-",
+ compVlCn: item.compVlCn ?? "-",
+ prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
+ pbcpAprvYn: item.pbcpAprvYn,
+ regDt: (item.regDt ?? "").slice(0, 16),
+ mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
+ prcsOpnnExpln: item.prcsOpnnExpln ?? "",
+ };
+ },
+ [],
+ );
const fetchVerificationResultPage = useCallback(
- async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => {
+ async (vrfcMthdCd: VerificationMethodCd, page: number) => {
if (!stableQueryContext) return null;
const param: VerificationResultListRequest = {
sewerBzmnCd: stableQueryContext.sewerBzmnCd,
@@ -224,7 +229,7 @@ export function VerificationResultDialog({
pbcpAprvYn: "N",
vrfcMthdCd,
prcsYnList: "0",
- page: pageIndex + 1,
+ page: page + 1,
size: 10000,
};
return statisticsApi.getVerificationResultList(param);
@@ -232,81 +237,56 @@ export function VerificationResultDialog({
[stableQueryContext],
);
- const loadInitial = useCallback(async () => {
- if (!stableQueryContext) {
- setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
- return;
- }
- setErrorMessage(null);
- setNormalLoading(true);
- setImrLoading(true);
- setNormalPageIndex(0);
- setImrPageIndex(0);
- try {
- const [nvRes, imrRes] = await Promise.all([
- fetchVerificationResultPage("NV", 0),
- fetchVerificationResultPage("IMR", 0),
- ]);
- const nvList = nvRes?.data?.verifyResultList ?? [];
- const imrList = imrRes?.data?.verifyResultList ?? [];
- setNormalRows(nvList.map(mapItemToRow));
- setImrRows(imrList.map(mapItemToRow));
- setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
- setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
- } catch {
- setNormalRows([]);
- setImrRows([]);
- setNormalPageCount(1);
- setImrPageCount(1);
- setErrorMessage("검증 결과 조회 중 오류가 발생했습니다.");
- } finally {
- setNormalLoading(false);
- setImrLoading(false);
- setNormalCheckedIndices(new Set());
- setImrCheckedIndices(new Set());
- }
- }, [fetchVerificationResultPage, mapItemToRow, stableQueryContext]);
-
- const handleNormalPageChange = useCallback(
- async (pageIndex: number) => {
- if (pageIndex === normalPageIndex || !stableQueryContext) return;
+ const loadPage = useCallback(
+ async (nextPageIndex: number) => {
+ if (!stableQueryContext) {
+ setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
+ return;
+ }
setErrorMessage(null);
- setNormalLoading(true);
+ setLoading(true);
try {
- const response = await fetchVerificationResultPage("NV", pageIndex);
- const list = response?.data?.verifyResultList ?? [];
- setNormalRows(list.map(mapItemToRow));
- setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1);
- setNormalPageIndex(pageIndex);
+ const responses = await Promise.all(
+ VERIFICATION_METHOD_CODES.map((code) =>
+ fetchVerificationResultPage(code, nextPageIndex),
+ ),
+ );
+ const merged: VerificationResultGridRow[] = [];
+ let maxPageCount = 1;
+ VERIFICATION_METHOD_CODES.forEach((code, index) => {
+ const response = responses[index];
+ const list = response?.data?.verifyResultList ?? [];
+ merged.push(...list.map((item) => mapItemToRow(item, code)));
+ maxPageCount = Math.max(
+ maxPageCount,
+ response?.data?.pageInfo?.totalPages ?? 1,
+ );
+ });
+ setRows(merged);
+ setPageCount(maxPageCount);
+ setPageIndex(nextPageIndex);
} catch {
- setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다.");
+ setRows([]);
+ setPageCount(1);
+ setErrorMessage("검증 결과 조회 중 오류가 발생했습니다.");
} finally {
- setNormalLoading(false);
- setNormalCheckedIndices(new Set());
+ setLoading(false);
+ setCheckedIndices(new Set());
}
},
- [fetchVerificationResultPage, mapItemToRow, normalPageIndex, stableQueryContext],
+ [fetchVerificationResultPage, mapItemToRow, stableQueryContext],
);
- const handleImrPageChange = useCallback(
- async (pageIndex: number) => {
- if (pageIndex === imrPageIndex || !stableQueryContext) return;
- setErrorMessage(null);
- setImrLoading(true);
- try {
- const response = await fetchVerificationResultPage("IMR", pageIndex);
- const list = response?.data?.verifyResultList ?? [];
- setImrRows(list.map(mapItemToRow));
- setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
- setImrPageIndex(pageIndex);
- } catch {
- setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다.");
- } finally {
- setImrLoading(false);
- setImrCheckedIndices(new Set());
- }
+ const loadInitial = useCallback(async () => {
+ await loadPage(0);
+ }, [loadPage]);
+
+ const handlePageChange = useCallback(
+ async (nextPageIndex: number) => {
+ if (nextPageIndex === pageIndex || !stableQueryContext) return;
+ await loadPage(nextPageIndex);
},
- [fetchVerificationResultPage, mapItemToRow, imrPageIndex, stableQueryContext],
+ [loadPage, pageIndex, stableQueryContext],
);
useEffect(() => {
@@ -317,58 +297,32 @@ export function VerificationResultDialog({
void loadInitial();
}, [loadInitial, open, resetState]);
- /** 조회된 행에 예외사유 텍스트가 있으면 체크바·체크 인덱스와 맞춤 */
useEffect(() => {
- if (normalLoading) return;
+ if (loading) return;
const id = requestAnimationFrame(() => {
- setNormalCheckedIndices((prev) => {
+ setCheckedIndices((prev) => {
const next = new Set(prev);
- normalRows.forEach((row, itemIndex) => {
+ rows.forEach((row, itemIndex) => {
const has = String(row.prcsOpnnExpln ?? "").trim().length > 0;
- setRowCheckByItemIndex(normalGridRef, itemIndex, has);
+ setRowCheckByItemIndex(gridRef, itemIndex, has);
if (has) next.add(itemIndex);
});
return next;
});
});
return () => cancelAnimationFrame(id);
- }, [normalRows, normalLoading]);
-
- useEffect(() => {
- if (imrLoading) return;
- const id = requestAnimationFrame(() => {
- setImrCheckedIndices((prev) => {
- const next = new Set(prev);
- imrRows.forEach((row, itemIndex) => {
- const has = String(row.prcsOpnnExpln ?? "").trim().length > 0;
- setRowCheckByItemIndex(imrGridRef, itemIndex, has);
- if (has) next.add(itemIndex);
- });
- return next;
- });
- });
- return () => cancelAnimationFrame(id);
- }, [imrRows, imrLoading]);
+ }, [rows, loading]);
const handleExceptionSubmitClick = useCallback(async () => {
- const nvGrid = getNormalGridDataRef.current?.() ?? [];
- const imGrid = getImrGridDataRef.current?.() ?? [];
+ const gridRows = getGridDataRef.current?.() ?? [];
const payload: VerificationExceptionSubmitItem[] = [];
- const pushChecked = (
- indices: Set,
- gridRows: VerificationResultGridRow[],
- idRows: VerificationResultGridRow[],
- ) => {
- indices.forEach((idx) => {
- const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim();
- if (!prcsOpnnExpln) return;
- const vrfcSn = Number(idRows[idx]?.vrfcSn);
- if (!Number.isFinite(vrfcSn)) return;
- payload.push({ vrfcSn, prcsOpnnExpln });
- });
- };
- pushChecked(normalCheckedIndices, nvGrid, normalRows);
- pushChecked(imrCheckedIndices, imGrid, imrRows);
+ checkedIndices.forEach((idx) => {
+ const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim();
+ if (!prcsOpnnExpln) return;
+ const vrfcSn = Number(rows[idx]?.vrfcSn);
+ if (!Number.isFinite(vrfcSn)) return;
+ payload.push({ vrfcSn, prcsOpnnExpln });
+ });
if (payload.length === 0) {
alert("작성된 예외 제출 항목이 없습니다.");
return;
@@ -402,75 +356,13 @@ export function VerificationResultDialog({
}
}, [
alert,
- imrCheckedIndices,
- imrRows,
- normalCheckedIndices,
- normalRows,
+ checkedIndices,
+ rows,
onExceptionSubmitted,
onOpenChange,
stableQueryContext,
]);
- const renderGrid = (
- rows: VerificationResultGridRow[],
- setRows: Dispatch>,
- pageIndex: number,
- pageCount: number,
- onPageIndexChange: (page: number) => void,
- loading: boolean,
- gridRef: MutableRefObject,
- getGridDataRef: MutableRefObject<(() => VerificationResultGridRow[]) | null>,
- onPrcsOpnnExplnCellEdited: (params: CellEditedCallbackParams) => void,
- setCheckedIndices: Dispatch>>,
- ) => {
- if (!loading && rows.length === 0) {
- return {emptyMessage}
;
- }
-
- return (
-
-
[]>>}
- isExcelImport={false}
- isExcelExport={false}
- showHeaderCheckbox
- enableSort={false}
- getGridRef={gridRef}
- getGridDataRef={getGridDataRef}
- onCellEdited={onPrcsOpnnExplnCellEdited}
- onItemChecked={(itemIndex, checked) => {
- setCheckedIndices((prev) => {
- const next = new Set(prev);
- if (checked) next.add(itemIndex);
- else next.delete(itemIndex);
- return next;
- });
- }}
- onItemAllChecked={(checked) => {
- setCheckedIndices(
- checked ? new Set(rows.map((_, i) => i)) : new Set(),
- );
- }}
- />
-
- {loading && (
-
- )}
-
- );
- };
-
const titleSuffix = useMemo(() => {
if (!stableQueryContext) return "";
const sgg = stableQueryContext.sggNm?.trim() ? ` ${stableQueryContext.sggNm}` : "";
@@ -493,33 +385,56 @@ export function VerificationResultDialog({
)}
- 일반검증 내역
- {renderGrid(
- normalRows,
- setNormalRows,
- normalPageIndex,
- normalPageCount,
- handleNormalPageChange,
- normalLoading,
- normalGridRef,
- getNormalGridDataRef,
- onNormalPrcsOpnnExplnEdited,
- setNormalCheckedIndices,
- )}
-
-
- I-MR 검증내역
- {renderGrid(
- imrRows,
- setImrRows,
- imrPageIndex,
- imrPageCount,
- handleImrPageChange,
- imrLoading,
- imrGridRef,
- getImrGridDataRef,
- onImrPrcsOpnnExplnEdited,
- setImrCheckedIndices,
+ {!loading && rows.length === 0 ? (
+
+ {emptyMessage}
+
+ ) : (
+
+
[]>
+ >
+ }
+ isExcelImport={false}
+ isExcelExport={false}
+ showHeaderCheckbox
+ enableSort={false}
+ getGridRef={gridRef}
+ getGridDataRef={getGridDataRef}
+ onCellEdited={onPrcsOpnnExplnEdited}
+ cellStyleCallback={verificationMthdCellStyle}
+ onItemChecked={(itemIndex, checked) => {
+ setCheckedIndices((prev) => {
+ const next = new Set(prev);
+ if (checked) next.add(itemIndex);
+ else next.delete(itemIndex);
+ return next;
+ });
+ }}
+ onItemAllChecked={(checked) => {
+ setCheckedIndices(
+ checked ? new Set(rows.map((_, i) => i)) : new Set(),
+ );
+ }}
+ />
+
+ {loading && (
+
+ )}
+
)}
diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx
index 342a8c414..cd599665b 100644
--- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx
+++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx
@@ -1,7 +1,18 @@
"use client";
-import { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
+import {
+ type Dispatch,
+ type MutableRefObject,
+ type SetStateAction,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import type { RealGridReact } from "realgrid-react";
+import { getUserInfo } from "../../../../../packages/common/components/src/api";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
import {
@@ -22,6 +33,7 @@ import {
import DataGrid from "@/components/module/DataGrid";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
+import { useAlertStore } from "@/shared/lib";
type VerificationResultGridRow = Record;
type VerificationHistoryTabKey = "NV" | "IMR" | "HB";
@@ -32,6 +44,24 @@ const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string
{ value: "HB", label: "HB 검증내역" },
];
+const PAGE_SIZE = 30;
+
+/** vrfcSn → prcsYn (페이지 이동 후에도 취소 가능 여부 검증용) */
+const createEmptyCheckedVrfcByTab = (): Record> => ({
+ NV: new Map(),
+ IMR: new Map(),
+ HB: new Map(),
+});
+
+const initialTabPageState = (): Record<
+ VerificationHistoryTabKey,
+ { pageIndex: number; pageCount: number }
+> => ({
+ NV: { pageIndex: 0, pageCount: 1 },
+ IMR: { pageIndex: 0, pageCount: 1 },
+ HB: { pageIndex: 0, pageCount: 1 },
+});
+
function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined {
if (raw === undefined || raw === null) return undefined;
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
@@ -41,7 +71,62 @@ function prcsYnCodeToNumber(raw: string | number | undefined): number | undefine
return Number.isFinite(n) ? n : undefined;
}
-/** prcsYn 0·4 → 빨간색, 그 외 → 초록색 (처리여부 컬럼) */
+/** 예외 제출 취소 가능: prcsYn = 2 (예외처리) */
+function canExceptionReset(prcsYn: string | number | undefined): boolean {
+ return prcsYnCodeToNumber(prcsYn) === 2;
+}
+
+const EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE =
+ "예외 제출 취소는 처리여부가 사용자예외처리인 항목만 선택할 수 있습니다.";
+
+type GridViewWithCheckApi = {
+ getCheckedRows?: (allItems?: boolean, visibleOnly?: boolean, exclusive?: boolean) => number[];
+ checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
+};
+
+function uncheckGridRow(gridRef: MutableRefObject, itemIndex: number) {
+ try {
+ (gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.checkItem?.(itemIndex, false, false);
+ } catch {
+ /* ignore */
+ }
+}
+
+function syncCheckedMapFromGrid(
+ entries: VerificationResultReportItem[],
+ gridRef: MutableRefObject,
+ prevMap: Map,
+): Map {
+ const next = new Map(prevMap);
+ for (const entry of entries) {
+ const sn = Number(entry.vrfcSn);
+ if (Number.isFinite(sn)) next.delete(sn);
+ }
+ const indices =
+ (gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.getCheckedRows?.(true, false, true) ??
+ [];
+ for (const itemIndex of indices) {
+ const entry = entries[itemIndex];
+ if (!entry) continue;
+ const sn = Number(entry.vrfcSn);
+ if (!Number.isFinite(sn)) continue;
+ next.set(sn, String(entry.prcsYn ?? ""));
+ }
+ return next;
+}
+
+function selectionHasNonExceptionResetAllowed(
+ map: Map,
+ currentEntries: VerificationResultReportItem[],
+): boolean {
+ for (const [vrfcSn, storedPrcsYn] of map) {
+ const onPage = currentEntries.find((e) => Number(e.vrfcSn) === vrfcSn);
+ const prcsYn = onPage ? onPage.prcsYn : storedPrcsYn;
+ if (!canExceptionReset(prcsYn)) return true;
+ }
+ return false;
+}
+
function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined {
if (fieldName !== "prcsYnNm") return undefined;
const code = prcsYnCodeToNumber(row.prcsYn);
@@ -62,8 +147,22 @@ const verificationResultColumnInfo: ColumnInfo = {
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
+ { name: "vrfcSn", fieldName: "vrfcSn", header: { text: "검증일련번호" }, editable: false, visible: false },
+ ],
+ columnLayout: [
+ "ctpvNm",
+ "sggNm",
+ "vrfcTrgtNm",
+ "rsnExpln",
+ "nowVlCn",
+ "compVlCn",
+ "prcsYnNm",
+ "pbcpAprvYn",
+ "regDt",
+ "mdfcnDt",
+ "prcsYn",
+ "vrfcSn",
],
- columnLayout: ["ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYnNm", "pbcpAprvYn", "regDt", "mdfcnDt", "prcsYn"],
};
export interface VerificationResultHistoryDialogProps {
@@ -87,152 +186,168 @@ export function VerificationResultHistoryDialog({
title = "검증 결과 내역",
emptyMessage = "표시할 검증 결과가 없습니다.",
}: VerificationResultHistoryDialogProps) {
+ const stableQueryContext = useMemo(() => {
+ if (!queryContext) return null;
+ return queryContext;
+ }, [
+ queryContext?.sewerBzmnCd,
+ queryContext?.ctpvNm,
+ queryContext?.sggNm,
+ queryContext?.statsYr,
+ queryContext?.statsCd,
+ ]);
+
const [activeTab, setActiveTab] = useState("NV");
- const [normalEntries, setNormalEntries] = useState([]);
- const [imrEntries, setImrEntries] = useState([]);
- const [hbEntries, setHbEntries] = useState([]);
- const [normalPageIndex, setNormalPageIndex] = useState(0);
- const [imrPageIndex, setImrPageIndex] = useState(0);
- const [hbPageIndex, setHbPageIndex] = useState(0);
- const [normalPageCount, setNormalPageCount] = useState(1);
- const [imrPageCount, setImrPageCount] = useState(1);
- const [hbPageCount, setHbPageCount] = useState(1);
- const [normalLoading, setNormalLoading] = useState(false);
- const [imrLoading, setImrLoading] = useState(false);
- const [hbLoading, setHbLoading] = useState(false);
+ const [entriesByTab, setEntriesByTab] = useState<
+ Record
+ >({
+ NV: [],
+ IMR: [],
+ HB: [],
+ });
+ const [tabPages, setTabPages] = useState(initialTabPageState);
+ const [loadingByTab, setLoadingByTab] = useState>({
+ NV: false,
+ IMR: false,
+ HB: false,
+ });
const [errorMessage, setErrorMessage] = useState(null);
+ const [resetSubmitting, setResetSubmitting] = useState(false);
+ const [checkedVrfcByTab, setCheckedVrfcByTab] = useState(createEmptyCheckedVrfcByTab);
+
+ const nvGridRef = useRef(null);
+ const imrGridRef = useRef(null);
+ const hbGridRef = useRef(null);
+ const alert = useAlertStore((state) => state.setMessage);
+
+ const gridRefByTab: Record> = {
+ NV: nvGridRef,
+ IMR: imrGridRef,
+ HB: hbGridRef,
+ };
+ const checkedVrfcByTabRef = useRef(checkedVrfcByTab);
+ checkedVrfcByTabRef.current = checkedVrfcByTab;
const resetState = useCallback(() => {
setActiveTab("NV");
- setNormalEntries([]);
- setImrEntries([]);
- setHbEntries([]);
- setNormalPageIndex(0);
- setImrPageIndex(0);
- setHbPageIndex(0);
- setNormalPageCount(1);
- setImrPageCount(1);
- setHbPageCount(1);
- setNormalLoading(false);
- setImrLoading(false);
- setHbLoading(false);
+ setEntriesByTab({ NV: [], IMR: [], HB: [] });
+ setTabPages(initialTabPageState());
+ setLoadingByTab({ NV: false, IMR: false, HB: false });
setErrorMessage(null);
+ setResetSubmitting(false);
+ setCheckedVrfcByTab(createEmptyCheckedVrfcByTab());
}, []);
const fetchVerificationResultPage = useCallback(
async (vrfcMthdCd: VerificationHistoryTabKey, pageIndex: number) => {
- if (!queryContext) return null;
+ if (!stableQueryContext) return null;
const param: VerificationResultListRequest = {
- sewerBzmnCd: queryContext.sewerBzmnCd,
- ctpvNm: queryContext.ctpvNm,
- sggNm: queryContext.sggNm,
- statsYr: queryContext.statsYr,
- statsCd: queryContext.statsCd,
- pbcpAprvYn: 'N',
+ sewerBzmnCd: stableQueryContext.sewerBzmnCd,
+ ctpvNm: stableQueryContext.ctpvNm,
+ sggNm: stableQueryContext.sggNm,
+ statsYr: stableQueryContext.statsYr,
+ statsCd: stableQueryContext.statsCd,
+ pbcpAprvYn: "N",
vrfcMthdCd,
prcsYnList: "0,2,3",
page: pageIndex + 1,
- size: 30,
+ size: PAGE_SIZE,
};
return statisticsApi.getVerificationResultList(param);
},
- [queryContext],
+ [stableQueryContext],
+ );
+
+ const applyPageResponse = useCallback(
+ (tab: VerificationHistoryTabKey, pageIndex: number, list: VerificationResultReportItem[], totalPages: number) => {
+ const pageCount = Math.max(totalPages, 1);
+ const clampedIndex = Math.min(Math.max(pageIndex, 0), pageCount - 1);
+ setEntriesByTab((prev) => ({ ...prev, [tab]: list }));
+ setTabPages((prev) => ({
+ ...prev,
+ [tab]: { pageIndex: clampedIndex, pageCount },
+ }));
+ },
+ [],
+ );
+
+ const loadTabPage = useCallback(
+ async (tab: VerificationHistoryTabKey, pageIndex: number) => {
+ if (!stableQueryContext) {
+ setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
+ return;
+ }
+ setErrorMessage(null);
+ setLoadingByTab((prev) => ({ ...prev, [tab]: true }));
+ try {
+ const response = await fetchVerificationResultPage(tab, pageIndex);
+ const list = response?.data?.verifyResultList ?? [];
+ const totalPages = response?.data?.pageInfo?.totalPages ?? 1;
+ const apiCurrentPage = response?.data?.pageInfo?.currentPage;
+ const resolvedPageIndex =
+ apiCurrentPage != null && Number.isFinite(apiCurrentPage)
+ ? Math.max(0, Number(apiCurrentPage) - 1)
+ : pageIndex;
+ applyPageResponse(tab, resolvedPageIndex, list, totalPages);
+ } catch {
+ const tabLabel =
+ verificationHistoryTabs.find((t) => t.value === tab)?.label ?? tab;
+ setErrorMessage(`${tabLabel} 조회 중 오류가 발생했습니다.`);
+ } finally {
+ setLoadingByTab((prev) => ({ ...prev, [tab]: false }));
+ }
+ },
+ [applyPageResponse, fetchVerificationResultPage, stableQueryContext],
);
const loadInitial = useCallback(async () => {
- if (!queryContext) {
+ if (!stableQueryContext) {
setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
return;
}
setErrorMessage(null);
- setNormalLoading(true);
- setImrLoading(true);
- setHbLoading(true);
- setNormalPageIndex(0);
- setImrPageIndex(0);
- setHbPageIndex(0);
+ setTabPages(initialTabPageState());
+ setLoadingByTab({ NV: true, IMR: true, HB: true });
try {
- const [nvRes, imrRes, hbRes] = await Promise.all([
- fetchVerificationResultPage("NV", 0),
- fetchVerificationResultPage("IMR", 0),
- fetchVerificationResultPage("HB", 0),
- ]);
- setNormalEntries(nvRes?.data?.verifyResultList ?? []);
- setImrEntries(imrRes?.data?.verifyResultList ?? []);
- setHbEntries(hbRes?.data?.verifyResultList ?? []);
- setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
- setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
- setHbPageCount(hbRes?.data?.pageInfo?.totalPages ?? 1);
+ const results = await Promise.all(
+ verificationHistoryTabs.map((tab) =>
+ fetchVerificationResultPage(tab.value, 0),
+ ),
+ );
+ setEntriesByTab({
+ NV: results[0]?.data?.verifyResultList ?? [],
+ IMR: results[1]?.data?.verifyResultList ?? [],
+ HB: results[2]?.data?.verifyResultList ?? [],
+ });
+ setTabPages({
+ NV: {
+ pageIndex: 0,
+ pageCount: Math.max(results[0]?.data?.pageInfo?.totalPages ?? 1, 1),
+ },
+ IMR: {
+ pageIndex: 0,
+ pageCount: Math.max(results[1]?.data?.pageInfo?.totalPages ?? 1, 1),
+ },
+ HB: {
+ pageIndex: 0,
+ pageCount: Math.max(results[2]?.data?.pageInfo?.totalPages ?? 1, 1),
+ },
+ });
} catch {
- setNormalEntries([]);
- setImrEntries([]);
- setHbEntries([]);
- setNormalPageCount(1);
- setImrPageCount(1);
- setHbPageCount(1);
+ setEntriesByTab({ NV: [], IMR: [], HB: [] });
+ setTabPages(initialTabPageState());
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
} finally {
- setNormalLoading(false);
- setImrLoading(false);
- setHbLoading(false);
+ setLoadingByTab({ NV: false, IMR: false, HB: false });
}
- }, [fetchVerificationResultPage, queryContext]);
+ }, [fetchVerificationResultPage, stableQueryContext]);
- const handleNormalPageChange = useCallback(
- async (pageIndex: number) => {
- if (pageIndex === normalPageIndex || !queryContext) return;
- setErrorMessage(null);
- setNormalLoading(true);
- try {
- const response = await fetchVerificationResultPage("NV", pageIndex);
- setNormalEntries(response?.data?.verifyResultList ?? []);
- setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1);
- setNormalPageIndex(pageIndex);
- } catch {
- setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다.");
- } finally {
- setNormalLoading(false);
- }
+ const handleTabPageChange = useCallback(
+ (tab: VerificationHistoryTabKey, pageIndex: number) => {
+ if (pageIndex === tabPages[tab].pageIndex) return;
+ void loadTabPage(tab, pageIndex);
},
- [fetchVerificationResultPage, normalPageIndex, queryContext],
- );
-
- const handleImrPageChange = useCallback(
- async (pageIndex: number) => {
- if (pageIndex === imrPageIndex || !queryContext) return;
- setErrorMessage(null);
- setImrLoading(true);
- try {
- const response = await fetchVerificationResultPage("IMR", pageIndex);
- setImrEntries(response?.data?.verifyResultList ?? []);
- setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
- setImrPageIndex(pageIndex);
- } catch {
- setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다.");
- } finally {
- setImrLoading(false);
- }
- },
- [fetchVerificationResultPage, imrPageIndex, queryContext],
- );
-
- const handleHbPageChange = useCallback(
- async (pageIndex: number) => {
- if (pageIndex === hbPageIndex || !queryContext) return;
- setErrorMessage(null);
- setHbLoading(true);
- try {
- const response = await fetchVerificationResultPage("HB", pageIndex);
- setHbEntries(response?.data?.verifyResultList ?? []);
- setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
- setHbPageIndex(pageIndex);
- } catch {
- setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다.");
- } finally {
- setHbLoading(false);
- }
- },
- [fetchVerificationResultPage, hbPageIndex, queryContext],
+ [loadTabPage, tabPages],
);
useEffect(() => {
@@ -243,36 +358,115 @@ export function VerificationResultHistoryDialog({
void loadInitial();
}, [loadInitial, open, resetState]);
- const mapRows = (entries: VerificationResultReportItem[]): VerificationResultGridRow[] =>
- entries.map((item) => ({
- ctpvNm: item.ctpvNm,
- sggNm: item.sggNm,
- vrfcTrgtNm: item.vrfcTrgtNm,
- rsnExpln: item.rsnExpln,
- nowVlCn: item.nowVlCn ?? "-",
- compVlCn: item.compVlCn ?? "-",
- prcsYnNm: item.prcsYnNm ?? "-",
- prcsYn: item.prcsYn ?? "",
- pbcpAprvYn: item.pbcpAprvYn,
- regDt: (item.regDt ?? "").slice(0, 16),
- mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
- }));
+ const updateCheckedVrfcForTab = useCallback(
+ (tab: VerificationHistoryTabKey, updater: (map: Map) => void) => {
+ setCheckedVrfcByTab((prev) => {
+ const nextMap = new Map(prev[tab]);
+ updater(nextMap);
+ return { ...prev, [tab]: nextMap };
+ });
+ },
+ [],
+ );
- const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
- const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
- const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]);
+ const mapRows = useCallback(
+ (entries: VerificationResultReportItem[], checkedMap: Map): VerificationResultGridRow[] =>
+ entries.map((item) => {
+ const vrfcSn = Number(item.vrfcSn);
+ const isChecked = Number.isFinite(vrfcSn) && checkedMap.has(vrfcSn);
+ return {
+ vrfcSn: item.vrfcSn,
+ __checked: isChecked ? 1 : 0,
+ ctpvNm: item.ctpvNm,
+ sggNm: item.sggNm,
+ vrfcTrgtNm: item.vrfcTrgtNm,
+ rsnExpln: item.rsnExpln,
+ nowVlCn: item.nowVlCn ?? "-",
+ compVlCn: item.compVlCn ?? "-",
+ prcsYnNm: item.prcsYnNm ?? "-",
+ prcsYn: item.prcsYn ?? "",
+ pbcpAprvYn: item.pbcpAprvYn,
+ regDt: (item.regDt ?? "").slice(0, 16),
+ mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
+ };
+ }),
+ [],
+ );
+
+ /** 페이지(entries) 변경 시에만 __checked 반영 — 클릭마다 rowData를 바꾸지 않아 체크 UI가 안정적 */
+ const rowsByTab = useMemo(
+ () => {
+ const checked = checkedVrfcByTabRef.current;
+ return {
+ NV: mapRows(entriesByTab.NV, checked.NV),
+ IMR: mapRows(entriesByTab.IMR, checked.IMR),
+ HB: mapRows(entriesByTab.HB, checked.HB),
+ };
+ },
+ [entriesByTab, mapRows],
+ );
const noopSetRows = ((_: SetStateAction[]>) => {}) as Dispatch<
SetStateAction[]>
>;
- const renderGrid = (
- rows: VerificationResultGridRow[],
- pageIndex: number,
- pageCount: number,
- onPageIndexChange: (page: number) => void,
- loading: boolean,
- ) => {
+ const handleExceptionResetClick = useCallback(async () => {
+ if (!stableQueryContext) {
+ alert("조회 조건이 없습니다.");
+ return;
+ }
+ const tab = activeTab;
+ const currentEntries = entriesByTab[tab];
+ const syncedMap = syncCheckedMapFromGrid(currentEntries, gridRefByTab[tab], checkedVrfcByTab[tab]);
+ if (syncedMap.size === 0) {
+ alert("예외 제출 취소할 항목을 체크해주세요.");
+ return;
+ }
+ if (selectionHasNonExceptionResetAllowed(syncedMap, currentEntries)) {
+ alert(EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE);
+ return;
+ }
+ setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: syncedMap }));
+ const vrfcSnList = Array.from(syncedMap.keys());
+ let mdfrId: string;
+ try {
+ const userInfo = await getUserInfo();
+ mdfrId = userInfo.userUnqId;
+ } catch {
+ alert("사용자 정보를 불러오지 못했습니다.");
+ return;
+ }
+ setResetSubmitting(true);
+ setErrorMessage(null);
+ try {
+ const response = await statisticsApi.StatiscicsExceptionReset({
+ statsYr: stableQueryContext.statsYr,
+ sewerBzmnCd: stableQueryContext.sewerBzmnCd,
+ statsCd: stableQueryContext.statsCd,
+ mdfrId,
+ vrfcSnList,
+ });
+ if (response.status !== "success") {
+ alert(response.message ?? "예외 제출 취소 처리 중 오류가 발생했습니다.");
+ return;
+ }
+ alert("예외 제출 취소 처리되었습니다.");
+ setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: new Map() }));
+ await loadTabPage(tab, tabPages[tab].pageIndex);
+ } catch {
+ alert("예외 제출 취소 처리 중 오류가 발생했습니다.");
+ } finally {
+ setResetSubmitting(false);
+ }
+ }, [activeTab, alert, checkedVrfcByTab, entriesByTab, loadTabPage, stableQueryContext, tabPages]);
+
+ const renderActiveTabPanel = (tab: VerificationHistoryTabKey) => {
+ const entries = entriesByTab[tab];
+ const rows = rowsByTab[tab];
+ const { pageIndex, pageCount } = tabPages[tab];
+ const loading = loadingByTab[tab];
+ const gridRef = gridRefByTab[tab];
+
if (!loading && rows.length === 0) {
return {emptyMessage}
;
}
@@ -280,19 +474,57 @@ export function VerificationResultHistoryDialog({
return (
{
+ const entry = entries[itemIndex];
+ if (!entry) return;
+ const vrfcSn = Number(entry.vrfcSn);
+ if (!Number.isFinite(vrfcSn)) return;
+ if (checked && !canExceptionReset(entry.prcsYn)) {
+ alert(EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE);
+ uncheckGridRow(gridRef, itemIndex);
+ return;
+ }
+ updateCheckedVrfcForTab(tab, (map) => {
+ if (checked) map.set(vrfcSn, String(entry.prcsYn ?? ""));
+ else map.delete(vrfcSn);
+ });
+ }}
+ onItemAllChecked={(checked) => {
+ updateCheckedVrfcForTab(tab, (map) => {
+ entries.forEach((entry, itemIndex) => {
+ const vrfcSn = Number(entry.vrfcSn);
+ if (!Number.isFinite(vrfcSn)) return;
+ if (!checked) {
+ map.delete(vrfcSn);
+ return;
+ }
+ if (canExceptionReset(entry.prcsYn)) {
+ map.set(vrfcSn, String(entry.prcsYn ?? ""));
+ } else {
+ uncheckGridRow(gridRef, itemIndex);
+ }
+ });
+ });
+ if (checked && entries.some((e) => !canExceptionReset(e.prcsYn))) {
+ alert(EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE);
+ }
+ }}
/>
handleTabPageChange(tab, nextPage)}
className="mx-2 mb-2"
/>
{loading && (
@@ -318,7 +550,12 @@ export function VerificationResultHistoryDialog({
{errorMessage}
)}
- setActiveTab(value as VerificationHistoryTabKey)} className="w-full" variant="default">
+ setActiveTab(value as VerificationHistoryTabKey)}
+ className="w-full"
+ variant="default"
+ >
{verificationHistoryTabs.map((tab) => (
@@ -326,20 +563,28 @@ export function VerificationResultHistoryDialog({
))}
-
- {renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
-
-
- {renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
-
-
- {renderGrid(hbRows, hbPageIndex, hbPageCount, handleHbPageChange, hbLoading)}
-
+ {verificationHistoryTabs.map((tab) => (
+
+ {activeTab === tab.value ? renderActiveTabPanel(tab.value) : null}
+
+ ))}
+
-
diff --git a/apps/st/src/components/module/post-submit-verification-results.ts b/apps/st/src/components/module/post-submit-verification-results.ts
new file mode 100644
index 000000000..a46ad79cb
--- /dev/null
+++ b/apps/st/src/components/module/post-submit-verification-results.ts
@@ -0,0 +1,85 @@
+import { statisticsApi } from "@/entities/statistics/api/statistics-service";
+import type { VerificationResultListResponse } from "@/entities/statistics/model/statistics-interface";
+
+import type { VerificationResultDialogQueryContext } from "./VerificationResultDialog";
+
+const POST_SUBMIT_VRFC_MTHD_CODES = ["NV", "IMR", "HB"] as const;
+
+export const POST_SUBMIT_COMPLETE_MESSAGE = "제출 완료되었습니다.";
+
+export type OpenVerificationResultDialogOptions = {
+ notify?: (message: string) => void;
+ /** 미통과 검증 결과가 없을 때 제출 완료 알림 (기본 true, IMR 오류 catch 등에서는 false) */
+ notifySubmitComplete?: boolean;
+ submitCompleteMessage?: string;
+};
+
+function responseHasVerificationResults(response: VerificationResultListResponse | null): boolean {
+ const total = response?.data?.pageInfo?.totalElements;
+ if (typeof total === "number" && total > 0) return true;
+ const list = response?.data?.verifyResultList;
+ return Array.isArray(list) && list.length > 0;
+}
+
+/**
+ * 제출 직후 `VerificationResultDialog`에 표시할 미통과(prcsYn=0) 검증 결과가
+ * 일반검증(NV)·I-MR(IMR)·HB 중 하나라도 있는지 조회합니다.
+ */
+export async function hasAnyPostSubmitVerificationResult(
+ ctx: VerificationResultDialogQueryContext,
+): Promise {
+ const base = {
+ sewerBzmnCd: ctx.sewerBzmnCd,
+ ctpvNm: ctx.ctpvNm,
+ sggNm: ctx.sggNm,
+ statsYr: ctx.statsYr,
+ statsCd: ctx.statsCd,
+ pbcpAprvYn: "N" as const,
+ prcsYnList: "0",
+ page: 1,
+ size: 1,
+ };
+
+ const responses = await Promise.all(
+ POST_SUBMIT_VRFC_MTHD_CODES.map((vrfcMthdCd) =>
+ statisticsApi.getVerificationResultList({ ...base, vrfcMthdCd }),
+ ),
+ );
+
+ return responses.some(responseHasVerificationResults);
+}
+
+/**
+ * 미통과 검증 결과가 있으면 다이얼로그를 열고, 없으면 제출 완료 알림을 표시합니다.
+ * @returns 다이얼로그를 열었으면 true
+ */
+export async function openVerificationResultDialogIfNeeded(
+ ctx: VerificationResultDialogQueryContext | null | undefined,
+ setOpen: (open: boolean) => void,
+ options?: OpenVerificationResultDialogOptions,
+): Promise {
+ const {
+ notify,
+ notifySubmitComplete = true,
+ submitCompleteMessage = POST_SUBMIT_COMPLETE_MESSAGE,
+ } = options ?? {};
+
+ const notifyComplete = () => {
+ if (notifySubmitComplete && notify) {
+ notify(submitCompleteMessage);
+ }
+ };
+
+ if (!ctx) {
+ notifyComplete();
+ return false;
+ }
+
+ if (await hasAnyPostSubmitVerificationResult(ctx)) {
+ setOpen(true);
+ return true;
+ }
+
+ notifyComplete();
+ return false;
+}
diff --git a/apps/st/src/components/statistics/VerificationSelectionReport.tsx b/apps/st/src/components/statistics/VerificationSelectionReport.tsx
index ec81f97c3..47a6c8401 100644
--- a/apps/st/src/components/statistics/VerificationSelectionReport.tsx
+++ b/apps/st/src/components/statistics/VerificationSelectionReport.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useCallback } from "react";
+import { useCallback, useState } from "react";
import { useQueries } from "@tanstack/react-query";
import { useTabStore as useGridTabStore } from "@rsone/components";
@@ -19,16 +19,30 @@ import {
Badge,
type BadgeProps,
LoadingWithAnimation,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
} from "@common/components";
+type VerificationReportTabKey = "NV" | "IMR" | "HB";
+
+const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [
+ { value: "NV", label: "일반" },
+ { value: "IMR", label: "I-MR" },
+ { value: "HB", label: "HB" },
+];
+
const TYPE_CODE_COLOR_MAP: Record = {
NV: "gray",
IMR: "red",
+ HB: "blue",
};
const TYPE_CODE_NAME_MAP: Record = {
NV: "일반",
IMR: "I-MR",
+ HB: "HB",
};
type VerificationSelectionReportProps = {
@@ -46,6 +60,7 @@ const VerificationSelectionReport = ({
(s) => s.setPendingInputTab,
);
const alert = useAlertStore((s) => s.setMessage);
+ const [activeTab, setActiveTab] = useState("NV");
const statsYr = String(dayjs().year() - 1);
@@ -57,7 +72,7 @@ const VerificationSelectionReport = ({
prcsYnList: "0",
};
- const [nvQuery, imrQuery] = useQueries({
+ const [nvQuery, imrQuery, hbQuery] = useQueries({
queries: [
{
queryKey: [
@@ -91,14 +106,34 @@ const VerificationSelectionReport = ({
vrfcMthdCd: "IMR",
}),
},
+ {
+ queryKey: [
+ "verificationResultList",
+ statsYr,
+ "HB",
+ baseRequest.pbcpAprvYn,
+ baseRequest.page,
+ baseRequest.size,
+ baseRequest.prcsYnList,
+ ],
+ queryFn: () =>
+ statisticsApi.getVerificationResultList({
+ ...baseRequest,
+ vrfcMthdCd: "HB",
+ }),
+ },
],
});
- const isLoading = nvQuery.isLoading || imrQuery.isLoading;
- const error = nvQuery.error ?? imrQuery.error;
+ const isLoading =
+ nvQuery.isLoading || imrQuery.isLoading || hbQuery.isLoading;
+ const error = nvQuery.error ?? imrQuery.error ?? hbQuery.error;
- const nvList = nvQuery.data?.data?.verifyResultList ?? [];
- const imrList = imrQuery.data?.data?.verifyResultList ?? [];
+ const listsByTab: Record = {
+ NV: nvQuery.data?.data?.verifyResultList ?? [],
+ IMR: imrQuery.data?.data?.verifyResultList ?? [],
+ HB: hbQuery.data?.data?.verifyResultList ?? [],
+ };
const openInputFromStatsCd = useCallback(
(item: VerificationResultReportItem) => {
@@ -147,68 +182,61 @@ const VerificationSelectionReport = ({
],
);
- const renderSection = (
- typeCode: "IMR" | "NV",
+ const renderResultList = (
+ typeCode: VerificationReportTabKey,
items: VerificationResultReportItem[],
) => (
-
-
-
- {TYPE_CODE_NAME_MAP[typeCode]} {items.length}
-
-
-
- {items.length === 0 ? (
-
- 항목이 없습니다.
-
- ) : (
-
- {items.map((item) => (
-
openInputFromStatsCd(item)}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- openInputFromStatsCd(item);
+
+ {items.length === 0 ? (
+
+ 항목이 없습니다.
+
+ ) : (
+
+ {items.map((item) => (
+
openInputFromStatsCd(item)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ openInputFromStatsCd(item);
+ }
+ }}
+ >
+
+
+ {item.statsNm}
+
+
-
-
- {item.statsNm}
-
-
- {TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ??
- item.vrfcMthdNm}
-
-
-
- {item.rsnExpln}
-
-
- {item.admdstNm}
-
- 현재: {item.nowVlCn ?? "-"} / 비교:{" "}
- {item.compVlCn ?? "-"}
-
-
+ className="text-sm"
+ >
+ {TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ??
+ item.vrfcMthdNm}
+
- ))}
-
- )}
-
-
+
+ {item.rsnExpln}
+
+
+ {item.admdstNm}
+
+ 현재: {item.nowVlCn ?? "-"} / 비교:{" "}
+ {item.compVlCn ?? "-"}
+
+
+
+ ))}
+
+ )}
+
);
return (
@@ -246,10 +274,42 @@ const VerificationSelectionReport = ({
)}
{!isLoading && !error && (
-
- {renderSection("IMR", imrList)}
- {renderSection("NV", nvList)}
-
+
+
+ setActiveTab(value as VerificationReportTabKey)
+ }
+ className="flex min-h-0 flex-1 flex-col h-full"
+ variant="default"
+ >
+
+ {VERIFICATION_REPORT_TABS.map((tab) => (
+
+ {tab.label} ({listsByTab[tab.value].length})
+
+ ))}
+
+ {VERIFICATION_REPORT_TABS.map((tab) => (
+
+ {activeTab === tab.value
+ ? renderResultList(
+ tab.value,
+ listsByTab[tab.value],
+ )
+ : null}
+
+ ))}
+
+
)}
diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
index e8c71de1b..6735f9752 100644
--- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
+++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
@@ -38,6 +38,7 @@ import VerificationResultButton from '@/components/button/VerificationResultButt
import DataGrid from '@/components/module/DataGrid';
import SearchForm from '@/components/module/SearchForm';
import SplitDataGrid from '@/components/module/SplitDataGrid';
+import { openVerificationResultDialogIfNeeded } from '@/components/module/post-submit-verification-results';
import { VerificationResultDialog } from '@/components/module/VerificationResultDialog';
import { VerificationResultHistoryDialog } from '@/components/module/VerificationResultHistoryDialog';
import type { CellStyleCallbackParams, ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type';
@@ -342,14 +343,34 @@ const PublicSewageFacilityInp = () => {
searchParamRef.current,
lastSearchedValues,
);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ {
+ sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
+ ctpvNm: lastSearchedValues.sido ?? '',
+ sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''),
+ statsYr: lastSearchedValues.year ?? '',
+ statsCd: statsCdForTab,
+ },
+ setVerificationResultDialogOpen,
+ { notify: alert },
+ );
} catch (e) {
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ {
+ sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
+ ctpvNm: lastSearchedValues.sido ?? '',
+ sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''),
+ statsYr: lastSearchedValues.year ?? '',
+ statsCd: statsCdForTab,
+ },
+ setVerificationResultDialogOpen,
+ { notify: alert, notifySubmitComplete: false },
+ );
const msg =
e instanceof Error && e.message
? e.message
@@ -363,7 +384,17 @@ const PublicSewageFacilityInp = () => {
searchParamRef.current,
lastSearchedValues,
);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ {
+ sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
+ ctpvNm: lastSearchedValues.sido ?? '',
+ sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''),
+ statsYr: lastSearchedValues.year ?? '',
+ statsCd: statsCdForTab,
+ },
+ setVerificationResultDialogOpen,
+ { notify: alert },
+ );
}
} catch {
alert('데이터 제출 중 오류가 발생했습니다.');
@@ -472,7 +503,7 @@ const PublicSewageFacilityInp = () => {
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId,
- superAdmin: isAdminInfo ? 'superAdmin' : '',
+ // superAdmin: isAdminInfo ? 'superAdmin' : '',
noApplicable: getNoApplicableForRequest(),
saveList,
};
diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx
index 963be10fb..9329dc967 100644
--- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx
+++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx
@@ -56,6 +56,7 @@ import DataGrid from "@/components/module/DataGrid";
import GridTopActionButton from "@/components/module/GridTopActionButton";
import SearchForm from "@/components/module/SearchForm";
import SplitDataGrid from "@/components/module/SplitDataGrid";
+import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig,ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
@@ -1248,7 +1249,17 @@ const SewagePipeInp = () => {
true,
false,
);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ {
+ sewerBzmnCd: values.bsnmCd ?? "",
+ ctpvNm: values.sido ?? "",
+ sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
+ statsYr: values.year ?? "",
+ statsCd: statsCdForTab,
+ },
+ setVerificationResultDialogOpen,
+ { notify: alert },
+ );
} catch (e) {
await loadSewagePipeActiveTabData(
tabKey,
@@ -1257,7 +1268,17 @@ const SewagePipeInp = () => {
true,
false,
);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ {
+ sewerBzmnCd: values.bsnmCd ?? "",
+ ctpvNm: values.sido ?? "",
+ sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
+ statsYr: values.year ?? "",
+ statsCd: statsCdForTab,
+ },
+ setVerificationResultDialogOpen,
+ { notify: alert, notifySubmitComplete: false },
+ );
const msg =
e instanceof Error && e.message
? e.message
@@ -1273,7 +1294,17 @@ const SewagePipeInp = () => {
true,
false,
);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ {
+ sewerBzmnCd: values.bsnmCd ?? "",
+ ctpvNm: values.sido ?? "",
+ sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
+ statsYr: values.year ?? "",
+ statsCd: statsCdForTab,
+ },
+ setVerificationResultDialogOpen,
+ { notify: alert },
+ );
}
} catch {
alert("데이터 제출 중 오류가 발생했습니다.");
@@ -1429,7 +1460,7 @@ const SewagePipeInp = () => {
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId,
- superAdmin: isAdminInfo ? "superAdmin" : "",
+ // superAdmin: isAdminInfo ? "superAdmin" : "",
saveList,
};
@@ -1509,7 +1540,7 @@ const SewagePipeInp = () => {
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId,
- superAdmin: isAdminInfo ? "superAdmin" : "",
+ // superAdmin: isAdminInfo ? "superAdmin" : "",
saveList,
};
@@ -1594,7 +1625,7 @@ const SewagePipeInp = () => {
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId,
- superAdmin: isAdminInfo ? "superAdmin" : "",
+ // superAdmin: isAdminInfo ? "superAdmin" : "",
saveList,
};
@@ -1657,7 +1688,7 @@ const SewagePipeInp = () => {
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId,
- superAdmin: isAdminInfo ? "superAdmin" : "",
+ // superAdmin: isAdminInfo ? "superAdmin" : "",
saveList,
};
@@ -1736,7 +1767,7 @@ const SewagePipeInp = () => {
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId,
- superAdmin: isAdminInfo ? "superAdmin" : "",
+ // superAdmin: isAdminInfo ? "superAdmin" : "",
saveList,
};
diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx
index 22b160656..2e0f318f9 100644
--- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx
+++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx
@@ -41,6 +41,7 @@ import VerificationResultButton from "@/components/button/VerificationResultButt
// import { DataGrid } from "@rsone/components";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
+import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
@@ -348,9 +349,9 @@ const SeweragePenetrationRateInp = () => {
statsCd: statsCd,
};
- if (isAdminInfo) {
- params.superAdmin = 'superAdmin';
- }
+ // if (isAdminInfo) {
+ // params.superAdmin = 'superAdmin';
+ // }
try {
setIsLoading(true);
@@ -525,11 +526,35 @@ const SeweragePenetrationRateInp = () => {
}
setGlobalProgress(100);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
+ ? {
+ sewerBzmnCd: searchValues.bsnmCd,
+ ctpvNm: searchValues.sido,
+ sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
+ statsYr: searchValues.year,
+ statsCd,
+ }
+ : null,
+ setVerificationResultDialogOpen,
+ { notify: alert },
+ );
} catch (e) {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
setIsLoading(false);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
+ ? {
+ sewerBzmnCd: searchValues.bsnmCd,
+ ctpvNm: searchValues.sido,
+ sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
+ statsYr: searchValues.year,
+ statsCd,
+ }
+ : null,
+ setVerificationResultDialogOpen,
+ { notify: alert, notifySubmitComplete: false },
+ );
const msg =
e instanceof Error && e.message
? e.message
@@ -540,7 +565,19 @@ const SeweragePenetrationRateInp = () => {
} else {
setGlobalProgress(100);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
- setVerificationResultDialogOpen(true);
+ await openVerificationResultDialogIfNeeded(
+ searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
+ ? {
+ sewerBzmnCd: searchValues.bsnmCd,
+ ctpvNm: searchValues.sido,
+ sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
+ statsYr: searchValues.year,
+ statsCd,
+ }
+ : null,
+ setVerificationResultDialogOpen,
+ { notify: alert },
+ );
}
} else {
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
@@ -641,9 +678,9 @@ const SeweragePenetrationRateInp = () => {
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계
};
- if (isAdminInfo) {
- requestData.superAdmin = 'superAdmin';
- }
+ // if (isAdminInfo) {
+ // requestData.superAdmin = 'superAdmin';
+ // }
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts
index 3dea0d65a..9edcc2b03 100644
--- a/apps/st/src/components/type/DataGrid.type.ts
+++ b/apps/st/src/components/type/DataGrid.type.ts
@@ -162,6 +162,8 @@ export type ColumnConfig = {
editable?: boolean; // 셀 편집 가능 여부
/** 그리드에서 컬럼 숨김 (수식용 필드 등) */
visible?: boolean;
+ /** true이면 엑셀보내기에서 제외 (화면에는 표시 가능) */
+ excludeFromExcelExport?: boolean;
/** 컬럼 너비 (px). 지정 시 ... 생략 없이 텍스트 표시에 유리 */
width?: number;
/** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */
diff --git a/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts
index f8f5d3a06..9f02da89a 100644
--- a/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts
+++ b/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts
@@ -4,7 +4,7 @@ import { ValueType } from "realgrid";
// 하수도 통계 관리 > 02.하수관로 > tab 5. 맨홀 현황
// SeweragePipeInputManholeStatusItem + commonInfo.ctpvNm (그리드 행에 병합)
-// API: yy14=2014년 … yy{(현재연도-1)%100} (필드 접두 yy + 연도 끝자리)
+// API: yy14=2015년 이전(2014년 데이터) … yy{(현재연도-1)%100} (필드 접두 yy + 연도 끝자리)
const MANHOLE_START_YEAR = 2014;
const MANHOLE_LAST_DATA_YEAR = Math.max(MANHOLE_START_YEAR, new Date().getFullYear() - 1);
@@ -12,6 +12,12 @@ const MANHOLE_YY_MIN = MANHOLE_START_YEAR % 100;
const MANHOLE_YY_MAX = MANHOLE_LAST_DATA_YEAR % 100;
const calendarYearFromYy = (yy: number) => 2000 + yy;
+/** yy14(2014) 구간은 헤더에 「2015년 이전」 표기 (합계·검토 컬럼과 동일) */
+const manholeYearHeaderText = (yy: number): string => {
+ const y = calendarYearFromYy(yy);
+ return y === MANHOLE_START_YEAR ? "2015년 이전" : `${y}년`;
+};
+
const intCol = (name: string, headerText: string): ColumnConfig => ({
name,
fieldName: name,
@@ -98,7 +104,7 @@ function yyYearLayout(yy: number): ColumnLayoutItem {
return {
name: `year${y}`,
hideChildHeaders: false,
- header: { text: `${y}년` },
+ header: { text: manholeYearHeaderText(yy) },
items: [
{
name: `year${y}Material`,
diff --git a/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts
index 94f943f0a..61ac482da 100644
--- a/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts
+++ b/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts
@@ -211,8 +211,9 @@ export const PipeTypeStatusInpColumn = {
],
},
...yearLayoutItems,
- "rmrk",
+ 'rmrk',
'admdstCd',
+ 'pipeSeCd'
] as ColumnLayoutItem[],
rowStartNum: 2,
fixedColumns: 2,
diff --git a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts
index 832a1f338..0cf31e2b1 100644
--- a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts
+++ b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts
@@ -955,6 +955,7 @@ export const getST00020003Config = (statsYr: string | number) => {
valueType: ValueType.NUMBER,
dataFormat: "#,##0.0",
editable: false,
+ excludeFromExcelExport: true,
},
...(() => {
const yearColumns = [];
diff --git a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts
index a17043460..01b224322 100644
--- a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts
+++ b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts
@@ -134,6 +134,7 @@ export const SeweragePentrationRateTotColumn = {
editable: false,
valueType: ValueType.NUMBER,
dataFormat: '#,##0.0',
+ excludeFromExcelExport: true,
},
// 비고
diff --git a/apps/st/src/components/util/gridExcelExport.ts b/apps/st/src/components/util/gridExcelExport.ts
new file mode 100644
index 000000000..ce86d6492
--- /dev/null
+++ b/apps/st/src/components/util/gridExcelExport.ts
@@ -0,0 +1,45 @@
+import type { ColumnConfig } from "@/components/type/DataGrid.type";
+import type { GridView } from "realgrid";
+
+export type ExcelExportVisibilitySnapshot = Map;
+
+/** 엑셀보내기 직전: 제외 컬럼 숨김, 그리드용 숨김(visible:false) 컬럼은 export에만 표시 */
+export function prepareGridColumnsForExcelExport(
+ gridView: GridView,
+ columns: ColumnConfig[],
+): ExcelExportVisibilitySnapshot {
+ const excludedNames = new Set(
+ columns.filter((c) => c.excludeFromExcelExport).map((c) => c.name),
+ );
+ const snapshot = new Map();
+
+ columns.forEach((col) => {
+ const gridCol = gridView.columnByName?.(col.name);
+ if (!gridCol) return;
+
+ const wasVisible = gridCol.visible !== false;
+ snapshot.set(col.name, wasVisible);
+
+ if (excludedNames.has(col.name)) {
+ gridCol.visible = false;
+ } else if (col.visible === false) {
+ gridCol.visible = true;
+ }
+ });
+
+ return snapshot;
+}
+
+export function restoreGridColumnsAfterExcelExport(
+ gridView: GridView,
+ snapshot: ExcelExportVisibilitySnapshot,
+): void {
+ snapshot.forEach((visible, name) => {
+ const gridCol = gridView.columnByName?.(name);
+ if (gridCol) gridCol.visible = visible;
+ });
+}
+
+export function shouldPrepareColumnsForExcelExport(columns: ColumnConfig[]): boolean {
+ return columns.some((c) => c.excludeFromExcelExport || c.visible === false);
+}
diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts
index 20edb9935..fc36b73c8 100644
--- a/apps/st/src/entities/statistics/api/statistics-service.ts
+++ b/apps/st/src/entities/statistics/api/statistics-service.ts
@@ -21,6 +21,8 @@ import {
type ReviewSubmitResponse,
type SearchCondition,
type Sgg,
+ type StatiscicsExceptionResetRequest,
+ type StatiscicsExceptionResetResponse,
type StatiscicsManagementReviewHistoryRequest,
type StatiscicsManagementReviewHistoryResponse,
type StatisticsColumnGuideListResponse,
@@ -142,7 +144,7 @@ export class StatisticsApiService implements StatisticsService {
public async getImrStatus(param: string): Promise {
const response = await client.get(
- `${ENDPOINT.STATVERIFY_SERVICE.IMR}/status/${param}`,
+ `${ENDPOINT.STATVERIFY_SERVICE.IMR}/status/${param}`,
);
return response.data;
}
@@ -311,6 +313,14 @@ export class StatisticsApiService implements StatisticsService {
);
return response.data;
}
+
+ public async StatiscicsExceptionReset(param: StatiscicsExceptionResetRequest): Promise {
+ const response = await stClient.put(
+ `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/exception/reset`,
+ param,
+ );
+ return response.data;
+ }
}
export const statisticsApi = new StatisticsApiService();
diff --git a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts
index 2c82cbb0d..328818e48 100644
--- a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts
+++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts
@@ -232,7 +232,7 @@ export interface PublicSewageFacilitySaveRequest {
sggNm: string;
sewerBzmnCd: string;
userId: string;
- superAdmin: string;
+ superAdmin?: string;
/**
* 저장 요청에서 "해당없음" 처리 여부가 함께 전달되는 경우를 고려합니다.
diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts
index d6357d78f..4076e5f4f 100644
--- a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts
+++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts
@@ -112,7 +112,7 @@ export interface SeweragePipeInputRegionalFactStatusSaveRequest {
sggNm: string;
sewerBzmnCd: string;
userId: string;
- superAdmin: string;
+ superAdmin?: string;
saveList: SeweragePipeInputRegionalFactStatusSaveItem[];
}
@@ -159,7 +159,7 @@ export interface SeweragePipeInputFactInstallStatusSaveRequest {
sggNm: string;
sewerBzmnCd: string;
userId: string;
- superAdmin: string;
+ superAdmin?: string;
saveList: SeweragePipeInputFactInstallStatusSaveItem[];
}
@@ -300,7 +300,7 @@ export interface SeweragePipeInputPipeTypeStatusSaveRequest {
sggNm: string;
sewerBzmnCd: string;
userId: string;
- superAdmin: string;
+ superAdmin?: string;
saveList: SeweragePipeInputPipeTypeStatusSaveItem[];
}
@@ -372,7 +372,7 @@ export interface SeweragePipeInputPipeDiameterStatusSaveRequest {
sggNm: string;
sewerBzmnCd: string;
userId: string;
- superAdmin: string;
+ superAdmin?: string;
saveList: SeweragePipeInputPipeDiameterStatusSaveItem[];
}
@@ -470,7 +470,7 @@ export interface SeweragePipeInputManholeStatusSaveRequest {
sggNm: string;
sewerBzmnCd: string;
userId: string;
- superAdmin: string;
+ superAdmin?: string;
saveList: SeweragePipeInputManholeStatusSaveItem[];
}
diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts
index de3a9bf7b..464c391c4 100644
--- a/apps/st/src/entities/statistics/model/statistics-interface.ts
+++ b/apps/st/src/entities/statistics/model/statistics-interface.ts
@@ -462,6 +462,19 @@ export interface StatiscicsManagementReviewHistoryResponse {
data: StatiscicsManagementReviewHistoryItem[];
}
+export interface StatiscicsExceptionResetRequest {
+ statsYr: string;
+ sewerBzmnCd: string;
+ statsCd: string;
+ mdfrId: string,
+ vrfcSnList: number[]
+}
+
+export interface StatiscicsExceptionResetResponse {
+ status: string;
+ message?: string;
+}
+
export interface StatisticsService {
getSidoList(params?: { instSeCd?: string }): Promise>;
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise>;
@@ -490,4 +503,5 @@ export interface StatisticsService {
getColumnGuides(param: string): Promise;
getReviewerInfo(param: ReviewerInfoRequest): Promise;
getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise;
+ StatiscicsExceptionReset(param: StatiscicsExceptionResetRequest): Promise;
}
\ No newline at end of file
diff --git a/apps/st/src/shared/lib/stats-cd-input-navigation.ts b/apps/st/src/shared/lib/stats-cd-input-navigation.ts
index 3acdf6f0b..6a00d33ae 100644
--- a/apps/st/src/shared/lib/stats-cd-input-navigation.ts
+++ b/apps/st/src/shared/lib/stats-cd-input-navigation.ts
@@ -37,8 +37,12 @@ export const STATISTICS_INPUT_URL_PATH = {
SEWAGE_PIPE: "SewagePipeInp",
SEWAGE_PIPE_MAINTAIN: "SewagePipeMaintainInp",
SEWERAGE_PENETRATION_RATE: "SeweragePenetrationRateInp",
+ PUBLIC_SEWAGE_FACILITY: "PublicSewageFacilityInp",
} as const;
+/** `PublicSewageFacilityInp.tsx` — 공공하수처리시설 탭 */
+const PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS = ["ST00060001"] as const;
+
function normalizeSewagePipeTabKey(tabKey: string): string {
if (
tabKey === "adminAreaConnectionInp" ||
@@ -59,6 +63,10 @@ const NAV_ROWS: { urlPath: string; tabKeys: readonly string[] }[] = [
urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE,
tabKeys: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS,
},
+ {
+ urlPath: STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY,
+ tabKeys: PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS,
+ },
];
/**