From 9f9c6f30cb43b5c8c3064c703d7543c446d05da0 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 4 May 2026 18:04:32 +0900 Subject: [PATCH] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20=EB=AF=B8?= =?UTF-8?q?=ED=86=B5=EA=B3=BC=20=EB=82=B4=EC=97=AD=20-=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=94=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/VerificationResultDialog.tsx | 470 +++++++++++++++++- .../VerificationSelectionReport.tsx | 164 +++++- .../statistics/input/SewagePipeInp.tsx | 70 ++- .../input/SewagePipeMaintainInp.tsx | 63 ++- .../input/SeweragePenetrationRateInp.tsx | 61 ++- .../statistics/input/SubmitDailog.tsx | 6 +- apps/st/src/components/util/MenuMatchList.ts | 33 ++ .../statistics/api/statistics-service.ts | 10 + .../statistics/model/statistics-interface.ts | 21 +- .../shared/lib/stats-cd-input-navigation.ts | 99 ++++ .../src/store/statistics-input-focus-store.ts | 41 ++ 11 files changed, 998 insertions(+), 40 deletions(-) create mode 100644 apps/st/src/components/util/MenuMatchList.ts create mode 100644 apps/st/src/shared/lib/stats-cd-input-navigation.ts create mode 100644 apps/st/src/store/statistics-input-focus-store.ts diff --git a/apps/st/src/components/module/VerificationResultDialog.tsx b/apps/st/src/components/module/VerificationResultDialog.tsx index 5e11dd6ac..fda48ba75 100644 --- a/apps/st/src/components/module/VerificationResultDialog.tsx +++ b/apps/st/src/components/module/VerificationResultDialog.tsx @@ -1,5 +1,16 @@ "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, @@ -8,25 +19,477 @@ import { 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 { 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; } export function VerificationResultDialog({ open, onOpenChange, + queryContext, + title = "검증 결과", + emptyMessage = "표시할 검증 결과가 없습니다.", }: 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); + 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; + /** DataGrid는 onCellEdited 시 React rowData를 갱신하지 않아, 제출용 상태를 여기서 맞춤 */ + setNormalRows((prev) => { + const next = [...prev]; + const row = next[params.itemIndex]; + if (!row) return prev; + next[params.itemIndex] = { ...row, prcsOpnnExpln: text }; + return next; + }); + 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; + setImrRows((prev) => { + const next = [...prev]; + const row = next[params.itemIndex]; + if (!row) return prev; + next[params.itemIndex] = { ...row, prcsOpnnExpln: text }; + return next; + }); + 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(() => { + const payload: VerificationExceptionSubmitItem[] = []; + const pushIfHasExpln = (row: VerificationResultGridRow | undefined) => { + if (!row) return; + const prcsOpnnExpln = String(row.prcsOpnnExpln ?? "").trim(); + if (!prcsOpnnExpln) return; + payload.push({ + vrfcSn: Number(row.vrfcSn), + prcsOpnnExpln, + }); + }; + normalCheckedIndices.forEach((idx) => pushIfHasExpln(normalRows[idx])); + imrCheckedIndices.forEach((idx) => pushIfHasExpln(imrRows[idx])); + if (payload.length === 0) { + alert("작성된 예외 제출 항목이 없습니다."); + return; + } + console.log(payload); + }, [alert, imrCheckedIndices, imrRows, normalCheckedIndices, normalRows]); + + const renderGrid = ( + rows: VerificationResultGridRow[], + setRows: Dispatch>, + pageIndex: number, + pageCount: number, + onPageIndexChange: (page: number) => void, + loading: boolean, + gridRef: MutableRefObject, + onPrcsOpnnExplnCellEdited: (params: CellEditedCallbackParams) => void, + setCheckedIndices: Dispatch>>, + ) => { + if (!loading && rows.length === 0) { + return

{emptyMessage}

; + } + + return ( +
+ []>>} + isExcelImport={false} + isExcelExport={false} + showHeaderCheckbox + enableSort={false} + getGridRef={gridRef} + 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, + onNormalPrcsOpnnExplnEdited, + setNormalCheckedIndices, + )} +
+
+

I-MR 검증내역

+ {renderGrid( + imrRows, + setImrRows, + imrPageIndex, + imrPageCount, + handleImrPageChange, + imrLoading, + imrGridRef, + onImrPrcsOpnnExplnEdited, + setImrCheckedIndices, + )} +
+
+
); } - diff --git a/apps/st/src/components/statistics/VerificationSelectionReport.tsx b/apps/st/src/components/statistics/VerificationSelectionReport.tsx index 65b4a8a20..8a06cebec 100644 --- a/apps/st/src/components/statistics/VerificationSelectionReport.tsx +++ b/apps/st/src/components/statistics/VerificationSelectionReport.tsx @@ -1,9 +1,19 @@ "use client"; -import { useDashboardVrfcReport } from "@/entities/statistics"; -import dayjs from "dayjs"; +import { useCallback } from "react"; +import { useQueries } from "@tanstack/react-query"; +import { useTabStore as useGridTabStore } from "@rsone/components"; -import { useUserStore } from "@/shared/lib"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { VerificationResultReportItem } from "@/entities/statistics/model/statistics-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, @@ -28,20 +38,119 @@ type VerificationSelectionReportProps = { const VerificationSelectionReport = ({ onClose, }: VerificationSelectionReportProps) => { - const userInfo = useUserStore((state) => state.userInfo); + 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 param = { - statsYr: String(dayjs().year() - 1), - dtlInstCd: userInfo?.dtlInstCd || '', - enoCd: userInfo?.enoCd || '', - laeSeCd: userInfo?.laeSeCd || '', + const statsYr = String(dayjs().year() - 1); + + const baseRequest = { + statsYr, + pbcpAprvYn: "N" as const, + page: 1, + size: 10000, + prcsYnList: "0", }; - const { data, isLoading, error } = useDashboardVrfcReport(param); - const imrList = data?.list.filter((item) => item.vrfcCd === "IMR") ?? []; - const nvList = data?.list.filter((item) => item.vrfcCd === "NV") ?? []; + const [nvQuery, imrQuery] = useQueries({ + queries: [ + { + queryKey: [ + "verificationResultList", + statsYr, + "NV", + baseRequest.pbcpAprvYn, + baseRequest.page, + baseRequest.size, + baseRequest.prcsYnList, + ], + queryFn: () => + statisticsApi.getVerificationResultList({ + ...baseRequest, + vrfcMthdCd: "NV", + }), + }, + { + queryKey: [ + "verificationResultList", + statsYr, + "IMR", + baseRequest.pbcpAprvYn, + baseRequest.page, + baseRequest.size, + baseRequest.prcsYnList, + ], + queryFn: () => + statisticsApi.getVerificationResultList({ + ...baseRequest, + vrfcMthdCd: "IMR", + }), + }, + ], + }); - const renderSection = (typeCode: "IMR" | "NV", items: typeof imrList) => ( + const isLoading = nvQuery.isLoading || imrQuery.isLoading; + const error = nvQuery.error ?? imrQuery.error; + + const nvList = nvQuery.data?.data?.verifyResultList ?? []; + const imrList = imrQuery.data?.data?.verifyResultList ?? []; + + const openInputFromStatsCd = useCallback( + (item: VerificationResultReportItem) => { + 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 renderSection = ( + typeCode: "IMR" | "NV", + items: VerificationResultReportItem[], + ) => (
@@ -55,20 +164,33 @@ const VerificationSelectionReport = ({
) : (
- {items.map((item, index) => ( + {items.map((item) => (
openInputFromStatsCd(item)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + openInputFromStatsCd(item); + } + }} >
{item.statsNm} - {TYPE_CODE_NAME_MAP[item.vrfcCd]} + {TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ?? + item.vrfcMthdNm}

@@ -112,18 +234,18 @@ const VerificationSelectionReport = ({

{isLoading && ( -
+
)} {error && ( -
+
데이터를 불러올 수 없습니다.
)} - {data && ( + {!isLoading && !error && (
{renderSection("IMR", imrList)} {renderSection("NV", nvList)} @@ -135,4 +257,4 @@ const VerificationSelectionReport = ({ ); }; -export default VerificationSelectionReport; \ No newline at end of file +export default VerificationSelectionReport; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 77d4484ca..7bd155cc7 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useRef, useEffect } from "react"; +import { useState, useMemo, useRef, useEffect, useLayoutEffect } from "react"; import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; @@ -15,6 +15,10 @@ import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewa import type { ColumnInfo, ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; +import { + STATISTICS_INPUT_URL_PATH, + verificationSearchToFormValues, +} from "@/shared/lib/stats-cd-input-navigation"; import { useAdminStore } from "@/store/admin-store"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; @@ -39,6 +43,8 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from " import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { cancelSubmitRequest } from "@/entities/statistics/model/statistics-interface"; +import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; +import { useTabStore } from "@/store/tab-store"; /** 탭별 status + 입력기간으로 편집 가능 여부 */ const computeTabEnterable = ( @@ -164,6 +170,14 @@ const SewagePipeInp = () => { /** 검색 완료 후(API 연결 전) 탭별 지연 로그용 — 첫 탭에 행이 없어도 동작하도록 보조 */ const searchDoneRef = useRef(false); const lazyTabLoggedRef = useRef>(new Set()); + const postVerificationSearchRef = useRef<{ + values: SearchFormValues; + tabKey: string; + } | null>(null); + + const [verificationSearchDefaults, setVerificationSearchDefaults] = + useState(null); + const [searchFormRemountKey, setSearchFormRemountKey] = useState(0); const firstTabKey = SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001"; const activeTabRef = useRef(firstTabKey); @@ -172,6 +186,25 @@ const SewagePipeInp = () => { activeTabRef.current = activeTab; }, [activeTab]); + const activeTabId = useTabStore((s) => s.activeTabId); + const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce); + useLayoutEffect(() => { + if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return; + const pending = useStatisticsInputFocusStore + .getState() + .takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE); + if (!pending) return; + const { tabKey, search } = pending; + activeTabRef.current = tabKey; + setActiveTab(tabKey); + if (search) { + const values = verificationSearchToFormValues(search); + setVerificationSearchDefaults(values); + postVerificationSearchRef.current = { values, tabKey }; + setSearchFormRemountKey((k) => k + 1); + } + }, [activeTabId, inputFocusNonce]); + const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => { if (loadedTabKey === activeTabRef.current) { setStatsCd(nextStatsCd); @@ -195,6 +228,8 @@ const SewagePipeInp = () => { setActiveTab(firstTabKey); setStatsCd(""); setPeriodCheckList([]); + setVerificationSearchDefaults(null); + setSearchFormRemountKey((k) => k + 1); }; const columnInfo = useMemo(() => { @@ -424,7 +459,10 @@ const SewagePipeInp = () => { return true; }; - const handleSearch = async (values: SearchFormValues) => { + const handleSearch = async ( + values: SearchFormValues, + options?: { forcedTabKey?: string }, + ) => { if (!values.sido || values.sido === 'ALL') { alert('시도를 선택해주세요.'); return; @@ -466,9 +504,17 @@ const SewagePipeInp = () => { setHasSearched(true); setLastSearchedYear(values.year ?? null); - const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTab; + const forcedTab = options?.forcedTabKey; + const tabKeyToLoad = forcedTab + ? forcedTab + : yearChangedFromLastSearch + ? firstTabKey + : activeTab; - if (yearChangedFromLastSearch) { + if (forcedTab) { + activeTabRef.current = forcedTab; + setActiveTab(forcedTab); + } else if (yearChangedFromLastSearch) { activeTabRef.current = firstTabKey; setActiveTab(firstTabKey); } else { @@ -482,6 +528,14 @@ const SewagePipeInp = () => { } }; + useEffect(() => { + const job = postVerificationSearchRef.current; + if (!job) return; + postVerificationSearchRef.current = null; + void handleSearch(job.values, { forcedTabKey: job.tabKey }); + // eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회 + }, [searchFormRemountKey]); + const handleActiveTabChange = async (tabKey: string) => { activeTabRef.current = tabKey; setActiveTab(tabKey); @@ -826,7 +880,13 @@ const SewagePipeInp = () => { return ( <> - + {showActionToolbarCard && (
diff --git a/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx b/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx index cddd6cc84..154318500 100644 --- a/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx @@ -1,6 +1,10 @@ -import { useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { useAlertStore } from "@/shared/lib"; +import { + STATISTICS_INPUT_URL_PATH, + verificationSearchToFormValues, +} from "@/shared/lib/stats-cd-input-navigation"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { sewagePipeMaintainApiService } from "@/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service"; import type { SewagePipeMaintainRequestParams } from "@/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface"; @@ -16,6 +20,8 @@ import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn"; import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; +import { useTabStore } from "@/store/tab-store"; function buildSewagePipeMaintainSearchParam( values: SearchFormValues, @@ -40,6 +46,13 @@ const SewagePipeMaintainInp = () => { const [lastSearchedYear, setLastSearchedYear] = useState(null); const [hasSearched, setHasSearched] = useState(false); const searchParamRef = useRef(null); + const postVerificationSearchRef = useRef<{ + values: SearchFormValues; + tabKey: string; + } | null>(null); + const [verificationSearchDefaults, setVerificationSearchDefaults] = + useState(null); + const [searchFormRemountKey, setSearchFormRemountKey] = useState(0); // tab List const SewagePipeMaintainInpTabList = [ @@ -47,6 +60,24 @@ const SewagePipeMaintainInp = () => { PipeDredgingStatusInpColumn ]; + const activeTabId = useTabStore((s) => s.activeTabId); + const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce); + useLayoutEffect(() => { + if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return; + const pending = useStatisticsInputFocusStore + .getState() + .takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN); + if (!pending) return; + const { tabKey, search } = pending; + setActiveTab(tabKey); + if (search) { + const values = verificationSearchToFormValues(search); + setVerificationSearchDefaults(values); + postVerificationSearchRef.current = { values, tabKey }; + setSearchFormRemountKey((k) => k + 1); + } + }, [activeTabId, inputFocusNonce]); + const SubmitExceptionFunc = () => { console.log('submit exception'); // 예외 제출 로직 @@ -136,9 +167,14 @@ const SewagePipeMaintainInp = () => { setLastSearchedYear(null); setHasSearched(false); searchParamRef.current = null; + setVerificationSearchDefaults(null); + setSearchFormRemountKey((k) => k + 1); }; - const handleSearch = async (values: SearchFormValues) => { + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { if (!values.sido || values.sido === "ALL") { alert("시도를 선택해주세요."); return; @@ -151,13 +187,18 @@ const SewagePipeMaintainInp = () => { return; } + const tabForLoad = opts?.forcedTabKey ?? activeTab; + if (opts?.forcedTabKey) { + setActiveTab(opts.forcedTabKey); + } + setIsLoading(true); const searchParam = buildSewagePipeMaintainSearchParam(values); searchParamRef.current = searchParam; setLastSearchedValues(values); setLastSearchedYear(values.year ?? null); setHasSearched(true); - await loadActiveTabData(activeTab, searchParam, false); + await loadActiveTabData(tabForLoad, searchParam, false); } catch { alert("데이터 조회 중 오류가 발생했습니다."); } finally { @@ -165,6 +206,14 @@ const SewagePipeMaintainInp = () => { } }; + useEffect(() => { + const job = postVerificationSearchRef.current; + if (!job) return; + postVerificationSearchRef.current = null; + void handleSearch(job.values, { forcedTabKey: job.tabKey }); + // eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회 + }, [searchFormRemountKey]); + const handleActiveTabChange = async (tabKey: string) => { setActiveTab(tabKey); const hasRows = (rowDataMap.get(tabKey) ?? []).length > 0; @@ -183,7 +232,13 @@ const SewagePipeMaintainInp = () => { return ( <> - + {(hasRowData || hasSearched) && (
diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index c31a4e060..d79bc6f0c 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useRef } from "react"; +import { useState, useMemo, useRef, useLayoutEffect, useEffect } from "react"; import SearchForm from "@/components/module/SearchForm"; // import { DataGrid } from "@rsone/components"; @@ -15,6 +15,10 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import SubmitDailog from "./SubmitDailog"; import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; +import { + STATISTICS_INPUT_URL_PATH, + verificationSearchToFormValues, +} from "@/shared/lib/stats-cd-input-navigation"; import { useAdminStore } from "@/store/admin-store"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; @@ -43,6 +47,8 @@ import type { import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; import { doList } from "@/components/util/column/input/DoList"; +import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; +import { useTabStore } from "@/store/tab-store"; type GridRowWithVerification = Record & { @@ -118,6 +124,27 @@ const SeweragePenetrationRateInp = () => { const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); const globalProgressVisible = useGlobalProgressStore((state) => state.visible); + const postVerificationSearchRef = useRef(null); + const [verificationSearchDefaults, setVerificationSearchDefaults] = + useState(null); + const [searchFormRemountKey, setSearchFormRemountKey] = useState(0); + + const activeTabId = useTabStore((s) => s.activeTabId); + const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce); + useLayoutEffect(() => { + if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) { + return; + } + const pending = useStatisticsInputFocusStore + .getState() + .takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE); + if (!pending?.search) return; + const values = verificationSearchToFormValues(pending.search); + setVerificationSearchDefaults(values); + postVerificationSearchRef.current = values; + setSearchFormRemountKey((k) => k + 1); + }, [activeTabId, inputFocusNonce]); + const { isAdminInfo } = useAdminStore(); const getGridDataRef = useRef<(() => Record[]) | null>(null); @@ -156,6 +183,8 @@ const SeweragePenetrationRateInp = () => { setLastSearchedYear(null); setLastSearchedValues(null); setFooterFromApi(null); + setVerificationSearchDefaults(null); + setSearchFormRemountKey((k) => k + 1); }; const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => { @@ -238,6 +267,14 @@ const SeweragePenetrationRateInp = () => { } }; + useEffect(() => { + const values = postVerificationSearchRef.current; + if (!values) return; + postVerificationSearchRef.current = null; + void handleSearch(values); + // eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회 + }, [searchFormRemountKey]); + const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => { const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); if (verificationBlocked) { @@ -623,7 +660,13 @@ const SeweragePenetrationRateInp = () => { return ( <>
- + {showActionToolbarCard && (
@@ -713,6 +756,20 @@ const SeweragePenetrationRateInp = () => { ); diff --git a/apps/st/src/components/statistics/input/SubmitDailog.tsx b/apps/st/src/components/statistics/input/SubmitDailog.tsx index 65f2f9306..856ef089c 100644 --- a/apps/st/src/components/statistics/input/SubmitDailog.tsx +++ b/apps/st/src/components/statistics/input/SubmitDailog.tsx @@ -175,12 +175,12 @@ const SubmitDailog = ({ - + diff --git a/apps/st/src/components/util/MenuMatchList.ts b/apps/st/src/components/util/MenuMatchList.ts new file mode 100644 index 000000000..ded92e5a4 --- /dev/null +++ b/apps/st/src/components/util/MenuMatchList.ts @@ -0,0 +1,33 @@ +/** + * statistics/input 화면과 탭(·분할 그리드) 키 매칭용 상수. + * 각 컴포넌트에 실제로 쓰이는 `tabKey`를 모두 포함합니다. + */ + +/** `SewagePipeInp.tsx` — 메인 탭 + `ConnectionStatusInpColumn` 좌·우 그리드 서브 키 */ +export const SEWAGE_PIPE_INP_TAB_KEYS = [ + "ST00020001", + "ST00020002", + "ST00020003", + "ST00020004", + "ST00020005", + "ST00020006", + "treatmentPlantConnectionInp", + "adminAreaConnectionInp", +] as const; + +/** `SewagePipeMaintainInp.tsx` — 보수·준설 탭 */ +export const SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS = ["ST00030001", "ST00030002"] as const; + +/** `SeweragePenetrationRateInp.tsx` — 단일 그리드(통계코드) */ +export const SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS = ["ST00010001"] as const; + +export type SewagePipeInpTabKey = (typeof SEWAGE_PIPE_INP_TAB_KEYS)[number]; +export type SewagePipeMaintainInpTabKey = (typeof SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS)[number]; +export type SeweragePenetrationRateInpTabKey = (typeof SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS)[number]; + +/** 컴포넌트 파일명 기준 묶음 (메뉴·라우트 매칭 시 참고) */ +export const STATISTICS_INPUT_TAB_KEYS_BY_SCREEN = { + SewagePipeInp: SEWAGE_PIPE_INP_TAB_KEYS, + SewagePipeMaintainInp: SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS, + SeweragePenetrationRateInp: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS, +} as const; diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index b7aec86ac..37cd9e3a6 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -26,6 +26,8 @@ import { type SubmitExceptionResponse, type ImrResponse, type ImrStatusResponse, + type SubmitVerifyExceptionRequest, + type SubmitVerifyExceptionResponse, } from "../model/statistics-interface"; export class StatisticsApiService implements StatisticsService { @@ -164,6 +166,14 @@ export class StatisticsApiService implements StatisticsService { ); return response.data; } + + public async SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/exception`, + param, + ); + return response.data; + } } export const statisticsApi = new StatisticsApiService(); diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index 5c95fb8bf..00383e994 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -268,6 +268,24 @@ export interface ReviewSubmitResponse { message?: string; } +export interface SubmitVerifyExceptionRequest { + sewerBzmnCd: string; + statsYr: string; + statsCd: string; + userId: string; + exceptionList: [ + { + vrfcSn: number; + prcsOpnnExpln: string; + } + ]; +} + +export interface SubmitVerifyExceptionResponse { + status: string; + message?: string; +} + export interface StatisticsService { getSidoList(params?: { instSeCd?: string }): Promise>; getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise>; @@ -280,7 +298,8 @@ export interface StatisticsService { saveReviewOpinion(param: ReviewOpinionSaveRequest): Promise; getReviewOpinionDetail(param: getReviewOpinionDetailRequest): Promise; saveReviewReturn(param: ReviewReturnSaveRequest): Promise; - submitReview(param :ReviewSubmitRequest): Promise; + submitReview(param: ReviewSubmitRequest): Promise; getVerificationResultList(param: VerificationResultListRequest): Promise; getImrStatus(param: string): Promise; + SubmitVerifyException(param: SubmitVerifyExceptionRequest): 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 new file mode 100644 index 000000000..3acdf6f0b --- /dev/null +++ b/apps/st/src/shared/lib/stats-cd-input-navigation.ts @@ -0,0 +1,99 @@ +import type { MenuItem } from "@/entities/menu"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doNameList } from "@/components/util/column/input/DoList"; +import { + SEWAGE_PIPE_INP_TAB_KEYS, + SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS, + SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS, +} from "@/components/util/MenuMatchList"; + +/** 검증 결과에서 입력 화면 자동 조회에 사용하는 조건 */ +export type StatisticsInputPendingSearch = { + statsYr: string; + sewerBzmnCd: string; + ctpvNm: string; + sggNm: string; +}; + +export function verificationSearchToFormValues( + s: StatisticsInputPendingSearch, +): SearchFormValues { + const ctpvNmTrimmed = s.ctpvNm?.trim() ?? ""; + const sggNmTrimmed = s.sggNm?.trim() ?? ""; + /** 시군구 필수 도(`DoList.ts`의 `doNameList`)일 때만 API `sggNm`을 검색폼에 반영 */ + const useSggNm = + sggNmTrimmed.length > 0 && doNameList.includes(ctpvNmTrimmed); + + return { + year: s.statsYr, + bsnmCd: s.sewerBzmnCd, + sido: s.ctpvNm, + gugun: useSggNm ? sggNmTrimmed : "ALL", + }; +} + +/** `statistics-page` 동적 import 경로의 파일명(확장자 제외)과 일치해야 함 */ +export const STATISTICS_INPUT_URL_PATH = { + SEWAGE_PIPE: "SewagePipeInp", + SEWAGE_PIPE_MAINTAIN: "SewagePipeMaintainInp", + SEWERAGE_PENETRATION_RATE: "SeweragePenetrationRateInp", +} as const; + +function normalizeSewagePipeTabKey(tabKey: string): string { + if ( + tabKey === "adminAreaConnectionInp" || + tabKey === "treatmentPlantConnectionInp" + ) { + return "ST00020006"; + } + return tabKey; +} + +const NAV_ROWS: { urlPath: string; tabKeys: readonly string[] }[] = [ + { urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE, tabKeys: SEWAGE_PIPE_INP_TAB_KEYS }, + { + urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN, + tabKeys: SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS, + }, + { + urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE, + tabKeys: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS, + }, +]; + +/** + * 검증 결과 등의 `statsCd`(또는 입력 화면 탭 키와 동일한 코드)로 + * 열 입력 화면 `urlPath`와 활성 탭 키를 반환합니다. + */ +export function resolveInputPageNavigationFromStatsCd(statsCd: string): { + urlPath: string; + tabKey: string; +} | null { + const trimmed = statsCd.trim(); + if (!trimmed) return null; + + for (const row of NAV_ROWS) { + if (row.tabKeys.includes(trimmed)) { + const tabKey = + row.urlPath === STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE + ? normalizeSewagePipeTabKey(trimmed) + : trimmed; + return { urlPath: row.urlPath, tabKey }; + } + } + return null; +} + +export function findMenuItemByUrlPath( + menus: MenuItem[], + urlPath: string, +): MenuItem | null { + for (const item of menus) { + if (item.urlPath === urlPath) return item; + if (item.children?.length) { + const found = findMenuItemByUrlPath(item.children, urlPath); + if (found) return found; + } + } + return null; +} diff --git a/apps/st/src/store/statistics-input-focus-store.ts b/apps/st/src/store/statistics-input-focus-store.ts new file mode 100644 index 000000000..910aeed10 --- /dev/null +++ b/apps/st/src/store/statistics-input-focus-store.ts @@ -0,0 +1,41 @@ +import { create } from "zustand"; + +import type { StatisticsInputPendingSearch } from "@/shared/lib/stats-cd-input-navigation"; + +type Pending = { + urlPath: string; + tabKey: string; + search: StatisticsInputPendingSearch | null; +}; + +export interface StatisticsInputFocusStore { + pending: Pending | null; + /** `setPendingInputTab` 호출 시마다 증가 — 이미 해당 화면이 활성일 때도 탭 적용 effect가 돌도록 함 */ + nonce: number; + setPendingInputTab: ( + urlPath: string, + tabKey: string, + search?: StatisticsInputPendingSearch | null, + ) => void; + takePendingForUrl: ( + urlPath: string, + ) => { tabKey: string; search: StatisticsInputPendingSearch | null } | undefined; +} + +export const useStatisticsInputFocusStore = create( + (set, get) => ({ + pending: null, + nonce: 0, + setPendingInputTab: (urlPath, tabKey, search = null) => + set((state) => ({ + pending: { urlPath, tabKey, search: search ?? null }, + nonce: state.nonce + 1, + })), + takePendingForUrl: (urlPath) => { + const p = get().pending; + if (!p || p.urlPath !== urlPath) return undefined; + set({ pending: null }); + return { tabKey: p.tabKey, search: p.search }; + }, + }), +);