"use client"; import type { Dispatch, SetStateAction } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; import type { RealGridReact } from "realgrid-react"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useMutationWrapper, useUserStore } from "@/shared/lib"; import { CodeApi, CodeSelect } from "@/entities/code"; import { type ApproveRequest, type RejectRequest, statisticsApprovalApi, type StatisticsVerificationFilterState, statisticsVerificationReportColumnInfo, statisticsVerificationResultColumnInfo, type StatisticsVerificationSubFilterState, type StatsInputStatusList, useStAnlsSidoCombo, useStAnlsSigunguCombo, useStatsVrfcRptData, useStatsVrfcRsltData, type VerifyResultItem, } from "@/entities/statistics"; import { Button, Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList, ComboboxValue, Label, LoadingWithAnimation, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useComboboxAnchor, } from "@common/components"; import DataGrid from "@/components/module/DataGrid"; import { type ApprovalFormData, ApprovalInputDialog, } from "@/components/statistics/verification/ApprovalInputDialog"; import type { CellEditedCallbackParams } from "@/components/type/DataGrid.type"; import { toGridRows } from "@/components/util/grid/gridFunction"; const getYearOptions = () => { const currentYear = dayjs().year(); const yearOptionsRange = Array.from( { length: currentYear - 2017 }, (_, i) => { const year = 2017 + i; return { label: String(year), value: String(year) }; }, ); return yearOptionsRange.map((year) => ({ label: year.label, value: year.value, })); }; function isSameStatsInputStatusRow( a: StatsInputStatusList, b: StatsInputStatusList, ): boolean { return ( a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.statsCd === b.statsCd ); } function isSameVerifyResultRow( a: VerifyResultItem, b: VerifyResultItem, ): boolean { if (a.vrfcSn != null && b.vrfcSn != null) return a.vrfcSn === b.vrfcSn; return ( a.statsYr === b.statsYr && a.statsCd === b.statsCd && a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.vrfcTrgtNm === b.vrfcTrgtNm && a.vrfcMthdCd === b.vrfcMthdCd ); } function useGridCheckboxSelection( sourceRows: T[], isSameRow: (a: T, b: T) => boolean, setSelected: Dispatch>, ) { const handleItemChecked = useCallback( (itemIndex: number, checked: boolean) => { const origin = sourceRows[itemIndex]; if (origin === undefined) return; setSelected((prev) => { if (checked) { if (prev.some((r) => isSameRow(r, origin))) return prev; return [...prev, origin]; } return prev.filter((r) => !isSameRow(r, origin)); }); }, [sourceRows, isSameRow, setSelected], ); const handleAllChecked = useCallback( (checked: boolean) => { if (checked) setSelected([...sourceRows]); else setSelected([]); }, [sourceRows, setSelected], ); return { handleItemChecked, handleAllChecked }; } const DEFAULT_FILTERS: StatisticsVerificationFilterState = { ctpvNm: null, sggNm: null, statsYr: String(dayjs().year() - 1), sewerBzmnCd: null, statsCd: null, laeSbmsnSttsCd: "2", }; const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = { ...DEFAULT_FILTERS, prcsYnList: "1,2,3", pbcpAprvYn: "N", }; const APPROVAL_STATUS_OPTIONS = [ { label: "승인", value: "Y" }, { label: "미승인", value: "N" }, ]; function StatisticsVerificationReport() { const userInfo = useUserStore((state) => state.userInfo); const { setMessage: alert } = useAlertStore((state) => state); const { confirm } = useConfirm(); const [filters, setFilters] = useState(DEFAULT_FILTERS); const [subFilters, setSubFilters] = useState(DEFAULT_SUB_FILTERS); const [selectedReportRows, setSelectedReportRows] = useState< StatsInputStatusList[] >([]); const [selectedResultRows, setSelectedResultRows] = useState< VerifyResultItem[] >([]); const [approvalDialogOpen, setApprovalDialogOpen] = useState(false); const editedResultRowMapRef = useRef>( new Map(), ); const resultGridRef = useRef(null); const prevLengthRef = useRef(0); const { data: sidoData } = useStAnlsSidoCombo(); const { data: sigunguData } = useStAnlsSigunguCombo( filters.sewerBzmnCd ?? "", ); const detailParams = useMemo( () => ({ statsYr: filters.statsYr, ctpvNm: subFilters.ctpvNm !== "" ? subFilters.ctpvNm : filters.ctpvNm, sggNm: subFilters.sggNm !== "" ? subFilters.sggNm : filters.sggNm, sewerBzmnCd: subFilters.sewerBzmnCd !== "" ? subFilters.sewerBzmnCd : filters.sewerBzmnCd, statsCd: subFilters.statsCd !== "" ? subFilters.statsCd : filters.statsCd, prcsYnList: subFilters.prcsYnList, pbcpAprvYn: subFilters.pbcpAprvYn, laeSbmsnSttsCd: subFilters.laeSbmsnSttsCd, }), [filters, subFilters], ); const { data: statusOptions } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STINPTEXCT"], queryFn: () => CodeApi.getCode("STINPTEXCT"), select: (data) => data .filter(({ lwpoCd }) => !["0", "4"].includes(lwpoCd)) .map(({ lwpoCdNm: label, lwpoCd: value }) => ({ label, value, })), }); const statisticsVerificationReportQuery = useStatsVrfcRptData(filters); const statisticsVerificationResultQuery = useStatsVrfcRsltData(detailParams); const statisticsVerificationReportData = statisticsVerificationReportQuery.data ?? []; const statisticsVerificationReportGridRows = useMemo( () => toGridRows(statisticsVerificationReportData), [statisticsVerificationReportData], ); const statisticsVerificationResultData = useMemo( () => statisticsVerificationResultQuery.data?.pages.flatMap( (page) => page.verifyResultList ?? [], ) ?? [], [statisticsVerificationResultQuery.data], ); const statisticsVerificationResultGridRows = useMemo( () => toGridRows(statisticsVerificationResultData), [statisticsVerificationResultData], ); function mergeWithEditedMap(rows: Record[]) { const editedMap = editedResultRowMapRef.current; if (editedMap.size === 0) return rows; return rows.map((row) => { const vrfcSn = Number(row.vrfcSn); const edited = editedMap.get(vrfcSn); return edited ? { ...row, ...edited } : row; }); } function captureCheckedVrfcSn(): number[] { const gridView = resultGridRef.current?.gridView; const dataProvider = resultGridRef.current?.dataProvider; if (!gridView || !dataProvider) return []; const checkedRows = gridView.getCheckedRows?.(true, false, true) ?? []; return checkedRows .map((row) => Number(dataProvider.getValue?.(row, "vrfcSn"))) .filter((key) => !Number.isNaN(key)); } function restoreCheckedVrfcSn( keys: number[], rows: Record[], ) { const gridView = resultGridRef.current?.gridView; if (!gridView || keys.length === 0) return; const keySet = new Set(keys); const rowsToCheck = rows .map((row, index) => ({ index, key: Number(row.vrfcSn) })) .filter((item) => !Number.isNaN(item.key) && keySet.has(item.key)) .map((item) => item.index); if (rowsToCheck.length === 0) return; gridView.checkRows?.(rowsToCheck, true, false); } useEffect(() => { const dataProvider = resultGridRef.current?.dataProvider; const nextRows = statisticsVerificationResultGridRows; const prevLength = prevLengthRef.current; if (!dataProvider) { prevLengthRef.current = nextRows.length; return; } if (nextRows.length === 0) { dataProvider.setRows?.([]); prevLengthRef.current = 0; return; } if (nextRows.length > prevLength && prevLength > 0) { // 무한스크롤 시 데이터 처리 const newRows = nextRows.slice(prevLength); const mergedNewRows = mergeWithEditedMap(newRows); dataProvider.addRows?.(mergedNewRows); } else { // 초기 조회 또는 재조회 시 데이터 처리 const checkedKeys = captureCheckedVrfcSn(); const mergedRows = mergeWithEditedMap(nextRows); dataProvider.setRows?.(mergedRows); restoreCheckedVrfcSn(checkedKeys, mergedRows); } prevLengthRef.current = nextRows.length; }, [statisticsVerificationResultGridRows]); const isLoading = statisticsVerificationReportQuery.isFetching || statisticsVerificationResultQuery.isFetching; const isAppendingDetail = statisticsVerificationResultQuery.isFetchingNextPage; const canLoadMoreDetail = Boolean( statisticsVerificationResultQuery.hasNextPage, ); const { handleItemChecked: handleReportItemChecked, handleAllChecked: handleReportAllChecked, } = useGridCheckboxSelection( statisticsVerificationReportData, isSameStatsInputStatusRow, setSelectedReportRows, ); const { handleItemChecked: handleResultItemChecked, handleAllChecked: handleResultAllChecked, } = useGridCheckboxSelection( statisticsVerificationResultData, isSameVerifyResultRow, setSelectedResultRows, ); const { mutateAsync: onApproveStats } = useMutationWrapper({ mutationFn: (approvalRequests: ApproveRequest[]) => statisticsApprovalApi.approveStats(approvalRequests), onSuccess: (data) => { alert(data.message ?? "승인이 완료되었습니다."); }, }); const { mutateAsync: onRejectStats } = useMutationWrapper({ mutationFn: (rejectRequests: RejectRequest[]) => statisticsApprovalApi.rejectStats(rejectRequests), onSuccess: (data) => { alert(data.message ?? "반려가 완료되었습니다."); }, }); useEffect(() => { if (!statisticsVerificationReportQuery.isError) return; alert("통계검증리포트 조회 중 오류가 발생했습니다."); setSelectedReportRows([]); }, [ alert, statisticsVerificationReportQuery.errorUpdatedAt, statisticsVerificationReportQuery.isError, ]); function resetResultState() { setSelectedResultRows([]); editedResultRowMapRef.current = new Map(); prevLengthRef.current = 0; resultGridRef.current?.dataProvider?.setRows?.([]); } function resetReportState() { setSelectedReportRows([]); resetResultState(); } async function handleLoadMoreDetail() { if (isLoading || isAppendingDetail || !canLoadMoreDetail) return; try { await statisticsVerificationResultQuery.fetchNextPage(); } catch { alert("추가 데이터를 불러오는 중 오류가 발생했습니다."); } } function handleFilterChange< K extends keyof StatisticsVerificationFilterState, >(key: K) { return ( value?: StatisticsVerificationFilterState[K], optionData?: { label: string; value: string }, ) => { resetReportState(); if (key === "sewerBzmnCd") { setFilters((prev) => ({ ...prev, ctpvNm: optionData?.label, sewerBzmnCd: optionData?.value, sggNm: "", })); setSubFilters((prev) => ({ ...prev, ctpvNm: "", sggNm: "", sewerBzmnCd: "", })); return; } if (key === "statsCd") { setSubFilters((prev) => ({ ...prev, statsCd: "" })); } setFilters((prev) => ({ ...prev, [key]: value })); }; } function handleSubFilterChange< K extends keyof StatisticsVerificationSubFilterState, >(key: K) { return (value?: StatisticsVerificationSubFilterState[K]) => { resetResultState(); setSubFilters((prev) => ({ ...prev, [key]: value })); }; } function handleSubFilterReset() { setSubFilters(DEFAULT_SUB_FILTERS); resetResultState(); } function handleCellEdited(params: CellEditedCallbackParams) { const { itemIndex, fieldName, newValue } = params; const dataProvider = resultGridRef.current?.dataProvider as | { getValue?: (row: number, field: string) => string | number } | undefined; const vrfcSn = Number(dataProvider?.getValue?.(itemIndex, "vrfcSn")); if (Number.isNaN(vrfcSn)) return; editedResultRowMapRef.current?.set(vrfcSn, { ...(editedResultRowMapRef.current?.get(vrfcSn) ?? {}), [fieldName]: newValue, }); } function handleReset() { setFilters(DEFAULT_FILTERS); setSubFilters(DEFAULT_SUB_FILTERS); resetReportState(); } function handleApprove() { if (selectedReportRows.length === 0) return; setApprovalDialogOpen(true); } async function handleApprovalConfirm(formData: ApprovalFormData) { const commonParam = { statsYr: filters.statsYr, mdfrId: userInfo?.userUnqId ?? "", }; const approvalRequests = selectedReportRows.map((row) => ({ ...commonParam, ...formData, statsCd: row.statsCd ?? "", sewerBzmnCd: row.sewerBzmnCd ?? "", })); try { if (await confirm("승인하시겠습니까?")) { await onApproveStats(approvalRequests); setApprovalDialogOpen(false); } } catch { alert("승인 중 오류가 발생했습니다."); } } async function handleReject() { const commonParam = { statsYr: filters.statsYr, sewerBzmnCd: filters.sewerBzmnCd ?? "", statsCd: filters.statsCd ?? "", mdfrId: userInfo?.userUnqId ?? "", }; const requestRows = selectedResultRows.map((row) => { const vrfcSn = Number(row.vrfcSn); const editedRow = Number.isNaN(vrfcSn) ? undefined : editedResultRowMapRef.current?.get(vrfcSn); return { ...row, ...(editedRow ?? {}) }; }); const hasEmptyRjctOpnnCn = requestRows.some( (row) => !String(row?.rjctOpnnCn ?? "").trim(), ); if (hasEmptyRjctOpnnCn) { alert("선택한 항목의 반려의견을 작성해주세요."); return; } const rejectRequests = requestRows.map((row) => { const vrfcSn = Number(row.vrfcSn); return { ...commonParam, vrfcSn: Number.isNaN(vrfcSn) ? 0 : vrfcSn, rjctOpnnCn: String(row.rjctOpnnCn ?? ""), }; }); try { if (await confirm("반려하시겠습니까?")) { await onRejectStats(rejectRequests); } } catch { alert("반려 중 오류가 발생했습니다."); } } return (
({ label: item.adstNm, value: item.adstCd, })) ?? [] } /> ({ label: item.adstNm, value: item.adstCd, })) ?? [] } />
{}} showHeaderCheckbox={true} gridHeight={30} onRowClick={(row: StatsInputStatusList) => { setSubFilters((prev) => ({ ...prev, ctpvNm: row?.ctpvNm ?? "", sggNm: row?.sggNm ?? "", sewerBzmnCd: row?.sewerBzmnCd ?? "", statsCd: row?.statsCd ?? "", })); }} />

검증 결과

{}} showHeaderCheckbox={true} onCellEdited={handleCellEdited} getGridRef={resultGridRef} /> {(isLoading || isAppendingDetail) && (
)}
); } interface FilterSelectProps { label: string; onChange: ( value?: string, optionData?: { label: string; value: string }, ) => void; options: Array<{ label: string; value: string }>; value?: string; } function FilterCombobox({ label, onChange, options, value, }: FilterSelectProps) { const anchor = useComboboxAnchor(); const items = useMemo( () => options.map((option) => option.value), [options], ); const selectedValues = useMemo(() => { if (!value) return []; return value.split(",").filter(Boolean); }, [value]); return (
{ if (next == null) { onChange(undefined); return; } const list = Array.isArray(next) ? next : [next]; onChange(list.length > 0 ? list.join(",") : undefined); }} > {(values: string[]) => ( <> {values.map((v) => ( {options.find((o) => o.value === v) ?.label ?? v} ))} )} 검색 결과가 없습니다. {(item: string) => ( {options.find((o) => o.value === item)?.label ?? item} )}
); } function FilterSelect({ label, onChange, options, value }: FilterSelectProps) { return (
); } export default StatisticsVerificationReport;