"use client"; import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import type { RealGridReact } from "realgrid-react"; import { Button, Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, LoadingWithAnimation, } 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 { 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 { useAlertStore } from "@/shared/lib"; type VerificationResultGridRow = Record; export type VerificationExceptionSubmitItem = { vrfcSn: number; prcsOpnnExpln: string; }; type GridViewWithCheckItem = { checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void; }; function setRowCheckByItemIndex( gridRef: MutableRefObject, itemIndex: number, checked: boolean, ) { const gv = gridRef.current?.gridView as GridViewWithCheckItem | undefined; try { gv?.checkItem?.(itemIndex, checked, false); } catch { /* RealGrid 체크 API 미지원 등 */ } } const PRCS_YN_LABEL: Record = { "0": "미통과", "1": "통과", "2": "예외처리", "3": "반려", }; /** History와 동일 + 편집 가능한 처리의견(예외사유, API 필드명 `prcsOpnnExpln`과 동일) */ const verificationSubmitResultColumnInfo: ColumnInfo = { columns: [ { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, { name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false }, { name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false }, { name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false }, { name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false }, { name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false }, { name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false }, { name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false }, { name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false }, { name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "예외사유" }, editable: true, }, ], columnLayout: [ "ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYn", "pbcpAprvYn", "regDt", "mdfcnDt", "prcsOpnnExpln", ], }; export interface VerificationResultDialogQueryContext { sewerBzmnCd: string; ctpvNm: string; sggNm: string; statsYr: string; statsCd: string; } export interface VerificationResultDialogProps { open: boolean; onOpenChange: (open: boolean) => void; queryContext?: VerificationResultDialogQueryContext | null; title?: string; emptyMessage?: string; /** 예외 제출 API 성공 후 부모(입력 화면)에서 메인 데이터를 다시 조회할 때 사용 */ onExceptionSubmitted?: () => void | Promise; } export function VerificationResultDialog({ open, onOpenChange, queryContext, title = "검증 결과", emptyMessage = "표시할 검증 결과가 없습니다.", onExceptionSubmitted, }: VerificationResultDialogProps) { /** 부모가 매 렌더마다 새 객체를 넘기면 queryContext 참조만 바뀌어 loadInitial이 재실행되므로 필드 값 기준으로 안정화 */ const stableQueryContext = useMemo((): VerificationResultDialogQueryContext | null => { if (!queryContext) return null; return queryContext; }, [ queryContext?.sewerBzmnCd, queryContext?.ctpvNm, queryContext?.sggNm, queryContext?.statsYr, 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 [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 normalGridRef = useRef(null); const imrGridRef = useRef(null); /** 메인 입력 화면처럼 편집 값은 그리드에 두고, 제출 시에만 스냅샷 조회 → Enter 시 rows 재주입 충돌 방지 */ const getNormalGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null); const getImrGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null); const alert = useAlertStore((state) => state.setMessage); const onNormalPrcsOpnnExplnEdited = 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) => { const next = new Set(prev); if (hasValue) next.add(params.itemIndex); else next.delete(params.itemIndex); return next; }); }, []); const resetState = useCallback(() => { setNormalPageIndex(0); setImrPageIndex(0); setNormalPageCount(1); setImrPageCount(1); setNormalLoading(false); setImrLoading(false); setErrorMessage(null); setNormalRows([]); setImrRows([]); setNormalCheckedIndices(new Set()); setImrCheckedIndices(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 fetchVerificationResultPage = useCallback( async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => { if (!stableQueryContext) return null; const param: VerificationResultListRequest = { sewerBzmnCd: stableQueryContext.sewerBzmnCd, ctpvNm: stableQueryContext.ctpvNm, sggNm: stableQueryContext.sggNm, statsYr: stableQueryContext.statsYr, statsCd: stableQueryContext.statsCd, pbcpAprvYn: "N", vrfcMthdCd, prcsYnList: "0", page: pageIndex + 1, size: 10000, }; return statisticsApi.getVerificationResultList(param); }, [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; setErrorMessage(null); setNormalLoading(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); } catch { setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다."); } finally { setNormalLoading(false); setNormalCheckedIndices(new Set()); } }, [fetchVerificationResultPage, mapItemToRow, normalPageIndex, 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()); } }, [fetchVerificationResultPage, mapItemToRow, imrPageIndex, stableQueryContext], ); useEffect(() => { if (!open) { resetState(); return; } void loadInitial(); }, [loadInitial, open, resetState]); /** 조회된 행에 예외사유 텍스트가 있으면 체크바·체크 인덱스와 맞춤 */ useEffect(() => { if (normalLoading) return; const id = requestAnimationFrame(() => { setNormalCheckedIndices((prev) => { const next = new Set(prev); normalRows.forEach((row, itemIndex) => { const has = String(row.prcsOpnnExpln ?? "").trim().length > 0; setRowCheckByItemIndex(normalGridRef, 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]); const handleExceptionSubmitClick = useCallback(async () => { const nvGrid = getNormalGridDataRef.current?.() ?? []; const imGrid = getImrGridDataRef.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); if (payload.length === 0) { alert("작성된 예외 제출 항목이 없습니다."); return; } if (!stableQueryContext) { alert("조회 조건이 없어 예외 제출을 할 수 없습니다."); return; } try { const userInfo = await getUserInfo(); const response = await statisticsApi.SubmitVerifyException({ sewerBzmnCd: stableQueryContext.sewerBzmnCd, statsYr: stableQueryContext.statsYr, statsCd: stableQueryContext.statsCd, userId: userInfo.userUnqId, exceptionList: payload, }); if (response.status === "success") { alert("예외 제출 처리되었습니다."); try { await onExceptionSubmitted?.(); } catch { alert("예외 제출은 완료되었으나 화면 데이터를 다시 불러오지 못했습니다. 조회로 새로고침 해주세요."); } onOpenChange(false); } else { alert(response.message ?? "예외 제출 처리 중 오류가 발생했습니다."); } } catch { alert("예외 제출 처리 중 오류가 발생했습니다."); } }, [ alert, imrCheckedIndices, imrRows, normalCheckedIndices, normalRows, 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}` : ""; return ` - ${stableQueryContext.statsYr} ${stableQueryContext.ctpvNm}${sgg}`; }, [stableQueryContext]); return ( {title} {titleSuffix}
{errorMessage && (

{errorMessage}

)}

일반검증 내역

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

I-MR 검증내역

{renderGrid( imrRows, setImrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading, imrGridRef, getImrGridDataRef, onImrPrcsOpnnExplnEdited, setImrCheckedIndices, )}
); }