[st] - fix: 오류 수정 및 누락 기능 추가
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
+1
-1
@@ -232,7 +232,7 @@ export interface PublicSewageFacilitySaveRequest {
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
superAdmin: string;
|
||||
superAdmin?: string;
|
||||
|
||||
/**
|
||||
* 저장 요청에서 "해당없음" 처리 여부가 함께 전달되는 경우를 고려합니다.
|
||||
|
||||
+5
-5
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user