diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index a79e3ebd4..76bebc387 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -21,6 +21,16 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok { @apply !font-semibold !text-[#3372cf]; } + /* 검증결과 다이얼로그: 구분(NV·IMR·HB) */ + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-nv { + @apply !bg-[#f3f4f6] !font-semibold !text-[#4b5563]; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-imr { + @apply !bg-[#fef2f2] !font-semibold !text-[#dc2626]; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-hb { + @apply !bg-[#eff6ff] !font-semibold !text-[#2563eb]; + } /* 개별 반려: 높이/레이아웃은 RealGrid 행에 그대로 두고, 색·테두리·정렬만 버튼 느낌 */ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn { text-align: center !important; @@ -252,6 +262,18 @@ [data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok { color: #3372cf !important; } +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-nv { + background-color: #4b5563 !important; + color: #f9fafb !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-imr { + background-color: rgb(127 29 29 / 0.55) !important; + color: #fecaca !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-mthd-hb { + background-color: rgb(30 58 138 / 0.55) !important; + color: #bfdbfe !important; +} [data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn { background-color: rgb(127 29 29 / 0.42) !important; border-color: rgb(248 113 113 / 0.5) !important; diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 0ae40300a..f32d3d72d 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -20,6 +20,11 @@ import type { } from "../type/DataGrid.type"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; +import { + prepareGridColumnsForExcelExport, + restoreGridColumnsAfterExcelExport, + shouldPrepareColumnsForExcelExport, +} from "@/components/util/gridExcelExport"; import { useTabStore } from "@/store/tab-store"; // RealGrid 타입 확장 @@ -47,6 +52,23 @@ if (typeof window !== "undefined") { RealGrid.setLicenseKey(process.env.NEXT_PUBLIC_ST_REALGRID_KEY || ""); +type GridViewWithCheckApi = { + checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void; + checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void; + checkAll?: (checked: boolean) => void; +}; + +function isRowCheckedFlag(value: unknown): boolean { + return ( + value === true || + value === 1 || + value === "1" || + value === "Y" || + value === "y" || + value === "true" + ); +} + /** setDataComparer(field, comparer) - setFields 없이 숫자 정렬용. comparer(field, row1, row2) => -1|0|1 */ interface RealGridDataProviderComparer { setDataComparer?( @@ -411,6 +433,7 @@ const DataGrid = ({ const onItemCheckedRef = useRef(onItemChecked); const onItemAllCheckedRef = useRef(onItemAllChecked); + const isSyncingChecksFromRowDataRef = useRef(false); columnsRef.current = columns; rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; @@ -547,6 +570,12 @@ const DataGrid = ({ gridView.setColumnLayout?.(normalizedLayout); } + const useExcelColumnVisibilityPrep = + shouldPrepareColumnsForExcelExport(columns); + const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep + ? prepareGridColumnsForExcelExport(gridView, columns) + : undefined; + const exportBaseName = excelExportFileSuffix ? `${menuName}_${excelExportFileSuffix}` : menuName; @@ -556,8 +585,14 @@ const DataGrid = ({ target: "local", fileName: `${exportBaseName}.xlsx`, footer: "hidden", - allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함 + allColumns: !useExcelColumnVisibilityPrep, done: () => { + if (excelExportVisibilitySnapshot) { + restoreGridColumnsAfterExcelExport( + gridView, + excelExportVisibilitySnapshot, + ); + } // 헤더 원복 columns.forEach((col) => { if (col.fieldName && gridView.columnByName) { @@ -1278,6 +1313,7 @@ const DataGrid = ({ if (onItemChecked) { gridView.onItemChecked = (_grid, itemIndex, checked) => { + if (isSyncingChecksFromRowDataRef.current) return; onItemCheckedRef.current?.(itemIndex, checked); }; } else { @@ -1286,6 +1322,7 @@ const DataGrid = ({ if (onItemAllChecked) { gridView.onItemAllChecked = (_grid, checked) => { + if (isSyncingChecksFromRowDataRef.current) return; onItemAllCheckedRef.current?.(checked); }; } else { @@ -1299,6 +1336,45 @@ const DataGrid = ({ }; }, [onItemChecked, onItemAllChecked]); + useEffect(() => { + if (!gridRef.current?.gridView || !showHeaderCheckbox) return; + + const gridView = gridRef.current.gridView as GridViewWithCheckApi; + const dataProvider = gridRef.current.dataProvider as + | { setRows?: (rows: Record[]) => void } + | undefined; + + if (dataProvider?.setRows) { + dataProvider.setRows(rowData); + } + + const hasCheckedColumn = rowData.some((row) => + Object.prototype.hasOwnProperty.call(row, "__checked"), + ); + if (!hasCheckedColumn) return; + + const timer = window.setTimeout(() => { + isSyncingChecksFromRowDataRef.current = true; + try { + gridView.checkAll?.(false); + rowData.forEach((row, itemIndex) => { + if (!isRowCheckedFlag((row as Record).__checked)) return; + if (gridView.checkItem) { + gridView.checkItem(itemIndex, true, false); + return; + } + gridView.checkRow?.(itemIndex, true, false); + }); + } catch (e) { + console.warn("check sync:", e); + } finally { + isSyncingChecksFromRowDataRef.current = false; + } + }, 0); + + return () => window.clearTimeout(timer); + }, [rowData, showHeaderCheckbox]); + useEffect(() => { const gridView = gridRef.current?.gridView; if (!gridView) return; diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index a2327c11b..e4c23eb83 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -16,6 +16,11 @@ import GridTopActionButton from "./GridTopActionButton"; import SplitDataGrid from "./SplitDataGrid"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" +import { + prepareGridColumnsForExcelExport, + restoreGridColumnsAfterExcelExport, + shouldPrepareColumnsForExcelExport, +} from "@/components/util/gridExcelExport" import { useTabStore } from "@/store/tab-store" // RealGrid 타입 확장 @@ -432,13 +437,21 @@ const TabDataGrid = ({ gridView.setColumnLayout?.(normalizedLayout); } + const useExcelColumnVisibilityPrep = shouldPrepareColumnsForExcelExport(columns); + const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep + ? prepareGridColumnsForExcelExport(gridView, columns) + : undefined; + gridView.exportGrid({ type: 'excel', target: 'local', fileName: `${menuName}_${tabLabel}.xlsx`, footer: 'hidden', - allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함 + allColumns: !useExcelColumnVisibilityPrep, done: () => { + if (excelExportVisibilitySnapshot) { + restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot); + } // 헤더 원복 columns.forEach((col) => { if (col.fieldName && gridView.columnByName) { diff --git a/apps/st/src/components/module/VerificationResultDialog.tsx b/apps/st/src/components/module/VerificationResultDialog.tsx index acd460f87..7311e184d 100644 --- a/apps/st/src/components/module/VerificationResultDialog.tsx +++ b/apps/st/src/components/module/VerificationResultDialog.tsx @@ -23,7 +23,11 @@ import { } from "@common/components"; import DataGrid from "@/components/module/DataGrid"; import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; -import type { CellEditedCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; +import type { + CellEditedCallbackParams, + CellStyleCallbackParams, + ColumnInfo, +} from "@/components/type/DataGrid.type"; import { getUserInfo } from "../../../../../packages/common/components/src/api"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface"; @@ -31,6 +35,16 @@ import { useAlertStore } from "@/shared/lib"; type VerificationResultGridRow = Record; +type VerificationMethodCd = "NV" | "IMR" | "HB"; + +const VERIFICATION_METHOD_CODES: VerificationMethodCd[] = ["NV", "IMR", "HB"]; + +const VRFC_MTHD_NAME_MAP: Record = { + NV: "일반", + IMR: "I-MR", + HB: "HB", +}; + export type VerificationExceptionSubmitItem = { vrfcSn: number; prcsOpnnExpln: string; @@ -60,9 +74,23 @@ const PRCS_YN_LABEL: Record = { "3": "반려", }; -/** History와 동일 + 편집 가능한 처리의견(예외사유, API 필드명 `prcsOpnnExpln`과 동일) */ +function verificationMthdCellStyle({ + fieldName, + row, +}: CellStyleCallbackParams): string | undefined { + if (fieldName !== "vrfcMthdNm") return undefined; + const code = String(row.vrfcMthdCd ?? ""); + if (code === "IMR") return "rg-cell-vrf-mthd-imr"; + if (code === "HB") return "rg-cell-vrf-mthd-hb"; + if (code === "NV") return "rg-cell-vrf-mthd-nv"; + return undefined; +} + +/** History와 동일 + 구분(검증유형) + 편집 가능한 처리의견(예외사유) */ const verificationSubmitResultColumnInfo: ColumnInfo = { columns: [ + { name: "vrfcMthdNm", fieldName: "vrfcMthdNm", header: { text: "구분" }, editable: false }, + { name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false }, { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, { name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false }, @@ -81,6 +109,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = { }, ], columnLayout: [ + "vrfcMthdNm", "ctpvNm", "sggNm", "vrfcTrgtNm", @@ -121,7 +150,6 @@ export function VerificationResultDialog({ emptyMessage = "표시할 검증 결과가 없습니다.", onExceptionSubmitted, }: VerificationResultDialogProps) { - /** 부모가 매 렌더마다 새 객체를 넘기면 queryContext 참조만 바뀌어 loadInitial이 재실행되므로 필드 값 기준으로 안정화 */ const stableQueryContext = useMemo((): VerificationResultDialogQueryContext | null => { if (!queryContext) return null; return queryContext; @@ -133,47 +161,24 @@ export function VerificationResultDialog({ queryContext?.statsCd, ]); - const [normalPageIndex, setNormalPageIndex] = useState(0); - const [imrPageIndex, setImrPageIndex] = useState(0); - const [normalPageCount, setNormalPageCount] = useState(1); - const [imrPageCount, setImrPageCount] = useState(1); - const [normalLoading, setNormalLoading] = useState(false); - const [imrLoading, setImrLoading] = useState(false); + const [pageIndex, setPageIndex] = useState(0); + const [pageCount, setPageCount] = useState(1); + const [loading, setLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); - const [normalRows, setNormalRows] = useState([]); - const [imrRows, setImrRows] = useState([]); - const [normalCheckedIndices, setNormalCheckedIndices] = useState>( - () => new Set(), - ); - const [imrCheckedIndices, setImrCheckedIndices] = useState>(() => new Set()); + const [rows, setRows] = useState([]); + const [checkedIndices, setCheckedIndices] = useState>(() => new Set()); - const normalGridRef = useRef(null); - const imrGridRef = useRef(null); - /** 메인 입력 화면처럼 편집 값은 그리드에 두고, 제출 시에만 스냅샷 조회 → Enter 시 rows 재주입 충돌 방지 */ - const getNormalGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null); - const getImrGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null); + const gridRef = useRef(null); + const getGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null); const alert = useAlertStore((state) => state.setMessage); - const onNormalPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => { + const onPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => { if (params.fieldName !== "prcsOpnnExpln") return; const text = String(params.newValue ?? ""); const hasValue = text.trim().length > 0; - setRowCheckByItemIndex(normalGridRef, params.itemIndex, hasValue); - setNormalCheckedIndices((prev) => { - const next = new Set(prev); - if (hasValue) next.add(params.itemIndex); - else next.delete(params.itemIndex); - return next; - }); - }, []); - - const onImrPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => { - if (params.fieldName !== "prcsOpnnExpln") return; - const text = String(params.newValue ?? ""); - const hasValue = text.trim().length > 0; - setRowCheckByItemIndex(imrGridRef, params.itemIndex, hasValue); - setImrCheckedIndices((prev) => { + setRowCheckByItemIndex(gridRef, params.itemIndex, hasValue); + setCheckedIndices((prev) => { const next = new Set(prev); if (hasValue) next.add(params.itemIndex); else next.delete(params.itemIndex); @@ -182,38 +187,38 @@ export function VerificationResultDialog({ }, []); const resetState = useCallback(() => { - setNormalPageIndex(0); - setImrPageIndex(0); - setNormalPageCount(1); - setImrPageCount(1); - setNormalLoading(false); - setImrLoading(false); + setPageIndex(0); + setPageCount(1); + setLoading(false); setErrorMessage(null); - setNormalRows([]); - setImrRows([]); - setNormalCheckedIndices(new Set()); - setImrCheckedIndices(new Set()); + setRows([]); + setCheckedIndices(new Set()); }, []); - const mapItemToRow = useCallback((item: VerificationResultReportItem): VerificationResultGridRow => { - return { - vrfcSn: item.vrfcSn, - ctpvNm: item.ctpvNm, - sggNm: item.sggNm, - vrfcTrgtNm: item.vrfcTrgtNm, - rsnExpln: item.rsnExpln, - nowVlCn: item.nowVlCn ?? "-", - compVlCn: item.compVlCn ?? "-", - prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn, - pbcpAprvYn: item.pbcpAprvYn, - regDt: (item.regDt ?? "").slice(0, 16), - mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16), - prcsOpnnExpln: item.prcsOpnnExpln ?? "", - }; - }, []); + const mapItemToRow = useCallback( + (item: VerificationResultReportItem, vrfcMthdCd: VerificationMethodCd): VerificationResultGridRow => { + return { + vrfcSn: item.vrfcSn, + vrfcMthdCd, + vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm, + ctpvNm: item.ctpvNm, + sggNm: item.sggNm, + vrfcTrgtNm: item.vrfcTrgtNm, + rsnExpln: item.rsnExpln, + nowVlCn: item.nowVlCn ?? "-", + compVlCn: item.compVlCn ?? "-", + prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn, + pbcpAprvYn: item.pbcpAprvYn, + regDt: (item.regDt ?? "").slice(0, 16), + mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16), + prcsOpnnExpln: item.prcsOpnnExpln ?? "", + }; + }, + [], + ); const fetchVerificationResultPage = useCallback( - async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => { + async (vrfcMthdCd: VerificationMethodCd, page: number) => { if (!stableQueryContext) return null; const param: VerificationResultListRequest = { sewerBzmnCd: stableQueryContext.sewerBzmnCd, @@ -224,7 +229,7 @@ export function VerificationResultDialog({ pbcpAprvYn: "N", vrfcMthdCd, prcsYnList: "0", - page: pageIndex + 1, + page: page + 1, size: 10000, }; return statisticsApi.getVerificationResultList(param); @@ -232,81 +237,56 @@ export function VerificationResultDialog({ [stableQueryContext], ); - const loadInitial = useCallback(async () => { - if (!stableQueryContext) { - setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다."); - return; - } - setErrorMessage(null); - setNormalLoading(true); - setImrLoading(true); - setNormalPageIndex(0); - setImrPageIndex(0); - try { - const [nvRes, imrRes] = await Promise.all([ - fetchVerificationResultPage("NV", 0), - fetchVerificationResultPage("IMR", 0), - ]); - const nvList = nvRes?.data?.verifyResultList ?? []; - const imrList = imrRes?.data?.verifyResultList ?? []; - setNormalRows(nvList.map(mapItemToRow)); - setImrRows(imrList.map(mapItemToRow)); - setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1); - setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1); - } catch { - setNormalRows([]); - setImrRows([]); - setNormalPageCount(1); - setImrPageCount(1); - setErrorMessage("검증 결과 조회 중 오류가 발생했습니다."); - } finally { - setNormalLoading(false); - setImrLoading(false); - setNormalCheckedIndices(new Set()); - setImrCheckedIndices(new Set()); - } - }, [fetchVerificationResultPage, mapItemToRow, stableQueryContext]); - - const handleNormalPageChange = useCallback( - async (pageIndex: number) => { - if (pageIndex === normalPageIndex || !stableQueryContext) return; + const loadPage = useCallback( + async (nextPageIndex: number) => { + if (!stableQueryContext) { + setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다."); + return; + } setErrorMessage(null); - setNormalLoading(true); + setLoading(true); try { - const response = await fetchVerificationResultPage("NV", pageIndex); - const list = response?.data?.verifyResultList ?? []; - setNormalRows(list.map(mapItemToRow)); - setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1); - setNormalPageIndex(pageIndex); + const responses = await Promise.all( + VERIFICATION_METHOD_CODES.map((code) => + fetchVerificationResultPage(code, nextPageIndex), + ), + ); + const merged: VerificationResultGridRow[] = []; + let maxPageCount = 1; + VERIFICATION_METHOD_CODES.forEach((code, index) => { + const response = responses[index]; + const list = response?.data?.verifyResultList ?? []; + merged.push(...list.map((item) => mapItemToRow(item, code))); + maxPageCount = Math.max( + maxPageCount, + response?.data?.pageInfo?.totalPages ?? 1, + ); + }); + setRows(merged); + setPageCount(maxPageCount); + setPageIndex(nextPageIndex); } catch { - setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다."); + setRows([]); + setPageCount(1); + setErrorMessage("검증 결과 조회 중 오류가 발생했습니다."); } finally { - setNormalLoading(false); - setNormalCheckedIndices(new Set()); + setLoading(false); + setCheckedIndices(new Set()); } }, - [fetchVerificationResultPage, mapItemToRow, normalPageIndex, stableQueryContext], + [fetchVerificationResultPage, mapItemToRow, stableQueryContext], ); - const handleImrPageChange = useCallback( - async (pageIndex: number) => { - if (pageIndex === imrPageIndex || !stableQueryContext) return; - setErrorMessage(null); - setImrLoading(true); - try { - const response = await fetchVerificationResultPage("IMR", pageIndex); - const list = response?.data?.verifyResultList ?? []; - setImrRows(list.map(mapItemToRow)); - setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1); - setImrPageIndex(pageIndex); - } catch { - setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다."); - } finally { - setImrLoading(false); - setImrCheckedIndices(new Set()); - } + const loadInitial = useCallback(async () => { + await loadPage(0); + }, [loadPage]); + + const handlePageChange = useCallback( + async (nextPageIndex: number) => { + if (nextPageIndex === pageIndex || !stableQueryContext) return; + await loadPage(nextPageIndex); }, - [fetchVerificationResultPage, mapItemToRow, imrPageIndex, stableQueryContext], + [loadPage, pageIndex, stableQueryContext], ); useEffect(() => { @@ -317,58 +297,32 @@ export function VerificationResultDialog({ void loadInitial(); }, [loadInitial, open, resetState]); - /** 조회된 행에 예외사유 텍스트가 있으면 체크바·체크 인덱스와 맞춤 */ useEffect(() => { - if (normalLoading) return; + if (loading) return; const id = requestAnimationFrame(() => { - setNormalCheckedIndices((prev) => { + setCheckedIndices((prev) => { const next = new Set(prev); - normalRows.forEach((row, itemIndex) => { + rows.forEach((row, itemIndex) => { const has = String(row.prcsOpnnExpln ?? "").trim().length > 0; - setRowCheckByItemIndex(normalGridRef, itemIndex, has); + setRowCheckByItemIndex(gridRef, itemIndex, has); if (has) next.add(itemIndex); }); return next; }); }); return () => cancelAnimationFrame(id); - }, [normalRows, normalLoading]); - - useEffect(() => { - if (imrLoading) return; - const id = requestAnimationFrame(() => { - setImrCheckedIndices((prev) => { - const next = new Set(prev); - imrRows.forEach((row, itemIndex) => { - const has = String(row.prcsOpnnExpln ?? "").trim().length > 0; - setRowCheckByItemIndex(imrGridRef, itemIndex, has); - if (has) next.add(itemIndex); - }); - return next; - }); - }); - return () => cancelAnimationFrame(id); - }, [imrRows, imrLoading]); + }, [rows, loading]); const handleExceptionSubmitClick = useCallback(async () => { - const nvGrid = getNormalGridDataRef.current?.() ?? []; - const imGrid = getImrGridDataRef.current?.() ?? []; + const gridRows = getGridDataRef.current?.() ?? []; const payload: VerificationExceptionSubmitItem[] = []; - const pushChecked = ( - indices: Set, - gridRows: VerificationResultGridRow[], - idRows: VerificationResultGridRow[], - ) => { - indices.forEach((idx) => { - const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim(); - if (!prcsOpnnExpln) return; - const vrfcSn = Number(idRows[idx]?.vrfcSn); - if (!Number.isFinite(vrfcSn)) return; - payload.push({ vrfcSn, prcsOpnnExpln }); - }); - }; - pushChecked(normalCheckedIndices, nvGrid, normalRows); - pushChecked(imrCheckedIndices, imGrid, imrRows); + checkedIndices.forEach((idx) => { + const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim(); + if (!prcsOpnnExpln) return; + const vrfcSn = Number(rows[idx]?.vrfcSn); + if (!Number.isFinite(vrfcSn)) return; + payload.push({ vrfcSn, prcsOpnnExpln }); + }); if (payload.length === 0) { alert("작성된 예외 제출 항목이 없습니다."); return; @@ -402,75 +356,13 @@ export function VerificationResultDialog({ } }, [ alert, - imrCheckedIndices, - imrRows, - normalCheckedIndices, - normalRows, + checkedIndices, + rows, onExceptionSubmitted, onOpenChange, stableQueryContext, ]); - const renderGrid = ( - rows: VerificationResultGridRow[], - setRows: Dispatch>, - pageIndex: number, - pageCount: number, - onPageIndexChange: (page: number) => void, - loading: boolean, - gridRef: MutableRefObject, - getGridDataRef: MutableRefObject<(() => VerificationResultGridRow[]) | null>, - onPrcsOpnnExplnCellEdited: (params: CellEditedCallbackParams) => void, - setCheckedIndices: Dispatch>>, - ) => { - if (!loading && rows.length === 0) { - return

{emptyMessage}

; - } - - return ( -
- []>>} - isExcelImport={false} - isExcelExport={false} - showHeaderCheckbox - enableSort={false} - getGridRef={gridRef} - getGridDataRef={getGridDataRef} - onCellEdited={onPrcsOpnnExplnCellEdited} - onItemChecked={(itemIndex, checked) => { - setCheckedIndices((prev) => { - const next = new Set(prev); - if (checked) next.add(itemIndex); - else next.delete(itemIndex); - return next; - }); - }} - onItemAllChecked={(checked) => { - setCheckedIndices( - checked ? new Set(rows.map((_, i) => i)) : new Set(), - ); - }} - /> - - {loading && ( -
-
- -
-
- )} -
- ); - }; - const titleSuffix = useMemo(() => { if (!stableQueryContext) return ""; const sgg = stableQueryContext.sggNm?.trim() ? ` ${stableQueryContext.sggNm}` : ""; @@ -493,33 +385,56 @@ export function VerificationResultDialog({

)}
-

일반검증 내역

- {renderGrid( - normalRows, - setNormalRows, - normalPageIndex, - normalPageCount, - handleNormalPageChange, - normalLoading, - normalGridRef, - getNormalGridDataRef, - onNormalPrcsOpnnExplnEdited, - setNormalCheckedIndices, - )} -
-
-

I-MR 검증내역

- {renderGrid( - imrRows, - setImrRows, - imrPageIndex, - imrPageCount, - handleImrPageChange, - imrLoading, - imrGridRef, - getImrGridDataRef, - onImrPrcsOpnnExplnEdited, - setImrCheckedIndices, + {!loading && rows.length === 0 ? ( +

+ {emptyMessage} +

+ ) : ( +
+ []> + > + } + isExcelImport={false} + isExcelExport={false} + showHeaderCheckbox + enableSort={false} + getGridRef={gridRef} + getGridDataRef={getGridDataRef} + onCellEdited={onPrcsOpnnExplnEdited} + cellStyleCallback={verificationMthdCellStyle} + onItemChecked={(itemIndex, checked) => { + setCheckedIndices((prev) => { + const next = new Set(prev); + if (checked) next.add(itemIndex); + else next.delete(itemIndex); + return next; + }); + }} + onItemAllChecked={(checked) => { + setCheckedIndices( + checked ? new Set(rows.map((_, i) => i)) : new Set(), + ); + }} + /> + + {loading && ( +
+
+ +
+
+ )} +
)}
diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx index 342a8c414..cd599665b 100644 --- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -1,7 +1,18 @@ "use client"; -import { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; +import { + type Dispatch, + type MutableRefObject, + type SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import type { RealGridReact } from "realgrid-react"; +import { getUserInfo } from "../../../../../packages/common/components/src/api"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface"; import { @@ -22,6 +33,7 @@ import { import DataGrid from "@/components/module/DataGrid"; import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; +import { useAlertStore } from "@/shared/lib"; type VerificationResultGridRow = Record; type VerificationHistoryTabKey = "NV" | "IMR" | "HB"; @@ -32,6 +44,24 @@ const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string { value: "HB", label: "HB 검증내역" }, ]; +const PAGE_SIZE = 30; + +/** vrfcSn → prcsYn (페이지 이동 후에도 취소 가능 여부 검증용) */ +const createEmptyCheckedVrfcByTab = (): Record> => ({ + NV: new Map(), + IMR: new Map(), + HB: new Map(), +}); + +const initialTabPageState = (): Record< + VerificationHistoryTabKey, + { pageIndex: number; pageCount: number } +> => ({ + NV: { pageIndex: 0, pageCount: 1 }, + IMR: { pageIndex: 0, pageCount: 1 }, + HB: { pageIndex: 0, pageCount: 1 }, +}); + function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined { if (raw === undefined || raw === null) return undefined; if (typeof raw === "number" && Number.isFinite(raw)) return raw; @@ -41,7 +71,62 @@ function prcsYnCodeToNumber(raw: string | number | undefined): number | undefine return Number.isFinite(n) ? n : undefined; } -/** prcsYn 0·4 → 빨간색, 그 외 → 초록색 (처리여부 컬럼) */ +/** 예외 제출 취소 가능: prcsYn = 2 (예외처리) */ +function canExceptionReset(prcsYn: string | number | undefined): boolean { + return prcsYnCodeToNumber(prcsYn) === 2; +} + +const EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE = + "예외 제출 취소는 처리여부가 사용자예외처리인 항목만 선택할 수 있습니다."; + +type GridViewWithCheckApi = { + getCheckedRows?: (allItems?: boolean, visibleOnly?: boolean, exclusive?: boolean) => number[]; + checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void; +}; + +function uncheckGridRow(gridRef: MutableRefObject, itemIndex: number) { + try { + (gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.checkItem?.(itemIndex, false, false); + } catch { + /* ignore */ + } +} + +function syncCheckedMapFromGrid( + entries: VerificationResultReportItem[], + gridRef: MutableRefObject, + prevMap: Map, +): Map { + const next = new Map(prevMap); + for (const entry of entries) { + const sn = Number(entry.vrfcSn); + if (Number.isFinite(sn)) next.delete(sn); + } + const indices = + (gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.getCheckedRows?.(true, false, true) ?? + []; + for (const itemIndex of indices) { + const entry = entries[itemIndex]; + if (!entry) continue; + const sn = Number(entry.vrfcSn); + if (!Number.isFinite(sn)) continue; + next.set(sn, String(entry.prcsYn ?? "")); + } + return next; +} + +function selectionHasNonExceptionResetAllowed( + map: Map, + currentEntries: VerificationResultReportItem[], +): boolean { + for (const [vrfcSn, storedPrcsYn] of map) { + const onPage = currentEntries.find((e) => Number(e.vrfcSn) === vrfcSn); + const prcsYn = onPage ? onPage.prcsYn : storedPrcsYn; + if (!canExceptionReset(prcsYn)) return true; + } + return false; +} + function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined { if (fieldName !== "prcsYnNm") return undefined; const code = prcsYnCodeToNumber(row.prcsYn); @@ -62,8 +147,22 @@ const verificationResultColumnInfo: ColumnInfo = { { name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false }, { name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false }, { name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false }, + { name: "vrfcSn", fieldName: "vrfcSn", header: { text: "검증일련번호" }, editable: false, visible: false }, + ], + columnLayout: [ + "ctpvNm", + "sggNm", + "vrfcTrgtNm", + "rsnExpln", + "nowVlCn", + "compVlCn", + "prcsYnNm", + "pbcpAprvYn", + "regDt", + "mdfcnDt", + "prcsYn", + "vrfcSn", ], - columnLayout: ["ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYnNm", "pbcpAprvYn", "regDt", "mdfcnDt", "prcsYn"], }; export interface VerificationResultHistoryDialogProps { @@ -87,152 +186,168 @@ export function VerificationResultHistoryDialog({ title = "검증 결과 내역", emptyMessage = "표시할 검증 결과가 없습니다.", }: VerificationResultHistoryDialogProps) { + const stableQueryContext = useMemo(() => { + if (!queryContext) return null; + return queryContext; + }, [ + queryContext?.sewerBzmnCd, + queryContext?.ctpvNm, + queryContext?.sggNm, + queryContext?.statsYr, + queryContext?.statsCd, + ]); + const [activeTab, setActiveTab] = useState("NV"); - const [normalEntries, setNormalEntries] = useState([]); - const [imrEntries, setImrEntries] = useState([]); - const [hbEntries, setHbEntries] = useState([]); - const [normalPageIndex, setNormalPageIndex] = useState(0); - const [imrPageIndex, setImrPageIndex] = useState(0); - const [hbPageIndex, setHbPageIndex] = useState(0); - const [normalPageCount, setNormalPageCount] = useState(1); - const [imrPageCount, setImrPageCount] = useState(1); - const [hbPageCount, setHbPageCount] = useState(1); - const [normalLoading, setNormalLoading] = useState(false); - const [imrLoading, setImrLoading] = useState(false); - const [hbLoading, setHbLoading] = useState(false); + const [entriesByTab, setEntriesByTab] = useState< + Record + >({ + NV: [], + IMR: [], + HB: [], + }); + const [tabPages, setTabPages] = useState(initialTabPageState); + const [loadingByTab, setLoadingByTab] = useState>({ + NV: false, + IMR: false, + HB: false, + }); const [errorMessage, setErrorMessage] = useState(null); + const [resetSubmitting, setResetSubmitting] = useState(false); + const [checkedVrfcByTab, setCheckedVrfcByTab] = useState(createEmptyCheckedVrfcByTab); + + const nvGridRef = useRef(null); + const imrGridRef = useRef(null); + const hbGridRef = useRef(null); + const alert = useAlertStore((state) => state.setMessage); + + const gridRefByTab: Record> = { + NV: nvGridRef, + IMR: imrGridRef, + HB: hbGridRef, + }; + const checkedVrfcByTabRef = useRef(checkedVrfcByTab); + checkedVrfcByTabRef.current = checkedVrfcByTab; const resetState = useCallback(() => { setActiveTab("NV"); - setNormalEntries([]); - setImrEntries([]); - setHbEntries([]); - setNormalPageIndex(0); - setImrPageIndex(0); - setHbPageIndex(0); - setNormalPageCount(1); - setImrPageCount(1); - setHbPageCount(1); - setNormalLoading(false); - setImrLoading(false); - setHbLoading(false); + setEntriesByTab({ NV: [], IMR: [], HB: [] }); + setTabPages(initialTabPageState()); + setLoadingByTab({ NV: false, IMR: false, HB: false }); setErrorMessage(null); + setResetSubmitting(false); + setCheckedVrfcByTab(createEmptyCheckedVrfcByTab()); }, []); const fetchVerificationResultPage = useCallback( async (vrfcMthdCd: VerificationHistoryTabKey, pageIndex: number) => { - if (!queryContext) return null; + if (!stableQueryContext) return null; const param: VerificationResultListRequest = { - sewerBzmnCd: queryContext.sewerBzmnCd, - ctpvNm: queryContext.ctpvNm, - sggNm: queryContext.sggNm, - statsYr: queryContext.statsYr, - statsCd: queryContext.statsCd, - pbcpAprvYn: 'N', + sewerBzmnCd: stableQueryContext.sewerBzmnCd, + ctpvNm: stableQueryContext.ctpvNm, + sggNm: stableQueryContext.sggNm, + statsYr: stableQueryContext.statsYr, + statsCd: stableQueryContext.statsCd, + pbcpAprvYn: "N", vrfcMthdCd, prcsYnList: "0,2,3", page: pageIndex + 1, - size: 30, + size: PAGE_SIZE, }; return statisticsApi.getVerificationResultList(param); }, - [queryContext], + [stableQueryContext], + ); + + const applyPageResponse = useCallback( + (tab: VerificationHistoryTabKey, pageIndex: number, list: VerificationResultReportItem[], totalPages: number) => { + const pageCount = Math.max(totalPages, 1); + const clampedIndex = Math.min(Math.max(pageIndex, 0), pageCount - 1); + setEntriesByTab((prev) => ({ ...prev, [tab]: list })); + setTabPages((prev) => ({ + ...prev, + [tab]: { pageIndex: clampedIndex, pageCount }, + })); + }, + [], + ); + + const loadTabPage = useCallback( + async (tab: VerificationHistoryTabKey, pageIndex: number) => { + if (!stableQueryContext) { + setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다."); + return; + } + setErrorMessage(null); + setLoadingByTab((prev) => ({ ...prev, [tab]: true })); + try { + const response = await fetchVerificationResultPage(tab, pageIndex); + const list = response?.data?.verifyResultList ?? []; + const totalPages = response?.data?.pageInfo?.totalPages ?? 1; + const apiCurrentPage = response?.data?.pageInfo?.currentPage; + const resolvedPageIndex = + apiCurrentPage != null && Number.isFinite(apiCurrentPage) + ? Math.max(0, Number(apiCurrentPage) - 1) + : pageIndex; + applyPageResponse(tab, resolvedPageIndex, list, totalPages); + } catch { + const tabLabel = + verificationHistoryTabs.find((t) => t.value === tab)?.label ?? tab; + setErrorMessage(`${tabLabel} 조회 중 오류가 발생했습니다.`); + } finally { + setLoadingByTab((prev) => ({ ...prev, [tab]: false })); + } + }, + [applyPageResponse, fetchVerificationResultPage, stableQueryContext], ); const loadInitial = useCallback(async () => { - if (!queryContext) { + if (!stableQueryContext) { setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다."); return; } setErrorMessage(null); - setNormalLoading(true); - setImrLoading(true); - setHbLoading(true); - setNormalPageIndex(0); - setImrPageIndex(0); - setHbPageIndex(0); + setTabPages(initialTabPageState()); + setLoadingByTab({ NV: true, IMR: true, HB: true }); try { - const [nvRes, imrRes, hbRes] = await Promise.all([ - fetchVerificationResultPage("NV", 0), - fetchVerificationResultPage("IMR", 0), - fetchVerificationResultPage("HB", 0), - ]); - setNormalEntries(nvRes?.data?.verifyResultList ?? []); - setImrEntries(imrRes?.data?.verifyResultList ?? []); - setHbEntries(hbRes?.data?.verifyResultList ?? []); - setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1); - setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1); - setHbPageCount(hbRes?.data?.pageInfo?.totalPages ?? 1); + const results = await Promise.all( + verificationHistoryTabs.map((tab) => + fetchVerificationResultPage(tab.value, 0), + ), + ); + setEntriesByTab({ + NV: results[0]?.data?.verifyResultList ?? [], + IMR: results[1]?.data?.verifyResultList ?? [], + HB: results[2]?.data?.verifyResultList ?? [], + }); + setTabPages({ + NV: { + pageIndex: 0, + pageCount: Math.max(results[0]?.data?.pageInfo?.totalPages ?? 1, 1), + }, + IMR: { + pageIndex: 0, + pageCount: Math.max(results[1]?.data?.pageInfo?.totalPages ?? 1, 1), + }, + HB: { + pageIndex: 0, + pageCount: Math.max(results[2]?.data?.pageInfo?.totalPages ?? 1, 1), + }, + }); } catch { - setNormalEntries([]); - setImrEntries([]); - setHbEntries([]); - setNormalPageCount(1); - setImrPageCount(1); - setHbPageCount(1); + setEntriesByTab({ NV: [], IMR: [], HB: [] }); + setTabPages(initialTabPageState()); setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다."); } finally { - setNormalLoading(false); - setImrLoading(false); - setHbLoading(false); + setLoadingByTab({ NV: false, IMR: false, HB: false }); } - }, [fetchVerificationResultPage, queryContext]); + }, [fetchVerificationResultPage, stableQueryContext]); - const handleNormalPageChange = useCallback( - async (pageIndex: number) => { - if (pageIndex === normalPageIndex || !queryContext) return; - setErrorMessage(null); - setNormalLoading(true); - try { - const response = await fetchVerificationResultPage("NV", pageIndex); - setNormalEntries(response?.data?.verifyResultList ?? []); - setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1); - setNormalPageIndex(pageIndex); - } catch { - setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다."); - } finally { - setNormalLoading(false); - } + const handleTabPageChange = useCallback( + (tab: VerificationHistoryTabKey, pageIndex: number) => { + if (pageIndex === tabPages[tab].pageIndex) return; + void loadTabPage(tab, pageIndex); }, - [fetchVerificationResultPage, normalPageIndex, queryContext], - ); - - const handleImrPageChange = useCallback( - async (pageIndex: number) => { - if (pageIndex === imrPageIndex || !queryContext) return; - setErrorMessage(null); - setImrLoading(true); - try { - const response = await fetchVerificationResultPage("IMR", pageIndex); - setImrEntries(response?.data?.verifyResultList ?? []); - setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1); - setImrPageIndex(pageIndex); - } catch { - setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다."); - } finally { - setImrLoading(false); - } - }, - [fetchVerificationResultPage, imrPageIndex, queryContext], - ); - - const handleHbPageChange = useCallback( - async (pageIndex: number) => { - if (pageIndex === hbPageIndex || !queryContext) return; - setErrorMessage(null); - setHbLoading(true); - try { - const response = await fetchVerificationResultPage("HB", pageIndex); - setHbEntries(response?.data?.verifyResultList ?? []); - setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1); - setHbPageIndex(pageIndex); - } catch { - setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다."); - } finally { - setHbLoading(false); - } - }, - [fetchVerificationResultPage, hbPageIndex, queryContext], + [loadTabPage, tabPages], ); useEffect(() => { @@ -243,36 +358,115 @@ export function VerificationResultHistoryDialog({ void loadInitial(); }, [loadInitial, open, resetState]); - const mapRows = (entries: VerificationResultReportItem[]): VerificationResultGridRow[] => - entries.map((item) => ({ - ctpvNm: item.ctpvNm, - sggNm: item.sggNm, - vrfcTrgtNm: item.vrfcTrgtNm, - rsnExpln: item.rsnExpln, - nowVlCn: item.nowVlCn ?? "-", - compVlCn: item.compVlCn ?? "-", - prcsYnNm: item.prcsYnNm ?? "-", - prcsYn: item.prcsYn ?? "", - pbcpAprvYn: item.pbcpAprvYn, - regDt: (item.regDt ?? "").slice(0, 16), - mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16), - })); + const updateCheckedVrfcForTab = useCallback( + (tab: VerificationHistoryTabKey, updater: (map: Map) => void) => { + setCheckedVrfcByTab((prev) => { + const nextMap = new Map(prev[tab]); + updater(nextMap); + return { ...prev, [tab]: nextMap }; + }); + }, + [], + ); - const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]); - const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]); - const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]); + const mapRows = useCallback( + (entries: VerificationResultReportItem[], checkedMap: Map): VerificationResultGridRow[] => + entries.map((item) => { + const vrfcSn = Number(item.vrfcSn); + const isChecked = Number.isFinite(vrfcSn) && checkedMap.has(vrfcSn); + return { + vrfcSn: item.vrfcSn, + __checked: isChecked ? 1 : 0, + ctpvNm: item.ctpvNm, + sggNm: item.sggNm, + vrfcTrgtNm: item.vrfcTrgtNm, + rsnExpln: item.rsnExpln, + nowVlCn: item.nowVlCn ?? "-", + compVlCn: item.compVlCn ?? "-", + prcsYnNm: item.prcsYnNm ?? "-", + prcsYn: item.prcsYn ?? "", + pbcpAprvYn: item.pbcpAprvYn, + regDt: (item.regDt ?? "").slice(0, 16), + mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16), + }; + }), + [], + ); + + /** 페이지(entries) 변경 시에만 __checked 반영 — 클릭마다 rowData를 바꾸지 않아 체크 UI가 안정적 */ + const rowsByTab = useMemo( + () => { + const checked = checkedVrfcByTabRef.current; + return { + NV: mapRows(entriesByTab.NV, checked.NV), + IMR: mapRows(entriesByTab.IMR, checked.IMR), + HB: mapRows(entriesByTab.HB, checked.HB), + }; + }, + [entriesByTab, mapRows], + ); const noopSetRows = ((_: SetStateAction[]>) => {}) as Dispatch< SetStateAction[]> >; - const renderGrid = ( - rows: VerificationResultGridRow[], - pageIndex: number, - pageCount: number, - onPageIndexChange: (page: number) => void, - loading: boolean, - ) => { + const handleExceptionResetClick = useCallback(async () => { + if (!stableQueryContext) { + alert("조회 조건이 없습니다."); + return; + } + const tab = activeTab; + const currentEntries = entriesByTab[tab]; + const syncedMap = syncCheckedMapFromGrid(currentEntries, gridRefByTab[tab], checkedVrfcByTab[tab]); + if (syncedMap.size === 0) { + alert("예외 제출 취소할 항목을 체크해주세요."); + return; + } + if (selectionHasNonExceptionResetAllowed(syncedMap, currentEntries)) { + alert(EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE); + return; + } + setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: syncedMap })); + const vrfcSnList = Array.from(syncedMap.keys()); + let mdfrId: string; + try { + const userInfo = await getUserInfo(); + mdfrId = userInfo.userUnqId; + } catch { + alert("사용자 정보를 불러오지 못했습니다."); + return; + } + setResetSubmitting(true); + setErrorMessage(null); + try { + const response = await statisticsApi.StatiscicsExceptionReset({ + statsYr: stableQueryContext.statsYr, + sewerBzmnCd: stableQueryContext.sewerBzmnCd, + statsCd: stableQueryContext.statsCd, + mdfrId, + vrfcSnList, + }); + if (response.status !== "success") { + alert(response.message ?? "예외 제출 취소 처리 중 오류가 발생했습니다."); + return; + } + alert("예외 제출 취소 처리되었습니다."); + setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: new Map() })); + await loadTabPage(tab, tabPages[tab].pageIndex); + } catch { + alert("예외 제출 취소 처리 중 오류가 발생했습니다."); + } finally { + setResetSubmitting(false); + } + }, [activeTab, alert, checkedVrfcByTab, entriesByTab, loadTabPage, stableQueryContext, tabPages]); + + const renderActiveTabPanel = (tab: VerificationHistoryTabKey) => { + const entries = entriesByTab[tab]; + const rows = rowsByTab[tab]; + const { pageIndex, pageCount } = tabPages[tab]; + const loading = loadingByTab[tab]; + const gridRef = gridRefByTab[tab]; + if (!loading && rows.length === 0) { return

{emptyMessage}

; } @@ -280,19 +474,57 @@ export function VerificationResultHistoryDialog({ return (
{ + const entry = entries[itemIndex]; + if (!entry) return; + const vrfcSn = Number(entry.vrfcSn); + if (!Number.isFinite(vrfcSn)) return; + if (checked && !canExceptionReset(entry.prcsYn)) { + alert(EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE); + uncheckGridRow(gridRef, itemIndex); + return; + } + updateCheckedVrfcForTab(tab, (map) => { + if (checked) map.set(vrfcSn, String(entry.prcsYn ?? "")); + else map.delete(vrfcSn); + }); + }} + onItemAllChecked={(checked) => { + updateCheckedVrfcForTab(tab, (map) => { + entries.forEach((entry, itemIndex) => { + const vrfcSn = Number(entry.vrfcSn); + if (!Number.isFinite(vrfcSn)) return; + if (!checked) { + map.delete(vrfcSn); + return; + } + if (canExceptionReset(entry.prcsYn)) { + map.set(vrfcSn, String(entry.prcsYn ?? "")); + } else { + uncheckGridRow(gridRef, itemIndex); + } + }); + }); + if (checked && entries.some((e) => !canExceptionReset(e.prcsYn))) { + alert(EXCEPTION_RESET_ONLY_PRCS_YN_2_MESSAGE); + } + }} /> handleTabPageChange(tab, nextPage)} className="mx-2 mb-2" /> {loading && ( @@ -318,7 +550,12 @@ export function VerificationResultHistoryDialog({ {errorMessage}

)} - setActiveTab(value as VerificationHistoryTabKey)} className="w-full" variant="default"> + setActiveTab(value as VerificationHistoryTabKey)} + className="w-full" + variant="default" + > {verificationHistoryTabs.map((tab) => ( @@ -326,20 +563,28 @@ export function VerificationResultHistoryDialog({ ))} - - {renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)} - - - {renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)} - - - {renderGrid(hbRows, hbPageIndex, hbPageCount, handleHbPageChange, hbLoading)} - + {verificationHistoryTabs.map((tab) => ( + + {activeTab === tab.value ? renderActiveTabPanel(tab.value) : null} + + ))}
+ - diff --git a/apps/st/src/components/module/post-submit-verification-results.ts b/apps/st/src/components/module/post-submit-verification-results.ts new file mode 100644 index 000000000..a46ad79cb --- /dev/null +++ b/apps/st/src/components/module/post-submit-verification-results.ts @@ -0,0 +1,85 @@ +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { VerificationResultListResponse } from "@/entities/statistics/model/statistics-interface"; + +import type { VerificationResultDialogQueryContext } from "./VerificationResultDialog"; + +const POST_SUBMIT_VRFC_MTHD_CODES = ["NV", "IMR", "HB"] as const; + +export const POST_SUBMIT_COMPLETE_MESSAGE = "제출 완료되었습니다."; + +export type OpenVerificationResultDialogOptions = { + notify?: (message: string) => void; + /** 미통과 검증 결과가 없을 때 제출 완료 알림 (기본 true, IMR 오류 catch 등에서는 false) */ + notifySubmitComplete?: boolean; + submitCompleteMessage?: string; +}; + +function responseHasVerificationResults(response: VerificationResultListResponse | null): boolean { + const total = response?.data?.pageInfo?.totalElements; + if (typeof total === "number" && total > 0) return true; + const list = response?.data?.verifyResultList; + return Array.isArray(list) && list.length > 0; +} + +/** + * 제출 직후 `VerificationResultDialog`에 표시할 미통과(prcsYn=0) 검증 결과가 + * 일반검증(NV)·I-MR(IMR)·HB 중 하나라도 있는지 조회합니다. + */ +export async function hasAnyPostSubmitVerificationResult( + ctx: VerificationResultDialogQueryContext, +): Promise { + const base = { + sewerBzmnCd: ctx.sewerBzmnCd, + ctpvNm: ctx.ctpvNm, + sggNm: ctx.sggNm, + statsYr: ctx.statsYr, + statsCd: ctx.statsCd, + pbcpAprvYn: "N" as const, + prcsYnList: "0", + page: 1, + size: 1, + }; + + const responses = await Promise.all( + POST_SUBMIT_VRFC_MTHD_CODES.map((vrfcMthdCd) => + statisticsApi.getVerificationResultList({ ...base, vrfcMthdCd }), + ), + ); + + return responses.some(responseHasVerificationResults); +} + +/** + * 미통과 검증 결과가 있으면 다이얼로그를 열고, 없으면 제출 완료 알림을 표시합니다. + * @returns 다이얼로그를 열었으면 true + */ +export async function openVerificationResultDialogIfNeeded( + ctx: VerificationResultDialogQueryContext | null | undefined, + setOpen: (open: boolean) => void, + options?: OpenVerificationResultDialogOptions, +): Promise { + const { + notify, + notifySubmitComplete = true, + submitCompleteMessage = POST_SUBMIT_COMPLETE_MESSAGE, + } = options ?? {}; + + const notifyComplete = () => { + if (notifySubmitComplete && notify) { + notify(submitCompleteMessage); + } + }; + + if (!ctx) { + notifyComplete(); + return false; + } + + if (await hasAnyPostSubmitVerificationResult(ctx)) { + setOpen(true); + return true; + } + + notifyComplete(); + return false; +} diff --git a/apps/st/src/components/statistics/VerificationSelectionReport.tsx b/apps/st/src/components/statistics/VerificationSelectionReport.tsx index ec81f97c3..47a6c8401 100644 --- a/apps/st/src/components/statistics/VerificationSelectionReport.tsx +++ b/apps/st/src/components/statistics/VerificationSelectionReport.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { useQueries } from "@tanstack/react-query"; import { useTabStore as useGridTabStore } from "@rsone/components"; @@ -19,16 +19,30 @@ import { Badge, type BadgeProps, LoadingWithAnimation, + Tabs, + TabsContent, + TabsList, + TabsTrigger, } from "@common/components"; +type VerificationReportTabKey = "NV" | "IMR" | "HB"; + +const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [ + { value: "NV", label: "일반" }, + { value: "IMR", label: "I-MR" }, + { value: "HB", label: "HB" }, +]; + const TYPE_CODE_COLOR_MAP: Record = { NV: "gray", IMR: "red", + HB: "blue", }; const TYPE_CODE_NAME_MAP: Record = { NV: "일반", IMR: "I-MR", + HB: "HB", }; type VerificationSelectionReportProps = { @@ -46,6 +60,7 @@ const VerificationSelectionReport = ({ (s) => s.setPendingInputTab, ); const alert = useAlertStore((s) => s.setMessage); + const [activeTab, setActiveTab] = useState("NV"); const statsYr = String(dayjs().year() - 1); @@ -57,7 +72,7 @@ const VerificationSelectionReport = ({ prcsYnList: "0", }; - const [nvQuery, imrQuery] = useQueries({ + const [nvQuery, imrQuery, hbQuery] = useQueries({ queries: [ { queryKey: [ @@ -91,14 +106,34 @@ const VerificationSelectionReport = ({ vrfcMthdCd: "IMR", }), }, + { + queryKey: [ + "verificationResultList", + statsYr, + "HB", + baseRequest.pbcpAprvYn, + baseRequest.page, + baseRequest.size, + baseRequest.prcsYnList, + ], + queryFn: () => + statisticsApi.getVerificationResultList({ + ...baseRequest, + vrfcMthdCd: "HB", + }), + }, ], }); - const isLoading = nvQuery.isLoading || imrQuery.isLoading; - const error = nvQuery.error ?? imrQuery.error; + const isLoading = + nvQuery.isLoading || imrQuery.isLoading || hbQuery.isLoading; + const error = nvQuery.error ?? imrQuery.error ?? hbQuery.error; - const nvList = nvQuery.data?.data?.verifyResultList ?? []; - const imrList = imrQuery.data?.data?.verifyResultList ?? []; + const listsByTab: Record = { + NV: nvQuery.data?.data?.verifyResultList ?? [], + IMR: imrQuery.data?.data?.verifyResultList ?? [], + HB: hbQuery.data?.data?.verifyResultList ?? [], + }; const openInputFromStatsCd = useCallback( (item: VerificationResultReportItem) => { @@ -147,68 +182,61 @@ const VerificationSelectionReport = ({ ], ); - const renderSection = ( - typeCode: "IMR" | "NV", + const renderResultList = ( + typeCode: VerificationReportTabKey, items: VerificationResultReportItem[], ) => ( -
-
- - {TYPE_CODE_NAME_MAP[typeCode]} {items.length} - -
-
- {items.length === 0 ? ( -
- 항목이 없습니다. -
- ) : ( -
- {items.map((item) => ( -
openInputFromStatsCd(item)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - openInputFromStatsCd(item); +
+ {items.length === 0 ? ( +
+ 항목이 없습니다. +
+ ) : ( +
+ {items.map((item) => ( +
openInputFromStatsCd(item)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + openInputFromStatsCd(item); + } + }} + > +
+ + {item.statsNm} + + -
- - {item.statsNm} - - - {TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ?? - item.vrfcMthdNm} - -
-

- {item.rsnExpln} -

-
- {item.admdstNm} - - 현재: {item.nowVlCn ?? "-"} / 비교:{" "} - {item.compVlCn ?? "-"} - -
+ className="text-sm" + > + {TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ?? + item.vrfcMthdNm} +
- ))} -
- )} -
-
+

+ {item.rsnExpln} +

+
+ {item.admdstNm} + + 현재: {item.nowVlCn ?? "-"} / 비교:{" "} + {item.compVlCn ?? "-"} + +
+ + ))} + + )} + ); return ( @@ -246,10 +274,42 @@ const VerificationSelectionReport = ({ )} {!isLoading && !error && ( -
- {renderSection("IMR", imrList)} - {renderSection("NV", nvList)} -
+
+ + setActiveTab(value as VerificationReportTabKey) + } + className="flex min-h-0 flex-1 flex-col h-full" + variant="default" + > + + {VERIFICATION_REPORT_TABS.map((tab) => ( + + {tab.label} ({listsByTab[tab.value].length}) + + ))} + + {VERIFICATION_REPORT_TABS.map((tab) => ( + + {activeTab === tab.value + ? renderResultList( + tab.value, + listsByTab[tab.value], + ) + : null} + + ))} + +
)} diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index e8c71de1b..6735f9752 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -38,6 +38,7 @@ import VerificationResultButton from '@/components/button/VerificationResultButt import DataGrid from '@/components/module/DataGrid'; import SearchForm from '@/components/module/SearchForm'; import SplitDataGrid from '@/components/module/SplitDataGrid'; +import { openVerificationResultDialogIfNeeded } from '@/components/module/post-submit-verification-results'; import { VerificationResultDialog } from '@/components/module/VerificationResultDialog'; import { VerificationResultHistoryDialog } from '@/components/module/VerificationResultHistoryDialog'; import type { CellStyleCallbackParams, ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type'; @@ -342,14 +343,34 @@ const PublicSewageFacilityInp = () => { searchParamRef.current, lastSearchedValues, ); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + ctpvNm: lastSearchedValues.sido ?? '', + sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''), + statsYr: lastSearchedValues.year ?? '', + statsCd: statsCdForTab, + }, + setVerificationResultDialogOpen, + { notify: alert }, + ); } catch (e) { await loadPublicSewageFacilityActiveTabData( 'ST00060001', searchParamRef.current, lastSearchedValues, ); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + ctpvNm: lastSearchedValues.sido ?? '', + sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''), + statsYr: lastSearchedValues.year ?? '', + statsCd: statsCdForTab, + }, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); const msg = e instanceof Error && e.message ? e.message @@ -363,7 +384,17 @@ const PublicSewageFacilityInp = () => { searchParamRef.current, lastSearchedValues, ); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + ctpvNm: lastSearchedValues.sido ?? '', + sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''), + statsYr: lastSearchedValues.year ?? '', + statsCd: statsCdForTab, + }, + setVerificationResultDialogOpen, + { notify: alert }, + ); } } catch { alert('데이터 제출 중 오류가 발생했습니다.'); @@ -472,7 +503,7 @@ const PublicSewageFacilityInp = () => { sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? 'superAdmin' : '', + // superAdmin: isAdminInfo ? 'superAdmin' : '', noApplicable: getNoApplicableForRequest(), saveList, }; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 963be10fb..9329dc967 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -56,6 +56,7 @@ import DataGrid from "@/components/module/DataGrid"; import GridTopActionButton from "@/components/module/GridTopActionButton"; import SearchForm from "@/components/module/SearchForm"; import SplitDataGrid from "@/components/module/SplitDataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import type { ColumnConfig,ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; @@ -1248,7 +1249,17 @@ const SewagePipeInp = () => { true, false, ); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + { + sewerBzmnCd: values.bsnmCd ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + statsYr: values.year ?? "", + statsCd: statsCdForTab, + }, + setVerificationResultDialogOpen, + { notify: alert }, + ); } catch (e) { await loadSewagePipeActiveTabData( tabKey, @@ -1257,7 +1268,17 @@ const SewagePipeInp = () => { true, false, ); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + { + sewerBzmnCd: values.bsnmCd ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + statsYr: values.year ?? "", + statsCd: statsCdForTab, + }, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); const msg = e instanceof Error && e.message ? e.message @@ -1273,7 +1294,17 @@ const SewagePipeInp = () => { true, false, ); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + { + sewerBzmnCd: values.bsnmCd ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + statsYr: values.year ?? "", + statsCd: statsCdForTab, + }, + setVerificationResultDialogOpen, + { notify: alert }, + ); } } catch { alert("데이터 제출 중 오류가 발생했습니다."); @@ -1429,7 +1460,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + // superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -1509,7 +1540,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + // superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -1594,7 +1625,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + // superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -1657,7 +1688,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + // superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -1736,7 +1767,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + // superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 22b160656..2e0f318f9 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -41,6 +41,7 @@ import VerificationResultButton from "@/components/button/VerificationResultButt // import { DataGrid } from "@rsone/components"; import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; @@ -348,9 +349,9 @@ const SeweragePenetrationRateInp = () => { statsCd: statsCd, }; - if (isAdminInfo) { - params.superAdmin = 'superAdmin'; - } + // if (isAdminInfo) { + // params.superAdmin = 'superAdmin'; + // } try { setIsLoading(true); @@ -525,11 +526,35 @@ const SeweragePenetrationRateInp = () => { } setGlobalProgress(100); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd + ? { + sewerBzmnCd: searchValues.bsnmCd, + ctpvNm: searchValues.sido, + sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""), + statsYr: searchValues.year, + statsCd, + } + : null, + setVerificationResultDialogOpen, + { notify: alert }, + ); } catch (e) { await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); setIsLoading(false); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd + ? { + sewerBzmnCd: searchValues.bsnmCd, + ctpvNm: searchValues.sido, + sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""), + statsYr: searchValues.year, + statsCd, + } + : null, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); const msg = e instanceof Error && e.message ? e.message @@ -540,7 +565,19 @@ const SeweragePenetrationRateInp = () => { } else { setGlobalProgress(100); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); - setVerificationResultDialogOpen(true); + await openVerificationResultDialogIfNeeded( + searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd + ? { + sewerBzmnCd: searchValues.bsnmCd, + ctpvNm: searchValues.sido, + sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""), + statsYr: searchValues.year, + statsCd, + } + : null, + setVerificationResultDialogOpen, + { notify: alert }, + ); } } else { alert(response.message || '데이터 제출 중 오류가 발생했습니다.'); @@ -641,9 +678,9 @@ const SeweragePenetrationRateInp = () => { sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계 }; - if (isAdminInfo) { - requestData.superAdmin = 'superAdmin'; - } + // if (isAdminInfo) { + // requestData.superAdmin = 'superAdmin'; + // } const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData); diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 3dea0d65a..9edcc2b03 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -162,6 +162,8 @@ export type ColumnConfig = { editable?: boolean; // 셀 편집 가능 여부 /** 그리드에서 컬럼 숨김 (수식용 필드 등) */ visible?: boolean; + /** true이면 엑셀보내기에서 제외 (화면에는 표시 가능) */ + excludeFromExcelExport?: boolean; /** 컬럼 너비 (px). 지정 시 ... 생략 없이 텍스트 표시에 유리 */ width?: number; /** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */ diff --git a/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts index f8f5d3a06..9f02da89a 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts @@ -4,7 +4,7 @@ import { ValueType } from "realgrid"; // 하수도 통계 관리 > 02.하수관로 > tab 5. 맨홀 현황 // SeweragePipeInputManholeStatusItem + commonInfo.ctpvNm (그리드 행에 병합) -// API: yy14=2014년 … yy{(현재연도-1)%100} (필드 접두 yy + 연도 끝자리) +// API: yy14=2015년 이전(2014년 데이터) … yy{(현재연도-1)%100} (필드 접두 yy + 연도 끝자리) const MANHOLE_START_YEAR = 2014; const MANHOLE_LAST_DATA_YEAR = Math.max(MANHOLE_START_YEAR, new Date().getFullYear() - 1); @@ -12,6 +12,12 @@ const MANHOLE_YY_MIN = MANHOLE_START_YEAR % 100; const MANHOLE_YY_MAX = MANHOLE_LAST_DATA_YEAR % 100; const calendarYearFromYy = (yy: number) => 2000 + yy; +/** yy14(2014) 구간은 헤더에 「2015년 이전」 표기 (합계·검토 컬럼과 동일) */ +const manholeYearHeaderText = (yy: number): string => { + const y = calendarYearFromYy(yy); + return y === MANHOLE_START_YEAR ? "2015년 이전" : `${y}년`; +}; + const intCol = (name: string, headerText: string): ColumnConfig => ({ name, fieldName: name, @@ -98,7 +104,7 @@ function yyYearLayout(yy: number): ColumnLayoutItem { return { name: `year${y}`, hideChildHeaders: false, - header: { text: `${y}년` }, + header: { text: manholeYearHeaderText(yy) }, items: [ { name: `year${y}Material`, diff --git a/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts index 94f943f0a..61ac482da 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts @@ -211,8 +211,9 @@ export const PipeTypeStatusInpColumn = { ], }, ...yearLayoutItems, - "rmrk", + 'rmrk', 'admdstCd', + 'pipeSeCd' ] as ColumnLayoutItem[], rowStartNum: 2, fixedColumns: 2, diff --git a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts index 832a1f338..0cf31e2b1 100644 --- a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts +++ b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts @@ -955,6 +955,7 @@ export const getST00020003Config = (statsYr: string | number) => { valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false, + excludeFromExcelExport: true, }, ...(() => { const yearColumns = []; diff --git a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts index a17043460..01b224322 100644 --- a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts +++ b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts @@ -134,6 +134,7 @@ export const SeweragePentrationRateTotColumn = { editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', + excludeFromExcelExport: true, }, // 비고 diff --git a/apps/st/src/components/util/gridExcelExport.ts b/apps/st/src/components/util/gridExcelExport.ts new file mode 100644 index 000000000..ce86d6492 --- /dev/null +++ b/apps/st/src/components/util/gridExcelExport.ts @@ -0,0 +1,45 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; +import type { GridView } from "realgrid"; + +export type ExcelExportVisibilitySnapshot = Map; + +/** 엑셀보내기 직전: 제외 컬럼 숨김, 그리드용 숨김(visible:false) 컬럼은 export에만 표시 */ +export function prepareGridColumnsForExcelExport( + gridView: GridView, + columns: ColumnConfig[], +): ExcelExportVisibilitySnapshot { + const excludedNames = new Set( + columns.filter((c) => c.excludeFromExcelExport).map((c) => c.name), + ); + const snapshot = new Map(); + + columns.forEach((col) => { + const gridCol = gridView.columnByName?.(col.name); + if (!gridCol) return; + + const wasVisible = gridCol.visible !== false; + snapshot.set(col.name, wasVisible); + + if (excludedNames.has(col.name)) { + gridCol.visible = false; + } else if (col.visible === false) { + gridCol.visible = true; + } + }); + + return snapshot; +} + +export function restoreGridColumnsAfterExcelExport( + gridView: GridView, + snapshot: ExcelExportVisibilitySnapshot, +): void { + snapshot.forEach((visible, name) => { + const gridCol = gridView.columnByName?.(name); + if (gridCol) gridCol.visible = visible; + }); +} + +export function shouldPrepareColumnsForExcelExport(columns: ColumnConfig[]): boolean { + return columns.some((c) => c.excludeFromExcelExport || c.visible === false); +} diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index 20edb9935..fc36b73c8 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -21,6 +21,8 @@ import { type ReviewSubmitResponse, type SearchCondition, type Sgg, + type StatiscicsExceptionResetRequest, + type StatiscicsExceptionResetResponse, type StatiscicsManagementReviewHistoryRequest, type StatiscicsManagementReviewHistoryResponse, type StatisticsColumnGuideListResponse, @@ -142,7 +144,7 @@ export class StatisticsApiService implements StatisticsService { public async getImrStatus(param: string): Promise { const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.IMR}/status/${param}`, + `${ENDPOINT.STATVERIFY_SERVICE.IMR}/status/${param}`, ); return response.data; } @@ -311,6 +313,14 @@ export class StatisticsApiService implements StatisticsService { ); return response.data; } + + public async StatiscicsExceptionReset(param: StatiscicsExceptionResetRequest): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/exception/reset`, + param, + ); + return response.data; + } } export const statisticsApi = new StatisticsApiService(); diff --git a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts index 2c82cbb0d..328818e48 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts @@ -232,7 +232,7 @@ export interface PublicSewageFacilitySaveRequest { sggNm: string; sewerBzmnCd: string; userId: string; - superAdmin: string; + superAdmin?: string; /** * 저장 요청에서 "해당없음" 처리 여부가 함께 전달되는 경우를 고려합니다. diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts index d6357d78f..4076e5f4f 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts @@ -112,7 +112,7 @@ export interface SeweragePipeInputRegionalFactStatusSaveRequest { sggNm: string; sewerBzmnCd: string; userId: string; - superAdmin: string; + superAdmin?: string; saveList: SeweragePipeInputRegionalFactStatusSaveItem[]; } @@ -159,7 +159,7 @@ export interface SeweragePipeInputFactInstallStatusSaveRequest { sggNm: string; sewerBzmnCd: string; userId: string; - superAdmin: string; + superAdmin?: string; saveList: SeweragePipeInputFactInstallStatusSaveItem[]; } @@ -300,7 +300,7 @@ export interface SeweragePipeInputPipeTypeStatusSaveRequest { sggNm: string; sewerBzmnCd: string; userId: string; - superAdmin: string; + superAdmin?: string; saveList: SeweragePipeInputPipeTypeStatusSaveItem[]; } @@ -372,7 +372,7 @@ export interface SeweragePipeInputPipeDiameterStatusSaveRequest { sggNm: string; sewerBzmnCd: string; userId: string; - superAdmin: string; + superAdmin?: string; saveList: SeweragePipeInputPipeDiameterStatusSaveItem[]; } @@ -470,7 +470,7 @@ export interface SeweragePipeInputManholeStatusSaveRequest { sggNm: string; sewerBzmnCd: string; userId: string; - superAdmin: string; + superAdmin?: string; saveList: SeweragePipeInputManholeStatusSaveItem[]; } diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index de3a9bf7b..464c391c4 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -462,6 +462,19 @@ export interface StatiscicsManagementReviewHistoryResponse { data: StatiscicsManagementReviewHistoryItem[]; } +export interface StatiscicsExceptionResetRequest { + statsYr: string; + sewerBzmnCd: string; + statsCd: string; + mdfrId: string, + vrfcSnList: number[] +} + +export interface StatiscicsExceptionResetResponse { + status: string; + message?: string; +} + export interface StatisticsService { getSidoList(params?: { instSeCd?: string }): Promise>; getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise>; @@ -490,4 +503,5 @@ export interface StatisticsService { getColumnGuides(param: string): Promise; getReviewerInfo(param: ReviewerInfoRequest): Promise; getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise; + StatiscicsExceptionReset(param: StatiscicsExceptionResetRequest): Promise; } \ No newline at end of file diff --git a/apps/st/src/shared/lib/stats-cd-input-navigation.ts b/apps/st/src/shared/lib/stats-cd-input-navigation.ts index 3acdf6f0b..6a00d33ae 100644 --- a/apps/st/src/shared/lib/stats-cd-input-navigation.ts +++ b/apps/st/src/shared/lib/stats-cd-input-navigation.ts @@ -37,8 +37,12 @@ export const STATISTICS_INPUT_URL_PATH = { SEWAGE_PIPE: "SewagePipeInp", SEWAGE_PIPE_MAINTAIN: "SewagePipeMaintainInp", SEWERAGE_PENETRATION_RATE: "SeweragePenetrationRateInp", + PUBLIC_SEWAGE_FACILITY: "PublicSewageFacilityInp", } as const; +/** `PublicSewageFacilityInp.tsx` — 공공하수처리시설 탭 */ +const PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS = ["ST00060001"] as const; + function normalizeSewagePipeTabKey(tabKey: string): string { if ( tabKey === "adminAreaConnectionInp" || @@ -59,6 +63,10 @@ const NAV_ROWS: { urlPath: string; tabKeys: readonly string[] }[] = [ urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE, tabKeys: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS, }, + { + urlPath: STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY, + tabKeys: PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS, + }, ]; /**