"use client"; import { useCallback, useEffect, useState } from "react"; import { type InfiniteData, keepPreviousData, useInfiniteQuery, useQueryClient, } from "@tanstack/react-query"; import { useTabStore as useGridTabStore } from "@rsone/components"; import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service"; import type { VerificationInputReportItem, VerificationInputReportResponse, VerificationInputReportTypeCountingInfo, } from "@/entities/statistics/model/verification-input-report-interface"; import { useAlertStore, useMenuStore } from "@/shared/lib"; import { findMenuItemByUrlPath, resolveInputPageNavigationFromStatsCd, } from "@/shared/lib/stats-cd-input-navigation"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useTabStore } from "@/store/tab-store"; import dayjs from "dayjs"; import { Badge, type BadgeProps, LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger, Label, } from "@common/components"; import { CodeSelectWithAll } from "@/entities/code"; 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", }; const PAGE_SIZE = 100; const SCROLL_LOAD_THRESHOLD_PX = 120; function getNextVerificationInputReportPage( lastPage: VerificationInputReportResponse, ): number | undefined { const pageInfo = lastPage?.data?.pageInfo; if (!pageInfo) return undefined; const { currentPage, totalPages } = pageInfo; if (currentPage + 1 <= totalPages) return currentPage + 1; return undefined; } function flattenVerificationInputReportResults( data: InfiniteData | undefined, ): VerificationInputReportItem[] { return ( data?.pages.flatMap((page) => page?.data?.verifyResultList ?? []) ?? [] ); } function getVerifyTypeCountingInfo( data: InfiniteData | undefined, ): VerificationInputReportTypeCountingInfo | null { return data?.pages[0]?.data?.verifyTypeCountingInfo ?? null; } function formatReportItemTitle(item: VerificationInputReportItem): string { return item.statsNm?.trim() || item.vrfcTrgtNm?.trim() || item.statsCd; } function formatReportItemRegion(item: VerificationInputReportItem): string { const parts = [item.ctpvNm, item.sggNm].filter( (v): v is string => Boolean(v?.trim()), ); return parts.length > 0 ? parts.join(" ") : "-"; } function useVerificationReportList( statsYr: string, statsCdQueryKey: string, vrfcMthdCd: VerificationReportTabKey, statsCd: string, enabled: boolean, ) { return useInfiniteQuery({ queryKey: [ "verificationInputReport", statsYr, vrfcMthdCd, statsCdQueryKey, ], initialPageParam: 1, queryFn: ({ pageParam }) => verificationInputReportApi.getVerificationInputReport({ statsYr, vrfcMthdCd, page: pageParam, size: PAGE_SIZE, ...(!(statsCd === "" || statsCd === "ALL") && { statsCd }), }), getNextPageParam: getNextVerificationInputReportPage, placeholderData: keepPreviousData, refetchOnWindowFocus: false, // 탭 전환만으로는 재조회하지 않음(명시적 새로고침/조건 변경 시만 조회) staleTime: Infinity, refetchOnMount: false, enabled, }); } type VerificationSelectionReportProps = { onClose?: () => void; }; const VerificationSelectionReport = ({ onClose, }: VerificationSelectionReportProps) => { const queryClient = useQueryClient(); const menuList = useTabStore((s) => s.menuList); const addTab = useTabStore((s) => s.addTab); const setCurrentMenuInfo = useMenuStore((s) => s.setCurrentMenuInfo); const addTabForGrid = useGridTabStore((s) => s.addTab); const setPendingInputTab = useStatisticsInputFocusStore( (s) => s.setPendingInputTab, ); const alert = useAlertStore((s) => s.setMessage); const [activeTab, setActiveTab] = useState("NV"); const [statsCd, setStatsCd] = useState("ALL"); const [isFullReloading, setIsFullReloading] = useState(false); const statsYr = String(dayjs().year() - 1); const statsCdQueryKey = statsCd === "" || statsCd === "ALL" ? "" : statsCd; const handleStatsCdChange = useCallback( (nextStatsCd: string) => { setStatsCd(nextStatsCd); setActiveTab("NV"); setIsFullReloading(true); // 통계항목 변경 시 모든 탭 캐시 제거 후 NV부터 다시 조회 void queryClient.removeQueries({ queryKey: ["verificationInputReport", statsYr], }); }, [queryClient, statsYr], ); const nvQuery = useVerificationReportList( statsYr, statsCdQueryKey, "NV", statsCd, activeTab === "NV", ); const imrQuery = useVerificationReportList( statsYr, statsCdQueryKey, "IMR", statsCd, activeTab === "IMR", ); const hbQuery = useVerificationReportList( statsYr, statsCdQueryKey, "HB", statsCd, activeTab === "HB", ); const queryByTab = { NV: nvQuery, IMR: imrQuery, HB: hbQuery, } as const; const activeQuery = queryByTab[activeTab]; const isRefreshing = activeQuery.isFetching && !activeQuery.isPending; const error = activeQuery.error; /** 통계항목 변경·최초 진입: 탭 영역 숨기고 카드 전체 로딩만 표시 */ const hideTabsSection = isFullReloading || (activeTab === "NV" && !nvQuery.data && (nvQuery.isPending || nvQuery.isFetching)); useEffect(() => { if (!isFullReloading) return; if (nvQuery.isFetching) return; if (nvQuery.isSuccess || nvQuery.isError) { setIsFullReloading(false); } }, [isFullReloading, nvQuery.isFetching, nvQuery.isSuccess, nvQuery.isError]); const handleRefresh = useCallback(() => { setStatsCd("ALL"); setActiveTab("NV"); setIsFullReloading(true); void queryClient.removeQueries({ queryKey: ["verificationInputReport", statsYr], }); }, [queryClient, statsYr]); const listsByTab: Record = { NV: flattenVerificationInputReportResults(nvQuery.data), IMR: flattenVerificationInputReportResults(imrQuery.data), HB: flattenVerificationInputReportResults(hbQuery.data), }; const typeCountingInfo = getVerifyTypeCountingInfo(nvQuery.data) ?? getVerifyTypeCountingInfo(imrQuery.data) ?? getVerifyTypeCountingInfo(hbQuery.data); const totalCountByTab: Record = { NV: typeCountingInfo?.nvCount ?? listsByTab.NV.length, IMR: typeCountingInfo?.imrCount ?? listsByTab.IMR.length, HB: typeCountingInfo?.hbCount ?? listsByTab.HB.length, }; const openInputFromStatsCd = useCallback( (item: VerificationInputReportItem) => { const nav = resolveInputPageNavigationFromStatsCd(item.statsCd); if (!nav) { alert( `입력 화면으로 연결되지 않는 통계코드입니다: ${item.statsCd}`, ); return; } if (!menuList) { alert( "메뉴 정보를 불러오는 중입니다. 잠시 후 다시 시도해 주세요.", ); return; } const menu = findMenuItemByUrlPath(menuList, nav.urlPath); if (!menu) { alert( "해당 입력 메뉴를 찾을 수 없습니다. 메뉴 권한을 확인해 주세요.", ); return; } setPendingInputTab(nav.urlPath, nav.tabKey, { statsYr: item.statsYr, sewerBzmnCd: item.sewerBzmnCd, ctpvNm: item.ctpvNm, sggNm: item.sggNm ?? "", }); setCurrentMenuInfo(menu); const ok = addTab(menu); if (ok) { addTabForGrid(menu); } else { alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다. 기존 화면을 닫은 후 이용해주세요.`); } }, [ menuList, addTab, addTabForGrid, setCurrentMenuInfo, setPendingInputTab, alert, ], ); const handleListScroll = useCallback( ( e: React.UIEvent, hasNextPage: boolean, isFetchingNextPage: boolean, fetchNextPage: () => void, ) => { const el = e.currentTarget; const remaining = el.scrollHeight - el.scrollTop - el.clientHeight; if (remaining > SCROLL_LOAD_THRESHOLD_PX) return; if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, [], ); const isTabContentLoading = (tab: VerificationReportTabKey) => { const query = queryByTab[tab]; return ( activeTab === tab && !hideTabsSection && (query.isPending || (query.isFetching && !query.isFetchingNextPage) || (!query.data && !query.error)) ); }; const renderResultList = ( typeCode: VerificationReportTabKey, items: VerificationInputReportItem[], isTabLoading: boolean, hasNextPage: boolean, isFetchingNextPage: boolean, fetchNextPage: () => void, ) => (
handleListScroll( e, hasNextPage, isFetchingNextPage, fetchNextPage, ) } > {isTabLoading ? (
) : items.length === 0 && !isFetchingNextPage ? (
항목이 없습니다.
) : (
{items.map((item) => (
openInputFromStatsCd(item)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openInputFromStatsCd(item); } }} >
{formatReportItemTitle(item)}
{TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ?? item.vrfcMthdNm}

{item.rsnExpln}

{formatReportItemRegion(item)} 현재: {item.nowVlCn ?? "-"} / 비교:{" "} {item.compVlCn ?? "-"}
))} {isFetchingNextPage && (
)}
)}
); return (

이상치 선별 보고서

{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleRefresh(); } }} >
onClose?.()} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClose?.(); } }} >
{hideTabsSection && (
)} {!hideTabsSection && error && (
데이터를 불러올 수 없습니다.
)} {!hideTabsSection && !error && (
setActiveTab(value as VerificationReportTabKey) } className="flex min-h-0 flex-1 flex-col" variant="default" > {VERIFICATION_REPORT_TABS.map((tab) => ( {tab.label} ({totalCountByTab[tab.value]}) ))} {VERIFICATION_REPORT_TABS.map((tab) => ( {renderResultList( tab.value, listsByTab[tab.value], isTabContentLoading(tab.value), queryByTab[tab.value].hasNextPage ?? false, queryByTab[tab.value].isFetchingNextPage, queryByTab[tab.value].fetchNextPage, )} ))}
)}
); }; export default VerificationSelectionReport;