"use client"; import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { RealGridReact } from "realgrid-react"; import { getUserInfo } from "../../../../../packages/common/components/src/api"; import { useAlertStore } from "@/shared/lib"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { ReviewInputSaveRequest, VerificationResultListRequest, VerificationResultReportItem, } from "@/entities/statistics/model/statistics-interface"; import { Button, Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, } from "@common/components"; import DataGrid from "@/components/module/DataGrid"; import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; /** 통계 입력 현황 > 행 클릭 시 사용자예외처리(prcsYn=2) 내역만 조회 */ export interface ExceptionProcessingHistoryQueryContext { sewerBzmnCd: string; ctpvNm: string; sggNm: string; statsYr: string; statsCd: string; } export interface ExceptionProcessingHistoryDialogProps { open: boolean; onOpenChange: (open: boolean) => void; queryContext?: ExceptionProcessingHistoryQueryContext | null; emptyMessage?: string; /** 반려 API 성공 후 (다이얼로그 내 목록 재조회 이후) 상위 화면 등에서 호출 */ onRejectSuccess?: () => void | Promise; } type ExceptionHistoryGridRow = Record; type ExceptionHistoryTabKey = "NV" | "IMR" | "HB"; const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [ { value: "NV", label: "일반검증 내역" }, { value: "IMR", label: "I-MR 검증내역" }, { value: "HB", label: "HB 검증내역" }, ]; const TITLE = "예외처리 내역 조회"; /** 예외처리 건만 */ const PRCS_YN_LIST = "2"; export type ExceptionRejectSubmitItem = { vrfcSn: number; statsYr: string; sewerBzmnCd: string; statsCd: string; mdfrId: string; rjctOpnnCn?: string; }; function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined { if (raw === undefined || raw === null) return undefined; if (typeof raw === "number" && Number.isFinite(raw)) return raw; const s = String(raw).trim(); if (s === "") return undefined; const n = Number.parseInt(s, 10); return Number.isFinite(n) ? n : undefined; } function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined { if (fieldName !== "prcsYnNm") return undefined; const code = prcsYnCodeToNumber(row.prcsYn); if (code === 0 || code === 4) return "rg-cell-vrf-prcs-bad"; return "rg-cell-vrf-prcs-ok"; } const exceptionHistoryColumnInfo: ColumnInfo = { columns: [ { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "admdstNm", fieldName: "admdstNm", 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: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false }, { name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false }, { name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false }, { name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false }, { name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false }, { name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false }, { name: "individualReject", fieldName: "individualReject", header: { text: "개별 반려" }, editable: false, width: 104, styleName: "rg-cell-reject-mini-btn", }, { name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false }, { name: "vrfcSn", fieldName: "vrfcSn", header: { text: "vrfcSn" }, editable: false, visible: false }, ], columnLayout: [ "ctpvNm", "admdstNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYnNm", "stVrfRjtComNm", "prcsOpnnExpln", "rjctOpnnCn", "regDt", "mdfcnDt", "individualReject", "prcsYn", "vrfcSn", ], }; export function ExceptionProcessingHistoryDialog({ open, onOpenChange, queryContext, emptyMessage = "표시할 예외처리 내역이 없습니다.", onRejectSuccess, }: ExceptionProcessingHistoryDialogProps) { const { setMessage: alert } = useAlertStore((state) => state); const nvGridRef = useRef(null); const imrGridRef = useRef(null); const hbGridRef = useRef(null); const [rejectReasonDialogOpen, setRejectReasonDialogOpen] = useState(false); const [pendingRejectVrfcSns, setPendingRejectVrfcSns] = useState([]); const [rejectReasonText, setRejectReasonText] = useState(""); const [rejectSubmitting, setRejectSubmitting] = useState(false); 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 [errorMessage, setErrorMessage] = useState(null); 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); setErrorMessage(null); setRejectReasonDialogOpen(false); setPendingRejectVrfcSns([]); setRejectReasonText(""); setRejectSubmitting(false); }, []); const fetchPage = useCallback( async (vrfcMthdCd: ExceptionHistoryTabKey, pageIndex: number) => { if (!queryContext) return null; const param: VerificationResultListRequest = { sewerBzmnCd: queryContext.sewerBzmnCd, ctpvNm: queryContext.ctpvNm, sggNm: queryContext.sggNm, statsYr: queryContext.statsYr, statsCd: queryContext.statsCd, vrfcMthdCd, prcsYnList: PRCS_YN_LIST, rejectCodeList: "0,1,2", page: pageIndex + 1, size: 30, }; return statisticsApi.getVerificationResultList(param); }, [queryContext], ); const loadInitial = useCallback(async () => { if (!queryContext) { setErrorMessage("조회 조건이 없어 예외처리 내역을 불러올 수 없습니다."); return; } setErrorMessage(null); setNormalLoading(true); setImrLoading(true); setHbLoading(true); setNormalPageIndex(0); setImrPageIndex(0); setHbPageIndex(0); try { const [nvRes, imrRes, hbRes] = await Promise.all([ fetchPage("NV", 0), fetchPage("IMR", 0), fetchPage("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); } catch { setNormalEntries([]); setImrEntries([]); setHbEntries([]); setNormalPageCount(1); setImrPageCount(1); setHbPageCount(1); setErrorMessage("예외처리 내역 조회 중 오류가 발생했습니다."); } finally { setNormalLoading(false); setImrLoading(false); setHbLoading(false); } }, [fetchPage, queryContext]); const handleNormalPageChange = useCallback( async (pageIndex: number) => { if (pageIndex === normalPageIndex || !queryContext) return; setErrorMessage(null); setNormalLoading(true); try { const response = await fetchPage("NV", pageIndex); setNormalEntries(response?.data?.verifyResultList ?? []); setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1); setNormalPageIndex(pageIndex); } catch { setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다."); } finally { setNormalLoading(false); } }, [fetchPage, normalPageIndex, queryContext], ); const handleImrPageChange = useCallback( async (pageIndex: number) => { if (pageIndex === imrPageIndex || !queryContext) return; setErrorMessage(null); setImrLoading(true); try { const response = await fetchPage("IMR", pageIndex); setImrEntries(response?.data?.verifyResultList ?? []); setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1); setImrPageIndex(pageIndex); } catch { setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다."); } finally { setImrLoading(false); } }, [fetchPage, imrPageIndex, queryContext], ); const handleHbPageChange = useCallback( async (pageIndex: number) => { if (pageIndex === hbPageIndex || !queryContext) return; setErrorMessage(null); setHbLoading(true); try { const response = await fetchPage("HB", pageIndex); setHbEntries(response?.data?.verifyResultList ?? []); setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1); setHbPageIndex(pageIndex); } catch { setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다."); } finally { setHbLoading(false); } }, [fetchPage, hbPageIndex, queryContext], ); useEffect(() => { if (!open) { resetState(); return; } void loadInitial(); }, [loadInitial, open, resetState]); const mapRows = (entries: VerificationResultReportItem[]): ExceptionHistoryGridRow[] => entries.map((item) => ({ ctpvNm: item.ctpvNm, admdstNm: item.admdstNm ?? item.sggNm ?? "-", vrfcTrgtNm: item.vrfcTrgtNm, rsnExpln: item.rsnExpln, nowVlCn: item.nowVlCn ?? "-", compVlCn: item.compVlCn ?? "-", prcsYnNm: item.prcsYnNm ?? "-", stVrfRjtComNm: item.stVrfRjtComNm ?? "-", prcsOpnnExpln: item.prcsOpnnExpln ?? "-", rjctOpnnCn: item.rjctOpnnCn ?? "-", prcsYn: item.prcsYn ?? "", regDt: (item.regDt ?? "").slice(0, 16), mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16), individualReject: "개별 반려", vrfcSn: item.vrfcSn, })); const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]); const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]); const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]); const noopSetRows = ((_: SetStateAction[]>) => {}) as Dispatch< SetStateAction[]> >; const handleOpenRejectReasonDialog = useCallback(() => { if (!queryContext) { alert("조회 조건이 없습니다."); return; } const rows = activeTab === "NV" ? normalRows : activeTab === "IMR" ? imrRows : hbRows; const gridRef = activeTab === "NV" ? nvGridRef : activeTab === "IMR" ? imrGridRef : hbGridRef; const gridView = gridRef.current?.gridView; if (!gridView) { alert("데이터가 존재하지 않습니다."); return; } const checkedIndices = (gridView.getCheckedRows?.(true, false, true) ?? []) as number[]; if (checkedIndices.length === 0) { alert("반려할 항목을 체크해주세요."); return; } const vrfcSns: number[] = []; for (const itemIndex of checkedIndices) { const row = rows[itemIndex]; if (!row) continue; const vrfcSn = Number(row.vrfcSn); if (!Number.isFinite(vrfcSn)) continue; vrfcSns.push(vrfcSn); } if (vrfcSns.length === 0) { alert("유효한 검증번호가 있는 행을 선택해주세요."); return; } setPendingRejectVrfcSns(vrfcSns); setRejectReasonText(""); setRejectSubmitting(false); setRejectReasonDialogOpen(true); }, [activeTab, alert, hbRows, imrRows, normalRows, queryContext]); const resetRejectReasonDialog = useCallback(() => { setRejectReasonDialogOpen(false); setPendingRejectVrfcSns([]); setRejectReasonText(""); setRejectSubmitting(false); }, []); const handleRejectReasonDialogOpenChange = useCallback( (nextOpen: boolean) => { if (!nextOpen) resetRejectReasonDialog(); }, [resetRejectReasonDialog], ); const handleConfirmReject = useCallback(async () => { if (!queryContext || pendingRejectVrfcSns.length === 0) { alert("반려할 항목이 없습니다. 다시 선택해주세요."); resetRejectReasonDialog(); return; } let userInfo; try { userInfo = await getUserInfo(); } catch { alert("사용자 정보를 불러오지 못했습니다."); return; } const mdfrId = userInfo.userUnqId; const trimmedReason = rejectReasonText.trim(); const items: ReviewInputSaveRequest[] = pendingRejectVrfcSns.map((vrfcSn) => ({ type: "INPUT", statsYr: queryContext.statsYr, sewerBzmnCd: queryContext.sewerBzmnCd, statsCd: queryContext.statsCd, mdfrId, vrfcSn: String(vrfcSn), ...(trimmedReason ? { rjctOpnnCn: trimmedReason } : {}), })); setRejectSubmitting(true); try { const response = await statisticsApi.saveInputReturn(items); if (response.resultCode.toLowerCase() !== "success") { alert(response.message ?? "반려 처리 중 오류가 발생했습니다."); return; } alert("반려 처리되었습니다."); resetRejectReasonDialog(); await Promise.all([ loadInitial(), onRejectSuccess?.() ?? Promise.resolve(), ]); } catch { alert("반려 처리 중 오류가 발생했습니다."); } finally { setRejectSubmitting(false); } }, [ alert, loadInitial, onRejectSuccess, pendingRejectVrfcSns, queryContext, rejectReasonText, resetRejectReasonDialog, ]); const handleGridRowClick = useCallback( (rowData: Record) => { const gridRef = activeTab === "NV" ? nvGridRef : activeTab === "IMR" ? imrGridRef : hbGridRef; const current = gridRef.current?.gridView?.getCurrent?.() as | { fieldName?: string; column?: string } | undefined; const fieldName = current?.fieldName ?? current?.column; if (fieldName !== "individualReject") return; const vrfcSn = Number(rowData.vrfcSn); if (!Number.isFinite(vrfcSn)) { alert("유효한 검증번호가 있는 행을 선택해주세요."); return; } setPendingRejectVrfcSns([vrfcSn]); setRejectReasonText(""); setRejectSubmitting(false); setRejectReasonDialogOpen(true); }, [activeTab, alert], ); const renderGrid = ( rows: ExceptionHistoryGridRow[], pageIndex: number, pageCount: number, onPageIndexChange: (page: number) => void, loading: boolean, gridRef: MutableRefObject, ) => { if (!loading && rows.length === 0) { return

{emptyMessage}

; } return (
{loading && (
)}
); }; return ( <> {`${TITLE} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}
{errorMessage && (

{errorMessage}

)} setActiveTab(value as ExceptionHistoryTabKey)} className="w-full" variant="default" > {exceptionHistoryTabs.map((tab) => ( {tab.label} ))} {renderGrid( normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading, nvGridRef, )} {renderGrid( imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading, imrGridRef, )} {renderGrid( hbRows, hbPageIndex, hbPageCount, handleHbPageChange, hbLoading, hbGridRef, )}
예외처리 반려