diff --git a/apps/st/app/(main)/(with-base-layout)/layout.tsx b/apps/st/app/(main)/(with-base-layout)/layout.tsx index 654a08534..1ba6133ea 100644 --- a/apps/st/app/(main)/(with-base-layout)/layout.tsx +++ b/apps/st/app/(main)/(with-base-layout)/layout.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { usePathname } from "next/navigation"; import { @@ -11,6 +11,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; +import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report"; import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout"; import { tokenManager } from "@/entities/auth"; import type { MenuItem } from "@/entities/menu"; @@ -20,6 +21,8 @@ import { getConfig } from "@common/config"; import { getTabStoreState, useTabStore } from "@/store/tab-store"; +import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport"; + const appConfig = getConfig(); const userId = getCookie(appConfig.AUTH_USER_ID ?? ""); const appSiteId = getCookie("site-id-key") ?? ""; @@ -33,6 +36,10 @@ export default function WithBaseLayout({ const pathname = usePathname(); const queryClient = useQueryClient(); + /** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */ + const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] = + useState(false); + const setCurrentMenuInfo = useMenuStore( (state) => state.setCurrentMenuInfo, ); @@ -145,6 +152,12 @@ export default function WithBaseLayout({ const handleMenuClick = useCallback( (item: MenuItem) => { setCurrentMenuInfo(item); + + if (isVerificationSelectionReportMenu(item)) { + setReportPanelOpen(true); + return; + } + const addTabResult: boolean = addTab(item); // packages/rsone의 스토어에도 동기화 @@ -153,11 +166,11 @@ export default function WithBaseLayout({ } if (!addTabResult) { - alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다. + alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다. 기존 화면을 닫은 후 이용해주세요.`); } }, - [setCurrentMenuInfo, addTab, addTabForGrid, alert], + [setCurrentMenuInfo, addTab, addTabForGrid, alert, setReportPanelOpen], ); return ( @@ -167,7 +180,14 @@ export default function WithBaseLayout({ currentPath={currentPath} handleMenuClick={handleMenuClick} > - {children} +
+ {isVerificationSelectionReportPanelOpen && ( + setReportPanelOpen(false)} + /> + )} + {children} +
); } diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index b48a6b6c3..cc938a282 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -250,6 +250,28 @@ const SearchForm = (props: SearchFormProps = {}) => { )} + {mergedProps.fctName && ( +
+ + + setValues({ + ...values, + fctName: e.target.value, + }) + } + autoComplete="off" + /> +
+ )} {mergedProps.isDetail && (
- {(mergedProps.fctName || mergedProps.userMenual) && ( -
- {mergedProps.fctName && ( -
- - - setValues({ - ...values, - fctName: e.target.value, - }) - } - autoComplete="off" - /> -
- )} - {mergedProps.userMenual && ( -
- -
- )} -
- )} ); 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 new file mode 100644 index 000000000..8a06cebec --- /dev/null +++ b/apps/st/src/components/statistics/VerificationSelectionReport.tsx @@ -0,0 +1,260 @@ +"use client"; + +import { useCallback } from "react"; +import { useQueries } from "@tanstack/react-query"; +import { useTabStore as useGridTabStore } from "@rsone/components"; + +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, + type BadgeProps, + LoadingWithAnimation, +} from "@common/components"; + +const TYPE_CODE_COLOR_MAP: Record = { + NV: "gray", + IMR: "red", +}; + +const TYPE_CODE_NAME_MAP: Record = { + NV: "일반", + IMR: "I-MR", +}; + +type VerificationSelectionReportProps = { + onClose?: () => void; +}; + +const VerificationSelectionReport = ({ + onClose, +}: VerificationSelectionReportProps) => { + 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 statsYr = String(dayjs().year() - 1); + + const baseRequest = { + statsYr, + pbcpAprvYn: "N" as const, + page: 1, + size: 10000, + prcsYnList: "0", + }; + + 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 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[], + ) => ( +
+
+ + {TYPE_CODE_NAME_MAP[typeCode]} {items.length} + +
+
+ {items.length === 0 ? ( +
+ 항목이 없습니다. +
+ ) : ( +
+ {items.map((item) => ( +
openInputFromStatsCd(item)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + openInputFromStatsCd(item); + } + }} + > +
+ + {item.statsNm} + + + {TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ?? + item.vrfcMthdNm} + +
+

+ {item.rsnExpln} +

+
+ {item.admdstNm} + + 현재: {item.nowVlCn ?? "-"} / 비교:{" "} + {item.compVlCn ?? "-"} + +
+
+ ))} +
+ )} +
+
+ ); + + return ( +
+
+
+

검증 미통과 내역

+ onClose?.()} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClose?.(); + } + }} + > + + +
+
+ {isLoading && ( +
+
+ +
+
+ )} + {error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {!isLoading && !error && ( +
+ {renderSection("IMR", imrList)} + {renderSection("NV", nvList)} +
+ )} +
+
+
+ ); +}; + +export default VerificationSelectionReport; diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index 0a8e1d277..fc3d1e66a 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -1,24 +1,20 @@ import { useState } from 'react'; import SearchForm from '@/components/module/SearchForm'; -import { DataGrid } from '@rsone/components'; +import DataGrid from '@/components/module/DataGrid'; +import SplitDataGrid from '@/components/module/SplitDataGrid'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@common/components'; import SubmitButton from '@/components/button/SumitButton'; import SaveButton from '@/components/button/SaveButton'; -import SubmitExceptionButton from '@/components/button/SubmitExceptionButton'; import OperateDataButton from '@/components/button/OperateDataButton'; -import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton" -import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton"; +import UpdateOperateDataButton from '@/components/button/UpdateOperateDataButton'; -import { PublicSewageFactInpColumn } from '@/components/util/column/input/PublicSewageFactInpColumn'; +import type { ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type'; +import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn'; // 하수도 통계 관리 > 06. 공공하수처리시설 const PublicSewageFacilityInp = () => { - - const SubmitExceptionFunc = () => { - console.log('submitException'); - // 예외제출 로직 - }; - + const submitFunc = () => { console.log('submit'); // 제출 로직 @@ -28,7 +24,7 @@ const PublicSewageFacilityInp = () => { console.log('save'); // 저장 로직 }; - + const getOperateDataFunc = () => { console.log('get operate data'); // 운영 데이터 가져오기 @@ -39,33 +35,109 @@ const PublicSewageFacilityInp = () => { // 운영 데이터 업데이트 로직 }; - const [rowData, setRowData] = useState[]>([]); - const [isNotApplicable, setIsNotApplicable] = useState(false); + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [activeTab, setActiveTab] = useState( + PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001', + ); + + const getGridColumnInfo = ( + tab: ColumnInfoList, + ): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], + }); return ( <>
- + +
-
-
- - - -
-
- + + + {PublicSewageFactInpColumnList.map((tab) => ( + + {tab.tabLabel || tab.tabKey} + + ))} + + + {PublicSewageFactInpColumnList.map((tab) => { + if (tab.isSplitGrid && tab.left && tab.right) { + const leftKey = tab.left.tabKey ?? `${tab.tabKey}_left`; + const rightKey = tab.right.tabKey ?? `${tab.tabKey}_right`; + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(leftKey) ?? []; + const n = + typeof updater === 'function' + ? (updater as (p: Record[]) => Record[])(cur) + : updater; + next.set(leftKey, n); + return next; + }); + }} + setRightRowData={(updater) => { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(rightKey) ?? []; + const n = + typeof updater === 'function' + ? (updater as (p: Record[]) => Record[])(cur) + : updater; + next.set(rightKey, n); + return next; + }); + }} + /> + + ); + } + + const gridColumnInfo = getGridColumnInfo(tab); + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tab.tabKey) ?? []; + const n = + typeof updater === 'function' + ? (updater as (p: Record[]) => Record[])(cur) + : updater; + next.set(tab.tabKey, n); + return next; + }); + }} + showHeaderCheckbox={false} + enableSort={false} + /> + + ); + })} + ); }; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 5ea57e205..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"; @@ -8,12 +8,17 @@ import SubmitButton from "@/components/button/SumitButton"; import SaveButton from "@/components/button/SaveButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn"; 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"; @@ -26,12 +31,20 @@ import type { SeweragePipeInputVerificationResult, SeweragePipeRequestParams, } from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface"; -import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import { + type CheckInputPeriodItem, + getVerificationPeriodActionBlockedMessage, + isInputPeriodCurrent, +} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn"; import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; 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 = ( @@ -133,6 +146,7 @@ const SewagePipeInp = () => { const [statsCd, setStatsCd] = useState(''); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [periodCheckList, setPeriodCheckList] = useState([]); /** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */ const [hasSearched, setHasSearched] = useState(false); /** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */ @@ -156,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); @@ -164,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); @@ -186,13 +227,15 @@ const SewagePipeInp = () => { activeTabRef.current = firstTabKey; setActiveTab(firstTabKey); setStatsCd(""); + setPeriodCheckList([]); + setVerificationSearchDefaults(null); + setSearchFormRemountKey((k) => k + 1); }; const columnInfo = useMemo(() => { return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => { const canEdit = - computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || - isAdminInfo; + computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo; const applyEditable = (cols: ColumnConfig[] | undefined) => (cols ?? []).map((col: ColumnConfig) => canEdit @@ -416,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; @@ -430,7 +476,9 @@ const SewagePipeInp = () => { return; } - const periodOk = isInputPeriodCurrent(periodResponse.data); + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + setPeriodCheckList(periods); + const periodOk = isInputPeriodCurrent(periods); periodEnterableRef.current = periodOk; setInputPeriodEnterable(periodOk); @@ -456,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 { @@ -472,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); @@ -524,10 +588,53 @@ const SewagePipeInp = () => { }; const submitFunc = () => { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } console.log('submit'); // 제출 로직 }; + const cnacelSubmitFunc = async () => { + try { + if (await confirm('제출된 내용을 취소하시겠습니까?')) { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } + + const userInfo = await getUserInfo(); + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: searchValues?.bsnmCd ?? '', + ctpvNm: searchValues?.sido ?? '', + sggNm: searchValues?.gugun ?? '', + statsYr: searchValues?.year ?? '', + mdfrId: userInfo.userUnqId, + statsCd: statsCd, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === 'success') { + const values = lastSearchedValues ?? searchValues; + if (values) { + const refreshedParam = buildSewagePipeSearchParam(values); + await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false); + } + alert('제출 취소되었습니다.'); + } else { + alert(response.message ?? '제출 취소 중 오류가 발생했습니다.'); + } + } + } catch { + alert('제출 취소 중 오류가 발생했습니다.'); + } + }; + const refreshConnectionStatusAfterPopupSave = async () => { const searchParam = searchParamRef.current; const values = lastSearchedValues ?? searchValues; @@ -545,6 +652,11 @@ const SewagePipeInp = () => { }; const saveFunc = async () => { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } if (!(await confirm("저장 하시겠습니까?"))) return; const values = lastSearchedValues ?? searchValues; @@ -743,10 +855,11 @@ const SewagePipeInp = () => { const gugunOkOrNotDoList = buildGugunOkForToolbar(lastSearchedValues); const searchContextForToolbar = lastSearchedValues ?? searchValues; + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabToolbar = toolbarSnapshotByTab.get(activeTab) ?? computeTabToolbarSnapshot( - tabStatusInfoMap.get(activeTab) ?? null, + activeTabStatus, tabRowDataExists(rowDataMap, activeTab), hasSearched, searchContextForToolbar, @@ -754,15 +867,26 @@ const SewagePipeInp = () => { isAdminInfo, ); + const hideToolbarForConnectionTab = activeTab === "ST00020006"; const { - showSaveInToolbar, - showEnterableToolbarButtons, - showActionToolbarCard, + showSaveInToolbar: baseShowSaveInToolbar, + showEnterableToolbarButtons: baseShowEnterableToolbarButtons, + showActionToolbarCard: baseShowActionToolbarCard, } = activeTabToolbar; + const showSaveInToolbar = !hideToolbarForConnectionTab && baseShowSaveInToolbar; + const showEnterableToolbarButtons = + !hideToolbarForConnectionTab && baseShowEnterableToolbarButtons; + const showActionToolbarCard = !hideToolbarForConnectionTab && baseShowActionToolbarCard; return ( <> - + {showActionToolbarCard && (
@@ -775,6 +899,9 @@ const SewagePipeInp = () => { )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} {showSaveInToolbar && }
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/components/util/column/input/PublicSewageFactInpColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn.ts deleted file mode 100644 index d070c9b9d..000000000 --- a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary"; -import { ValueType } from "realgrid"; - -/** - * 하수도 통계 관리 > 06. 공공하수처리시설 - * 단일 Tab - */ -export const PublicSewageFactInpColumn = { - columns: [ - // 행정구역 - { name: 'sidoCd', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'gugun', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 시설명 - { name: 'bscFctNm', fieldName: '시설명(500m³/일 이상/미만)', header: { text: '시설명(500m³/일 이상/미만)' }, editable: false }, - - // 소재지 - { name: 'addr', fieldName: '소재지', header: { text: '소재지' }, editable: false }, - - // 시설용량 - { name: 'fctCpc', fieldName: '시설용량(500m³/일 이상/미만)', header: { text: '시설용량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // (m³/일) - { name: 'fctCpc1', fieldName: '물리적', header: { text: '물리적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'fctCpc2', fieldName: '생물학적', header: { text: '생물학적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'fctCpc3', fieldName: '고도', header: { text: '고도' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 유입하수량 - { name: 'totInflAmntAve', fieldName: '유입하수량(500m³/일 이상/미만)', header: { text: '유입하수량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // (m³/일) - { name: 'pInflAmntAve', fieldName: '물리적', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'oInflAmntAve', fieldName: '생물학적', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'aInflAmntAve', fieldName: '고도', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 방류량 - { name: 'totTputAve', fieldName: '방류량(500m³/일 이상/미만)', header: { text: '방류량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 방류량 - (m³/일) - { name: 'pTputAve', fieldName: '물리적', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'oTputAve', fieldName: '생물학적', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'aTputAve', fieldName: '고도', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 수질 - { name: 'inflAssctBfrWqltBod', fieldName: '유입BOD', header: { text: '유입BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'efflWqltBod', fieldName: '방류BOD', header: { text: '방류BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'inflAssctBfrWqltToc', fieldName: '유입TOC', header: { text: '유입TOC' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'efflWqltToc', fieldName: '방류TOC', header: { text: '방류TOC' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'inflAssctBfrWqltSs', fieldName: '유입SS', header: { text: '유입SS' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'efflWqltSs', fieldName: '방류SS', header: { text: '방류SS' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'inflAssctBfrWqltTn', fieldName: '유입T-N', header: { text: '유입T-N' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'efflWqltTn', fieldName: '방류T-N', header: { text: '방류T-N' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'inflAssctBfrWqltTp', fieldName: '유입T-P', header: { text: '유입T-P' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'efflWqltTp', fieldName: '방류T-P', header: { text: '방류T-P' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'inflAssctBfrWqltClfgrpcnt', fieldName: '유입총대장균군수', header: { text: '유입총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'efflWqltClfgrpcnt', fieldName: '방류총대장균군수', header: { text: '방류총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 처리특성 - { name: 'tmEfficiency', fieldName: '처리효율(%)', header: { text: '처리효율(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'pcLoad', fieldName: '처리부하량(kgBOD/D)', header: { text: '처리부하량(kgBOD/D)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'trtFrfHanNm', fieldName: '처리방법', header: { text: '처리방법' }, editable: false }, - - // 연계처리량 - { name: 'excretionsTot', fieldName: '분뇨', header: { text: '분뇨' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'stkrsngWstwtrTot', fieldName: '축산', header: { text: '축산' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'lchtTot', fieldName: '침출수', header: { text: '침출수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'etcTot', fieldName: '기타', header: { text: '기타' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 시설정보 - { name: 'tel', fieldName: '전화번호', header: { text: '전화번호' }, editable: false }, - { name: 'rnDt', fieldName: '준공일', header: { text: '준공일' }, editable: false }, - { name: 'compDt', fieldName: '가동개시일', header: { text: '가동개시일' }, editable: false }, - { name: 'tot', fieldName: '사업비', header: { text: '사업비' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 방류수 소독방법 - { name: 'dsfctNm', fieldName: '방류수 소독방법', header: { text: '방류수 소독방법' }, editable: false }, - - // 방류수역 - { name: 'wbsnNm', fieldName: '수계', header: { text: '수계' }, editable: false }, - { name: 'rvNm', fieldName: '지류', header: { text: '지류' }, editable: false }, - { name: 'mdmbsn', fieldName: '세부단위구역', header: { text: '세부단위구역' }, editable: false }, - { name: 'efflWqltStndrdNm', fieldName: '지역구분', header: { text: '지역구분' }, editable: false }, - - // 방류수역 > 중권역 - { name: 'lgbsn', fieldName: '명칭', header: { text: '명칭' }, editable: false }, - { name: 'lgbsnBod', fieldName: '목표수질(BOD)', header: { text: '목표수질(BOD)' }, editable: false }, - - // 에너지 자립률 - { name: 'toeRt', fieldName: '에너지자립률(%)', header: { text: '에너지자립률(%)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 에너지 자체 생산량 - { name: 'useQnty', fieldName: '에너지자체생산량(TOE)', header: { text: '에너지자체생산량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 연간 총 에너지 사용량 - { name: 'toeSum', fieldName: '계(TOE)', header: { text: '계(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'elcpwAmnt', fieldName: '총 전력사용량(kWh)', header: { text: '총 전력사용량(kWh)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'toeQnty', fieldName: '자체 에너지사용량(TOE)', header: { text: '자체 에너지사용량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 하수처리량당 CO2배출량 - { name: 'co2Emissions', fieldName: '하수처리량당 CO2배출량(kgCO2/m³)', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - - // 운영주체 - { name: 'oprClsNm', fieldName: '운영주체(자체/공기업/민간대행)', header: { text: '운영주체(자체/공기업/민간대행)' }, editable: false }, - - // 위탁현황 - { name: 'trstOprEtpNm', fieldName: '위탁업체명', header: { text: '위탁업체명' } }, - { name: 'yrstpTrstCst', fieldName: '위탁비용(백만원/년)', header: { text: '위탁비용(백만원/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'trstOprDt', fieldName: '위탁계약기간(yyyy-mm-dd~yyyy-mm-dd)', header: { text: '위탁계약기간(yyyy-mm-dd~yyyy-mm-dd)' } }, - - // 기타 - { name: 'cntrTrtFctNm', fieldName: '중심처리시설', header: { text: '중심처리시설' } }, - { name: 'totWtrQrm', fieldName: '직원총수(명)', header: { text: '직원총수(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'safeAcdntCo', fieldName: '안전사고건수', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rmk', fieldName: '비고', header: { text: '비고' } }, - ], - - columnLayout: [ - { - name: 'administrative', - hideChildHeaders: false, - header: { text: '행정구역' }, - items: ['sidoCd', 'gugun'], - }, - 'bscFctNm', - 'addr', - 'fctCpc', - { - name: 'facilityCapacityDetail', - hideChildHeaders: false, - header: { text: '(m³/일)' }, - items: ['fctCpc1', 'fctCpc2', 'fctCpc3'], - }, - 'totInflAmntAve', - { - name: 'influentWaterDetail', - hideChildHeaders: false, - header: { text: '(m³/일)' }, - items: ['pInflAmntAve', 'oInflAmntAve', 'aInflAmntAve'], - }, - 'totTputAve', - { - name: 'effluentWaterDetail', - hideChildHeaders: false, - header: { text: '(m³/일)' }, - items: ['pTputAve', 'oTputAve', 'aTputAve'], - }, - 'inflAssctBfrWqltBod', - 'efflWqltBod', - 'inflAssctBfrWqltToc', - 'efflWqltToc', - 'inflAssctBfrWqltSs', - 'efflWqltSs', - 'inflAssctBfrWqltTn', - 'efflWqltTn', - 'inflAssctBfrWqltTp', - 'efflWqltTp', - 'inflAssctBfrWqltClfgrpcnt', - 'efflWqltClfgrpcnt', - 'tmEfficiency', - 'pcLoad', - 'trtFrfHanNm', - { - name: 'linkedTreatment', - hideChildHeaders: false, - header: { text: '연계처리량(m³/일) (500m³/일 이상/미만)' }, - items: ['excretionsTot', 'stkrsngWstwtrTot', 'lchtTot', 'etcTot'], - }, - 'tel', - 'rnDt', - 'compDt', - 'tot', - 'dsfctNm', - { - name: 'dischargeArea', - hideChildHeaders: false, - header: { text: '방류수역' }, - items: [ - 'wbsnNm', - 'rvNm', - 'mdmbsn', - 'efflWqltStndrdNm', - { - name: 'mediumWatershed', - hideChildHeaders: false, - header: { text: '중권역' }, - items: ['lgbsn', 'lgbsnBod'], - }, - ], - }, - 'toeRt', - 'useQnty', - { - name: 'energy', - hideChildHeaders: false, - header: { text: '연간 총 에너지 사용량(TOE)' }, - items: ['toeSum', 'elcpwAmnt', 'toeQnty'], - }, - 'co2Emissions', - 'oprClsNm', - { - name: 'consignmentStatus', - hideChildHeaders: false, - header: { text: '위탁현황' }, - items: ['trstOprEtpNm', 'yrstpTrstCst', 'trstOprDt'], - }, - 'cntrTrtFctNm', - 'totWtrQrm', - 'safeAcdntCo', - 'rmk', - ] -}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts new file mode 100644 index 000000000..eed27df03 --- /dev/null +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts @@ -0,0 +1,73 @@ +import { ValueType } from "realgrid"; + +export const MainFacilityColumn = { + columns: [ + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명' }, editable: false }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' }, editable: false }, + { name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' }, editable: false }, + { name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, editable: false }, + { name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' }, editable: false }, + { name: 'isMainYn', fieldName: 'isMainYn', header: { text: '중심처리시설여부' }, editable: false }, + + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + ], + columnLayout: [ + 'swtrmtFcltNm', + 'fcltPstnCn', + 'prxyBzentyNmCn', + 'cnsgnCstVl', + 'prxyCtrtCn', + 'isMainYn', + 'admdstCd', + 'fcltSn', + 'swtrmtFcltCd', + 'sggNm', + 'admdstNm', + ], + rowStartNum: 1, + fixedColumns: 1, + tabKey: 'MainFacility', + tabLabel: '지자체별공공하수처리시설' +}; + +// Tab 6-2: 하수처리장별 연결 현황 (왼쪽 그리드) +export const MainFacilityTargetColumn = { + columns: [ + // 하수처리장 정보 + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명' }, editable: false }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' }, editable: false }, + { name: 'mappedYn', fieldName: 'mappedYn', header: { text: '연결여부' }, editable: false }, + + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'mainFcltSn', fieldName: 'mainFcltSn', header: { text: '연결된중심시설일련번호' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'isMainYn', fieldName: 'isMainYn', header: { text: '중심처리시설여부' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + ], + columnLayout: [ + 'swtrmtFcltNm', + 'fcltPstnCn', + 'mappedYn', + 'fcltSn', + 'swtrmtFcltCd', + 'admdstNm', + 'mainFcltSn', + 'isMainYn', + ], + rowStartNum: 1, + fixedColumns: 1, + tabKey: 'MainFacilityTarget', + tabLabel: '연결대상시설' +}; + +export const MainFacilityInpColumn = { + tabKey: 'ST00060002', + tabLabel: '중심 처리 시설', + isSplitGrid: true, + left: MainFacilityColumn, + right: MainFacilityTargetColumn, +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts new file mode 100644 index 000000000..091286300 --- /dev/null +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts @@ -0,0 +1,220 @@ +import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary"; +import { ValueType } from "realgrid"; + +/** + * 하수도 통계 관리 > 06. 공공하수처리시설 + */ +export const PublicSewageFactColumn = { + columns: [ + // 행정구역 + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, footer: { text: '합계' }, styleName: 'rg-cell-readonly' }, + + // 시설명 + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명(500m³/일 이상/미만)' }, editable: false, styleName: 'rg-cell-readonly' }, + + // 소재지 + { name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '소재지' }, editable: false, styleName: 'rg-cell-readonly' }, + + // 시설용량 + { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, + + // (m³/일) + { name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsFcltBoilCpct', fieldName: 'prcsFcltBoilCpct', header: { text: '생물학적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsFcltAltdCpct', fieldName: 'prcsFcltAltdCpct', header: { text: '고도' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + // 유입하수량 + { name: 'inflwSwageQnt', fieldName: 'inflwSwageQnt', header: { text: '유입하수량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // (m³/일) + { name: 'inflwSwagePhysQnt', fieldName: 'inflwSwagePhysQnt', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'inflwSwageBoilQnt', fieldName: 'inflwSwageBoilQnt', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'inflwSwageAltdQnt', fieldName: 'inflwSwageAltdQnt', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + // 방류량 + { name: 'prcsQnt', fieldName: 'prcsQnt', header: { text: '방류량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // 방류량 - (m³/일) + { name: 'prcsPhysCpct', fieldName: 'prcsPhysCpct', header: { text: '물리적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'prcsBoilCpct', fieldName: 'prcsBoilCpct', header: { text: '생물학적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'prcsAltdCpct', fieldName: 'prcsAltdCpct', header: { text: '고도' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // 수질 + { name: 'inflwBodVl', fieldName: 'inflwBodVl', header: { text: '유입BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'dschgBodVl', fieldName: 'dschgBodVl', header: { text: '방류BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'inflwTocQnt', fieldName: 'inflwTocQnt', header: { text: '유입TOC' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'dschgTocQnt', fieldName: 'dschgTocQnt', header: { text: '방류TOC' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'inflwSsQnt', fieldName: 'inflwSsQnt', header: { text: '유입SS' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'dschgSsQnt', fieldName: 'dschgSsQnt', header: { text: '방류SS' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'inflwTnVl', fieldName: 'inflwTnVl', header: { text: '유입T-N' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, + { name: 'dschgTnVl', fieldName: 'dschgTnVl', header: { text: '방류T-N' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, + { name: 'inflwTpVl', fieldName: 'inflwTpVl', header: { text: '유입T-P' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, + { name: 'dschgTpVl', fieldName: 'dschgTpVl', header: { text: '방류T-P' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, + { name: 'inflwGrssClfgVl', fieldName: 'inflwGrssClfgVl', header: { text: '유입총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'dschgGrssClfgVl', fieldName: 'dschgGrssClfgVl', header: { text: '방류총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // 처리특성 + { name: 'prcsRt', fieldName: 'prcsRt', header: { text: '처리효율(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, + { name: 'prcsLodQnt', fieldName: 'prcsLodQnt', header: { text: '처리부하량(kgBOD/D)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, + { name: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리방법' }, editable: false, styleName: 'rg-cell-readonly' }, + + // 연계처리량 + { name: 'wholHmnQnt', fieldName: 'wholHmnQnt', header: { text: '분뇨' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'wholLvstQnt', fieldName: 'wholLvstQnt', header: { text: '축산' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'wholEfntQnt', fieldName: 'wholEfntQnt', header: { text: '침출수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'wholEtcQnt', fieldName: 'wholEtcQnt', header: { text: '기타' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // 시설정보 + { name: 'telNo', fieldName: 'telNo', header: { text: '전화번호' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'cmptnStrtYmd', fieldName: 'cmptnStrtYmd', header: { text: '준공일' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시일' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'wholBizVl', fieldName: 'wholBizVl', header: { text: '사업비' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // 방류수 소독방법 + { name: 'eflntDsnfMthdExpln', fieldName: 'eflntDsnfMthdExpln', header: { text: '방류수 소독방법' }, editable: false, styleName: 'rg-cell-readonly' }, + + // 방류수역 + { name: 'wtsysNm', fieldName: 'wtsysNm', header: { text: '수계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'dtlUnitZoneCn', fieldName: 'dtlUnitZoneCn', header: { text: '세부단위구역' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'rgnSeNm', fieldName: 'rgnSeNm', header: { text: '지역구분' }, editable: false, styleName: 'rg-cell-readonly' }, + + // 방류수역 > 중권역 + { name: 'msirbCn', fieldName: 'msirbCn', header: { text: '명칭' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'goalWqNo', fieldName: 'goalWqNo', header: { text: '목표수질(BOD)' }, editable: false, styleName: 'rg-cell-readonly' }, + + // 에너지 자립률 + { name: 'engySrlneRt', fieldName: 'engySrlneRt', header: { text: '에너지자립률(%)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // 에너지 자체 생산량 + { name: 'engyItslfPrdctnQnt', fieldName: 'engyItslfPrdctnQnt', header: { text: '에너지자체생산량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // 연간 총 에너지 사용량 + { name: 'anlEngyWholUseQnt', fieldName: 'anlEngyWholUseQnt', header: { text: '계(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '총 전력사용량(kWh)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'itslfEngyUseQnt', fieldName: 'itslfEngyUseQnt', header: { text: '자체 에너지사용량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + + // 하수처리량당 CO2배출량 + { name: 'swtrmtEmsnQnt', fieldName: 'swtrmtEmsnQnt', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-total' }, + + // 운영주체 + { name: 'operMnbdCn', fieldName: 'operMnbdCn', header: { text: '운영주체(자체/공기업/민간대행)' }, editable: false, styleName: 'rg-cell-readonly' }, + + // 위탁현황 + { name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' } }, + + // 기타 + { name: 'intgPrcsFcltCn', fieldName: 'intgPrcsFcltCn', header: { text: '중심처리시설' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수(명)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, + { name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'rmk', fieldName: '비고', header: { text: '비고' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + ], + + columnLayout: [ + { + name: 'administrative', + hideChildHeaders: false, + header: { text: '행정구역' }, + items: ['ctpvNm', 'sggNm'], + }, + 'swtrmtFcltNm', + 'impdFcltPstnCn', + 'prcsFcltCpct', + { + name: 'facilityCapacityDetail', + hideChildHeaders: false, + header: { text: '(m³/일)' }, + items: ['prcsFcltPhysCpct', 'prcsFcltBoilCpct', 'prcsFcltAltdCpct'], + }, + 'inflwSwageQnt', + { + name: 'influentWaterDetail', + hideChildHeaders: false, + header: { text: '(m³/일)' }, + items: ['inflwSwagePhysQnt', 'inflwSwageBoilQnt', 'inflwSwageAltdQnt'], + }, + 'prcsQnt', + { + name: 'effluentWaterDetail', + hideChildHeaders: false, + header: { text: '(m³/일)' }, + items: ['prcsPhysCpct', 'prcsBoilCpct', 'prcsAltdCpct'], + }, + 'inflwBodVl', + 'dschgBodVl', + 'inflwTocQnt', + 'dschgTocQnt', + 'inflwSsQnt', + 'dschgSsQnt', + 'inflwTnVl', + 'dschgTnVl', + 'inflwTpVl', + 'dschgTpVl', + 'inflwGrssClfgVl', + 'dschgGrssClfgVl', + 'prcsRt', + 'prcsLodQnt', + 'prcsMthdExpln', + { + name: 'linkedTreatment', + hideChildHeaders: false, + header: { text: '연계처리량(m³/일) (500m³/일 이상/미만)' }, + items: ['wholHmnQnt', 'wholLvstQnt', 'wholEfntQnt', 'wholEtcQnt'], + }, + 'telNo', + 'cmptnStrtYmd', + 'oprtngStrtYmd', + 'wholBizVl', + 'eflntDsnfMthdExpln', + { + name: 'dischargeArea', + hideChildHeaders: false, + header: { text: '방류수역' }, + items: [ + 'wtsysNm', + 'trbtrNm', + 'dtlUnitZoneCn', + 'rgnSeNm', + { + name: 'mediumWatershed', + hideChildHeaders: false, + header: { text: '중권역' }, + items: ['msirbCn', 'goalWqNo'], + }, + ], + }, + 'engySrlneRt', + 'engyItslfPrdctnQnt', + { + name: 'energy', + hideChildHeaders: false, + header: { text: '연간 총 에너지 사용량(TOE)' }, + items: ['anlEngyWholUseQnt', 'anlElpwrUseQnt', 'itslfEngyUseQnt'], + }, + 'swtrmtEmsnQnt', + 'operMnbdCn', + { + name: 'consignmentStatus', + hideChildHeaders: false, + header: { text: '위탁현황' }, + items: ['prxyBzentyNmCn', 'cnsgnCstVl', 'prxyCtrtCn'], + }, + 'intgPrcsFcltCn', + 'wholNope', + 'sftyAcdntEvntcsCnt', + 'rmk', + 'admdstCd', + 'fcltSn', + 'swtrmtFcltCd', + ], + rowStartNum: 3, + fixedColumns: 2, + tabKey: 'ST00060001', + tabLabel: '공공 하수처리 시설' +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn.ts new file mode 100644 index 000000000..3a8345224 --- /dev/null +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn.ts @@ -0,0 +1,8 @@ +import type { ColumnInfoList } from "@/components/type/DataGrid.type"; +import { MainFacilityInpColumn } from "./MainFacilityInpColumn"; +import { PublicSewageFactColumn } from "./PublicSewageFactColumn"; + +export const PublicSewageFactInpColumnList: ColumnInfoList[] = [ + PublicSewageFactColumn, + MainFacilityInpColumn, +]; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/dashboard/dashboard-queries.ts b/apps/st/src/entities/statistics/api/dashboard/dashboard-queries.ts index 2f37dbaa2..5b4e124f8 100644 --- a/apps/st/src/entities/statistics/api/dashboard/dashboard-queries.ts +++ b/apps/st/src/entities/statistics/api/dashboard/dashboard-queries.ts @@ -41,11 +41,14 @@ export function useDashboardUnSubmitCount( params: DashboardSearchParams | null, ) { return useQuery({ - queryKey: createDashboardQueryKey("un-submit-count", params), + queryKey: createDashboardQueryKey("submission-status", params), queryFn: async () => { if (!params) throw new Error("Dashboard search params are required."); - const response = await dashboardApi.getUnSubmitCount(params); + const response = await dashboardApi.getUnSubmitCount({ + ...params, + unSubmitOnly: true, + }); return response.data; }, enabled: hasRequiredDashboardParams(params), diff --git a/apps/st/src/entities/statistics/api/dashboard/dashboard-service.ts b/apps/st/src/entities/statistics/api/dashboard/dashboard-service.ts index 5fcaa3ac8..0bb830e61 100644 --- a/apps/st/src/entities/statistics/api/dashboard/dashboard-service.ts +++ b/apps/st/src/entities/statistics/api/dashboard/dashboard-service.ts @@ -35,7 +35,7 @@ async function getUnSubmitCount( ): Promise> { const response = await stClient.get< DashboardApiResponse - >(`${ENDPOINT.ST_SERVICE.DASHBOARD}/un-submit-count`, { params }); + >(`${ENDPOINT.ST_SERVICE.DASHBOARD}/submission-status`, { params }); return response.data; } 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/dashboard/dashboard-interface.ts b/apps/st/src/entities/statistics/model/dashboard/dashboard-interface.ts index f66eec7dc..b0d9093a0 100644 --- a/apps/st/src/entities/statistics/model/dashboard/dashboard-interface.ts +++ b/apps/st/src/entities/statistics/model/dashboard/dashboard-interface.ts @@ -3,6 +3,10 @@ export interface DashboardSearchParams { dtlInstCd: string; enoCd?: string; laeSeCd?: string; + sewerBzmnCd?: string; + sido?: string; + sggNm?: string; + unSubmitOnly?: boolean; } export interface DashboardApiResponse { @@ -34,13 +38,19 @@ export interface VrfcErrCountResponse { export interface UnSubmitCountResponse { statsYr: string; - unSubmitCount: number; - unSubmitList: UnSubmitStatsResponse[]; + statusList: UnSubmitStatsResponse[]; + submittedCount: number; + totalCount: number; + unSubmittedCount: number; } export interface UnSubmitStatsResponse { + sbmsnYn: string; + sidoSggNm: string; statsCd: string; statsNm: string; + statusCd: string; + statusNm: string; } export interface StatsSubmissionResponse { diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts index f88e9a08f..de49aee29 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts @@ -221,7 +221,7 @@ export function getVerificationPeriodActionBlockedMessage( const to = formatCheckInputYmdAsDash(v.endYmd); const range = from && to ? `${from} ~ ${to}` : [from, to].filter(Boolean).join(" ~ "); - return `검증기간에는 저장, 제출, 제출 취소를 할 수 없습니다.\n검증기간: ${range}`; + return `검증기간에는 저장, 제출, 제출 취소를\n 할 수 없습니다.\n(검증기간: ${range})`; } export interface StatisticsInputSewSupplyRateService { 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/pages/statistics/ui/panels/unsubmit-panel.tsx b/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx index fae349694..c4981be02 100644 --- a/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx @@ -24,7 +24,7 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) {

미제출 지표

{data && ( - {data.unSubmitCount}건 + {data.unSubmittedCount ?? 0}건 )}
@@ -43,13 +43,13 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) { )} {data && (
- {data.unSubmitList.length === 0 ? ( + {data.statusList.length === 0 ? (
미제출 지표가 없습니다.
) : (
- {data.unSubmitList.map((item) => ( + {data.statusList.map((item) => (
{ const { tabs, activeTabId } = useTabStore(); + + const tabsForBody = useMemo( + () => + tabs.filter( + (tab) => !isVerificationSelectionReportMenu(tab.menuItem), + ), + [tabs], + ); const fetchAdminInfo = useAdminStore((state) => state.fetchAdminInfo); // 컴포넌트 캐시를 useRef로 유지 (재렌더링 시에도 유지됨) @@ -45,7 +54,7 @@ const StatisticsPage = () => { return (
- {tabs.map((tab) => { + {tabsForBody.map((tab) => { // 렌더링 시점에 컴포넌트가 없으면 바로 로드 if (!componentCache.current.has(tab.id)) { const componentName = tab.urlPath; 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/shared/lib/verification-selection-report.ts b/apps/st/src/shared/lib/verification-selection-report.ts new file mode 100644 index 000000000..47424f88d --- /dev/null +++ b/apps/st/src/shared/lib/verification-selection-report.ts @@ -0,0 +1,11 @@ +import type { MenuItem } from "@/entities/menu"; + +export const VERIFICATION_SELECTION_REPORT_MARKER = + "VerificationSelectionReport"; + +export function isVerificationSelectionReportMenu(item: MenuItem): boolean { + return ( + item.menuEngNm === VERIFICATION_SELECTION_REPORT_MARKER || + !!item.urlPath?.includes(VERIFICATION_SELECTION_REPORT_MARKER) + ); +} diff --git a/apps/st/src/shared/ui/statistics-page-layout.tsx b/apps/st/src/shared/ui/statistics-page-layout.tsx index d7d5b98b4..857f8441e 100644 --- a/apps/st/src/shared/ui/statistics-page-layout.tsx +++ b/apps/st/src/shared/ui/statistics-page-layout.tsx @@ -10,7 +10,7 @@ function StatisticsPageLayout({ children }: { children: React.ReactNode }) { const menuPaths = pageTitle?.path?.split(">").map((path) => path.trim()); return ( -
+
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 }; + }, + }), +);