[st] - fix: 오류 수정 및 누락 기능 추가

This commit is contained in:
2026-05-18 11:03:43 +09:00
parent 8384743a30
commit b508568e86
21 changed files with 1121 additions and 518 deletions
+22
View File
@@ -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;
+77 -1
View File
@@ -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<typeof onItemChecked>(onItemChecked);
const onItemAllCheckedRef =
useRef<typeof onItemAllChecked>(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<string, string | number>[]) => 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<string, unknown>).__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;
+14 -1
View File
@@ -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) {
@@ -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<string, string | number>;
type VerificationMethodCd = "NV" | "IMR" | "HB";
const VERIFICATION_METHOD_CODES: VerificationMethodCd[] = ["NV", "IMR", "HB"];
const VRFC_MTHD_NAME_MAP: Record<VerificationMethodCd, string> = {
NV: "일반",
IMR: "I-MR",
HB: "HB",
};
export type VerificationExceptionSubmitItem = {
vrfcSn: number;
prcsOpnnExpln: string;
@@ -60,9 +74,23 @@ const PRCS_YN_LABEL: Record<string, string> = {
"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<string | null>(null);
const [normalRows, setNormalRows] = useState<VerificationResultGridRow[]>([]);
const [imrRows, setImrRows] = useState<VerificationResultGridRow[]>([]);
const [normalCheckedIndices, setNormalCheckedIndices] = useState<Set<number>>(
() => new Set(),
);
const [imrCheckedIndices, setImrCheckedIndices] = useState<Set<number>>(() => new Set());
const [rows, setRows] = useState<VerificationResultGridRow[]>([]);
const [checkedIndices, setCheckedIndices] = useState<Set<number>>(() => new Set());
const normalGridRef = useRef<RealGridReact | null>(null);
const imrGridRef = useRef<RealGridReact | null>(null);
/** 메인 입력 화면처럼 편집 값은 그리드에 두고, 제출 시에만 스냅샷 조회 → Enter 시 rows 재주입 충돌 방지 */
const getNormalGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null);
const getImrGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null);
const gridRef = useRef<RealGridReact | null>(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<number>,
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<SetStateAction<VerificationResultGridRow[]>>,
pageIndex: number,
pageCount: number,
onPageIndexChange: (page: number) => void,
loading: boolean,
gridRef: MutableRefObject<RealGridReact | null>,
getGridDataRef: MutableRefObject<(() => VerificationResultGridRow[]) | null>,
onPrcsOpnnExplnCellEdited: (params: CellEditedCallbackParams) => void,
setCheckedIndices: Dispatch<SetStateAction<Set<number>>>,
) => {
if (!loading && rows.length === 0) {
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground">{emptyMessage}</p>;
}
return (
<div className="relative space-y-2">
<DataGrid
columnInfo={verificationSubmitResultColumnInfo}
rowData={rows}
setRowData={setRows as Dispatch<SetStateAction<Record<string, string | number>[]>>}
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(),
);
}}
/>
<VerificationResultHistoryPagination
pageCount={Math.max(pageCount, 1)}
pageIndex={Math.max(pageIndex, 0)}
onPageIndexChange={onPageIndexChange}
className="mx-2 mb-2"
/>
{loading && (
<div className="absolute inset-0 z-10 rounded-md [data-theme='black']:bg-transparent bg-white/40 flex items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
</div>
);
};
const titleSuffix = useMemo(() => {
if (!stableQueryContext) return "";
const sgg = stableQueryContext.sggNm?.trim() ? ` ${stableQueryContext.sggNm}` : "";
@@ -493,33 +385,56 @@ export function VerificationResultDialog({
</p>
)}
<section className="rounded-md border border-border">
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">일반검증 내역</h3>
{renderGrid(
normalRows,
setNormalRows,
normalPageIndex,
normalPageCount,
handleNormalPageChange,
normalLoading,
normalGridRef,
getNormalGridDataRef,
onNormalPrcsOpnnExplnEdited,
setNormalCheckedIndices,
)}
</section>
<section className="rounded-md border border-border">
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">I-MR 검증내역</h3>
{renderGrid(
imrRows,
setImrRows,
imrPageIndex,
imrPageCount,
handleImrPageChange,
imrLoading,
imrGridRef,
getImrGridDataRef,
onImrPrcsOpnnExplnEdited,
setImrCheckedIndices,
{!loading && rows.length === 0 ? (
<p className="p-6 text-center text-[1.3rem] text-muted-foreground">
{emptyMessage}
</p>
) : (
<div className="relative space-y-2">
<DataGrid
columnInfo={verificationSubmitResultColumnInfo}
rowData={rows}
setRowData={
setRows as Dispatch<
SetStateAction<Record<string, string | number>[]>
>
}
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(),
);
}}
/>
<VerificationResultHistoryPagination
pageCount={Math.max(pageCount, 1)}
pageIndex={Math.max(pageIndex, 0)}
onPageIndexChange={handlePageChange}
className="mx-2 mb-2"
/>
{loading && (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-white/40 [data-theme='black']:bg-transparent">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
</div>
)}
</section>
</div>
@@ -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<string, string | number>;
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<VerificationHistoryTabKey, Map<number, string>> => ({
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<RealGridReact | null>, itemIndex: number) {
try {
(gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.checkItem?.(itemIndex, false, false);
} catch {
/* ignore */
}
}
function syncCheckedMapFromGrid(
entries: VerificationResultReportItem[],
gridRef: MutableRefObject<RealGridReact | null>,
prevMap: Map<number, string>,
): Map<number, string> {
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<number, string>,
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<VerificationHistoryTabKey>("NV");
const [normalEntries, setNormalEntries] = useState<VerificationResultReportItem[]>([]);
const [imrEntries, setImrEntries] = useState<VerificationResultReportItem[]>([]);
const [hbEntries, setHbEntries] = useState<VerificationResultReportItem[]>([]);
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<VerificationHistoryTabKey, VerificationResultReportItem[]>
>({
NV: [],
IMR: [],
HB: [],
});
const [tabPages, setTabPages] = useState(initialTabPageState);
const [loadingByTab, setLoadingByTab] = useState<Record<VerificationHistoryTabKey, boolean>>({
NV: false,
IMR: false,
HB: false,
});
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [resetSubmitting, setResetSubmitting] = useState(false);
const [checkedVrfcByTab, setCheckedVrfcByTab] = useState(createEmptyCheckedVrfcByTab);
const nvGridRef = useRef<RealGridReact | null>(null);
const imrGridRef = useRef<RealGridReact | null>(null);
const hbGridRef = useRef<RealGridReact | null>(null);
const alert = useAlertStore((state) => state.setMessage);
const gridRefByTab: Record<VerificationHistoryTabKey, MutableRefObject<RealGridReact | null>> = {
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<number, string>) => 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<number, string>): 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<Record<string, string | number>[]>) => {}) as Dispatch<
SetStateAction<Record<string, string | number>[]>
>;
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 <p className="p-6 text-center text-[1.3rem] text-muted-foreground">{emptyMessage}</p>;
}
@@ -280,19 +474,57 @@ export function VerificationResultHistoryDialog({
return (
<div className="relative space-y-2">
<DataGrid
key={`verification-history-grid-${tab}`}
columnInfo={verificationResultColumnInfo}
rowData={rows}
setRowData={noopSetRows}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
showHeaderCheckbox
enableSort={false}
getGridRef={gridRef}
cellStyleCallback={verificationPrcsYnNmCellStyle}
onItemChecked={(itemIndex, checked) => {
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);
}
}}
/>
<VerificationResultHistoryPagination
pageCount={Math.max(pageCount, 1)}
pageIndex={Math.max(pageIndex, 0)}
onPageIndexChange={onPageIndexChange}
key={`verification-history-pagination-${tab}`}
pageCount={pageCount}
pageIndex={pageIndex}
onPageIndexChange={(nextPage) => handleTabPageChange(tab, nextPage)}
className="mx-2 mb-2"
/>
{loading && (
@@ -318,7 +550,12 @@ export function VerificationResultHistoryDialog({
{errorMessage}
</p>
)}
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as VerificationHistoryTabKey)} className="w-full" variant="default">
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as VerificationHistoryTabKey)}
className="w-full"
variant="default"
>
<TabsList>
{verificationHistoryTabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
@@ -326,20 +563,28 @@ export function VerificationResultHistoryDialog({
</TabsTrigger>
))}
</TabsList>
<TabsContent value="NV" className="rounded-md border border-border">
{renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
</TabsContent>
<TabsContent value="IMR" className="rounded-md border border-border">
{renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
</TabsContent>
<TabsContent value="HB" className="rounded-md border border-border">
{renderGrid(hbRows, hbPageIndex, hbPageCount, handleHbPageChange, hbLoading)}
</TabsContent>
{verificationHistoryTabs.map((tab) => (
<TabsContent
key={tab.value}
value={tab.value}
className="rounded-md border border-border"
>
{activeTab === tab.value ? renderActiveTabPanel(tab.value) : null}
</TabsContent>
))}
</Tabs>
</div>
<DialogFooter>
<Button
type="button"
color="primary"
disabled={resetSubmitting || loadingByTab[activeTab]}
onClick={() => void handleExceptionResetClick()}
>
예외 제출 취소
</Button>
<DialogClose asChild>
<Button type="button" color="gray">
<Button type="button" color="gray" disabled={resetSubmitting}>
닫기
</Button>
</DialogClose>
@@ -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<boolean> {
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<boolean> {
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;
}
@@ -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<string, BadgeProps["color"]> = {
NV: "gray",
IMR: "red",
HB: "blue",
};
const TYPE_CODE_NAME_MAP: Record<string, string> = {
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<VerificationReportTabKey>("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<VerificationReportTabKey, VerificationResultReportItem[]> = {
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[],
) => (
<section className="flex min-h-0 flex-1 flex-col rounded-sm border border-border h-[50rem]">
<div className="flex items-center justify-between border-b border-border px-3 py-2">
<Badge color={TYPE_CODE_COLOR_MAP[typeCode]} className="rounded-full px-2 py-1 text-lg">
{TYPE_CODE_NAME_MAP[typeCode]} {items.length}
</Badge>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-2">
{items.length === 0 ? (
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
항목이 없습니다.
</div>
) : (
<div className="space-y-2 tracking-tight">
{items.map((item) => (
<div
key={`${typeCode}-${item.vrfcSn}`}
role="button"
tabIndex={0}
className="flex flex-col rounded-sm border border-border p-3 cursor-pointer hover:bg-gray-100"
onClick={() => openInputFromStatsCd(item)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openInputFromStatsCd(item);
<div className="min-h-0 flex-1 overflow-y-auto p-2">
{items.length === 0 ? (
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
항목이 없습니다.
</div>
) : (
<div className="space-y-2 tracking-tight">
{items.map((item) => (
<div
key={`${typeCode}-${item.vrfcSn}`}
role="button"
tabIndex={0}
className="flex flex-col rounded-sm border border-border p-3 cursor-pointer hover:bg-gray-100"
onClick={() => openInputFromStatsCd(item)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openInputFromStatsCd(item);
}
}}
>
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
</span>
<Badge
color={
TYPE_CODE_COLOR_MAP[item.vrfcMthdCd] ??
"gray"
}
}}
>
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
</span>
<Badge
color={
TYPE_CODE_COLOR_MAP[item.vrfcMthdCd] ??
"gray"
}
className="text-sm"
>
{TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ??
item.vrfcMthdNm}
</Badge>
</div>
<p className="text-lg text-muted-foreground">
{item.rsnExpln}
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
{item.compVlCn ?? "-"}
</span>
</div>
className="text-sm"
>
{TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ??
item.vrfcMthdNm}
</Badge>
</div>
))}
</div>
)}
</div>
</section>
<p className="text-lg text-muted-foreground">
{item.rsnExpln}
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
{item.compVlCn ?? "-"}
</span>
</div>
</div>
))}
</div>
)}
</div>
);
return (
@@ -246,10 +274,42 @@ const VerificationSelectionReport = ({
</div>
)}
{!isLoading && !error && (
<div className="grid grid-rows-2 gap-6">
{renderSection("IMR", imrList)}
{renderSection("NV", nvList)}
</div>
<section className="flex min-h-0 flex-1 flex-col h-[85rem]">
<Tabs
value={activeTab}
onValueChange={(value) =>
setActiveTab(value as VerificationReportTabKey)
}
className="flex min-h-0 flex-1 flex-col h-full"
variant="default"
>
<TabsList className="grid w-full grid-cols-3 p-1">
{VERIFICATION_REPORT_TABS.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="!px-2 !py-2 text-[1.3rem] whitespace-normal"
>
{tab.label} ({listsByTab[tab.value].length})
</TabsTrigger>
))}
</TabsList>
{VERIFICATION_REPORT_TABS.map((tab) => (
<TabsContent
key={tab.value}
value={tab.value}
className="mt-0 flex min-h-0 flex-1 flex-col p-0 data-[state=inactive]:hidden"
>
{activeTab === tab.value
? renderResultList(
tab.value,
listsByTab[tab.value],
)
: null}
</TabsContent>
))}
</Tabs>
</section>
)}
</div>
</div>
@@ -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,
};
@@ -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,
};
@@ -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);
@@ -162,6 +162,8 @@ export type ColumnConfig = {
editable?: boolean; // 셀 편집 가능 여부
/** 그리드에서 컬럼 숨김 (수식용 필드 등) */
visible?: boolean;
/** true이면 엑셀보내기에서 제외 (화면에는 표시 가능) */
excludeFromExcelExport?: boolean;
/** 컬럼 너비 (px). 지정 시 ... 생략 없이 텍스트 표시에 유리 */
width?: number;
/** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */
@@ -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`,
@@ -211,8 +211,9 @@ export const PipeTypeStatusInpColumn = {
],
},
...yearLayoutItems,
"rmrk",
'rmrk',
'admdstCd',
'pipeSeCd'
] as ColumnLayoutItem[],
rowStartNum: 2,
fixedColumns: 2,
@@ -955,6 +955,7 @@ export const getST00020003Config = (statsYr: string | number) => {
valueType: ValueType.NUMBER,
dataFormat: "#,##0.0",
editable: false,
excludeFromExcelExport: true,
},
...(() => {
const yearColumns = [];
@@ -134,6 +134,7 @@ export const SeweragePentrationRateTotColumn = {
editable: false,
valueType: ValueType.NUMBER,
dataFormat: '#,##0.0',
excludeFromExcelExport: true,
},
// 비고
@@ -0,0 +1,45 @@
import type { ColumnConfig } from "@/components/type/DataGrid.type";
import type { GridView } from "realgrid";
export type ExcelExportVisibilitySnapshot = Map<string, boolean>;
/** 엑셀보내기 직전: 제외 컬럼 숨김, 그리드용 숨김(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<string, boolean>();
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);
}
@@ -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<ImrStatusResponse> {
const response = await client.get<ImrStatusResponse>(
`${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<StatiscicsExceptionResetResponse> {
const response = await stClient.put<StatiscicsExceptionResetResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/exception/reset`,
param,
);
return response.data;
}
}
export const statisticsApi = new StatisticsApiService();
@@ -232,7 +232,7 @@ export interface PublicSewageFacilitySaveRequest {
sggNm: string;
sewerBzmnCd: string;
userId: string;
superAdmin: string;
superAdmin?: string;
/**
* 저장 요청에서 "해당없음" 처리 여부가 함께 전달되는 경우를 고려합니다.
@@ -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[];
}
@@ -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<ApiResponse<SearchCondition>>;
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
@@ -490,4 +503,5 @@ export interface StatisticsService {
getColumnGuides(param: string): Promise<StatisticsColumnGuideListResponse>;
getReviewerInfo(param: ReviewerInfoRequest): Promise<ReviewerInfoResponse>;
getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise<StatiscicsManagementReviewHistoryResponse>;
StatiscicsExceptionReset(param: StatiscicsExceptionResetRequest): Promise<StatiscicsExceptionResetResponse>;
}
@@ -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,
},
];
/**