From 16c8fe716db2ab19df2a8d0e3bad03c84a33a4a8 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 20 Apr 2026 16:08:14 +0900 Subject: [PATCH] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/package.json | 148 +-- apps/st/src/components/module/DataGrid.tsx | 156 ++- .../StatisticsVerificationReport.tsx | 697 ++++++++---- apps/st/src/components/type/DataGrid.type.ts | 166 +-- .../api/general-verification-service.ts | 128 ++- .../input/statistics-input-status-service.ts | 10 +- .../st/src/entities/statistics/api/queries.ts | 101 +- .../api/statistics-approval-service.ts | 22 + .../statistics/api/statistics-service.ts | 38 +- apps/st/src/entities/statistics/index.ts | 11 + .../model/general-verification-interface.ts | 125 +- .../statistics-input-status-interface.ts | 11 +- .../model/statistics-approval-interface.ts | 22 + .../statistics-verification-report-columns.ts | 257 ++++- .../statistics-verification-report-types.ts | 16 +- apps/st/src/shared/config/api.ts | 3 +- apps/st/tsconfig.json | 82 +- packages/common/components/package.json | 1 + .../components/src/ui/combobox/index.tsx | 315 ++++++ packages/common/components/src/ui/index.ts | 1 + .../components/src/ui/input-group/index.tsx | 156 +++ pnpm-lock.yaml | 1007 +++++++++-------- 22 files changed, 2362 insertions(+), 1111 deletions(-) create mode 100644 apps/st/src/entities/statistics/api/statistics-approval-service.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-approval-interface.ts create mode 100644 packages/common/components/src/ui/combobox/index.tsx create mode 100644 packages/common/components/src/ui/input-group/index.tsx diff --git a/apps/st/package.json b/apps/st/package.json index 184138657..5cf4bf67d 100644 --- a/apps/st/package.json +++ b/apps/st/package.json @@ -1,76 +1,76 @@ { - "name": "st", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@common/assets": "workspace:*", - "@common/auth": "workspace:*", - "@common/components": "workspace:*", - "@common/config": "workspace:*", - "@common/network": "workspace:*", - "@rsone/components": "workspace:*", - "@faker-js/faker": "^9.9.0", - "@fontsource-variable/noto-sans-kr": "^5.2.5", - "@frontend-opensource/use-react-hooks": "^1.1.4", - "@fullcalendar/core": "^6.1.17", - "@fullcalendar/daygrid": "^6.1.17", - "@fullcalendar/interaction": "^6.1.17", - "@fullcalendar/react": "^6.1.17", - "@fullcalendar/timegrid": "^6.1.17", - "@hookform/resolvers": "^4.1.3", - "@minoru/react-dnd-treeview": "3.5.1-alpha.1", - "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.8", - "@tanstack/react-query": "^5.67.1", - "@tanstack/react-query-devtools": "^5.75.5", - "@tanstack/react-table": "^8.21.2", - "@tinymce/tinymce-react": "^5.1.1", - "@types/xlsx": "^0.0.36", - "axios": "^1.8.1", - "chart.js": "^4.4.8", - "chartjs-plugin-zoom": "^2.2.0", - "dayjs": "^1.11.13", - "js-cookie": "^3.0.5", - "jszip": "^3.10.1", - "lucide-react": "^0.474.0", - "next": "15.3.8", - "next-qrcode": "^2.5.1", - "next-themes": "^0.4.6", - "react-chartjs-2": "^5.3.0", - "react-dnd": "^16.0.1", - "react-hook-form": "^7.54.2", - "realgrid": "^2.9.5", - "realgrid-react": "^0.9.7", - "remixicon": "^4.7.0", - "sonner": "^2.0.1", - "swr": "^2.3.7", - "tailwindcss-animate": "^1.0.7", - "tus-js-client": "^4.3.1", - "xlsx": "^0.18.5", - "zod": "^3.24.2", - "zustand": "^5.0.3" - }, - "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/js-cookie": "^3.0.6", - "@types/node": "^22.13.1", - "@types/webpack": "^5.28.5", - "eslint": "^9", - "eslint-config-next": "15.3.8", - "eslint-plugin-react-hooks": "^5.0.0", - "postcss": "^8", - "prettier": "^3.4.2", - "prettier-plugin-tailwindcss": "^0.6.11", - "steiger": "^0.5.5", - "tailwindcss": "^3.4.1", - "typescript": "^5" - } + "name": "st", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@common/assets": "workspace:*", + "@common/auth": "workspace:*", + "@common/components": "workspace:*", + "@common/config": "workspace:*", + "@common/network": "workspace:*", + "@rsone/components": "workspace:*", + "@faker-js/faker": "^9.9.0", + "@fontsource-variable/noto-sans-kr": "^5.2.5", + "@frontend-opensource/use-react-hooks": "^1.1.4", + "@fullcalendar/core": "^6.1.17", + "@fullcalendar/daygrid": "^6.1.17", + "@fullcalendar/interaction": "^6.1.17", + "@fullcalendar/react": "^6.1.17", + "@fullcalendar/timegrid": "^6.1.17", + "@hookform/resolvers": "^4.1.3", + "@minoru/react-dnd-treeview": "3.5.1-alpha.1", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.8", + "@tanstack/react-query": "^5.67.1", + "@tanstack/react-query-devtools": "^5.75.5", + "@tanstack/react-table": "^8.21.2", + "@tinymce/tinymce-react": "^5.1.1", + "@types/xlsx": "^0.0.36", + "axios": "^1.8.1", + "chart.js": "^4.4.8", + "chartjs-plugin-zoom": "^2.2.0", + "dayjs": "^1.11.13", + "js-cookie": "^3.0.5", + "jszip": "^3.10.1", + "lucide-react": "^0.474.0", + "next": "15.3.8", + "next-qrcode": "^2.5.1", + "next-themes": "^0.4.6", + "react-chartjs-2": "^5.3.0", + "react-dnd": "^16.0.1", + "react-hook-form": "^7.54.2", + "realgrid": "^2.9.5", + "realgrid-react": "^0.9.7", + "remixicon": "^4.7.0", + "sonner": "^2.0.1", + "swr": "^2.3.7", + "tailwindcss-animate": "^1.0.7", + "tus-js-client": "^4.3.1", + "xlsx": "^0.18.5", + "zod": "^3.24.2", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@types/js-cookie": "^3.0.6", + "@types/node": "^22.13.1", + "@types/webpack": "^5.28.5", + "eslint": "^9", + "eslint-config-next": "15.3.8", + "eslint-plugin-react-hooks": "^5.0.0", + "postcss": "^8", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "steiger": "^0.5.5", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } } diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index d85693a53..ba46b7a63 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -321,10 +321,14 @@ const DataGrid = ({ cellStyleCallback, cellTooltipCallback, onRowClick, + onItemChecked, + onItemAllChecked, + onScrollToBottom, statusInfo, highlightedRowId, idFieldName = "id", enableSort = false, + gridHeight = 65, // vh 단위 }: DataGridProps) => { const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); @@ -340,9 +344,18 @@ const DataGrid = ({ const cellTooltipCallbackRef = useRef( cellTooltipCallback, ); + const onScrollToBottomRef = useRef<(() => void) | undefined>( + onScrollToBottom, + ); + const onItemCheckedRef = useRef(onItemChecked); + const onItemAllCheckedRef = + useRef(onItemAllChecked); rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; cellTooltipCallbackRef.current = cellTooltipCallback; + onScrollToBottomRef.current = onScrollToBottom; + onItemCheckedRef.current = onItemChecked; + onItemAllCheckedRef.current = onItemAllChecked; // highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택 useEffect(() => { @@ -558,10 +571,10 @@ const DataGrid = ({ ) { const cell = worksheet[ - XLSX.utils.encode_cell({ - r: rowIndex, - c: col, - }) + XLSX.utils.encode_cell({ + r: rowIndex, + c: col, + }) ]; if ( cell && @@ -637,7 +650,7 @@ const DataGrid = ({ const col = range.s.c + colIndex; const cell = worksheet[ - XLSX.utils.encode_cell({ r: row, c: col }) + XLSX.utils.encode_cell({ r: row, c: col }) ]; const headerName = headers[colIndex] || `Column${colIndex + 1}`; @@ -761,13 +774,13 @@ const DataGrid = ({ onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean; } ).onSorting = enableSort - ? createOnSortingHandler( - columns, - setRowData, - sortDirRef, - originalRowsRef, - ) - : () => false; + ? createOnSortingHandler( + columns, + setRowData, + sortDirRef, + originalRowsRef, + ) + : () => false; (gridView.displayOptions as DisplayOptions).fitStyle = GridFitStyle.EVEN; @@ -937,7 +950,7 @@ const DataGrid = ({ ) { ( column.footer[ - index + index ] as FooterItemWithCallback ).valueCallback = footerItem.valueCallback; @@ -990,11 +1003,11 @@ const DataGrid = ({ getGridDataRef.current = () => { const gv = gridRef.current?.gridView as | { - getDataSource?: () => { - getRowCount(): number; - getValue(r: number, f: string): unknown; - }; - } + getDataSource?: () => { + getRowCount(): number; + getValue(r: number, f: string): unknown; + }; + } | undefined; if (!gv?.getDataSource) return []; const ds = gv.getDataSource(); @@ -1090,6 +1103,47 @@ const DataGrid = ({ }; }, [onRowClick, cellTooltipCallback, columns]); + useEffect(() => { + const gridView = gridRef.current?.gridView; + if (!gridView || !onScrollToBottom) return; + + gridView.onScrollToBottom = () => { + onScrollToBottomRef.current?.(); + }; + + return () => { + if (gridView.onScrollToBottom) + gridView.onScrollToBottom = undefined; + }; + }, [onScrollToBottom]); + + useEffect(() => { + const gridView = gridRef.current?.gridView; + if (!gridView) return; + + if (onItemChecked) { + gridView.onItemChecked = (_grid, itemIndex, checked) => { + onItemCheckedRef.current?.(itemIndex, checked); + }; + } else { + gridView.onItemChecked = undefined; + } + + if (onItemAllChecked) { + gridView.onItemAllChecked = (_grid, checked) => { + onItemAllCheckedRef.current?.(checked); + }; + } else { + gridView.onItemAllChecked = undefined; + } + + return () => { + if (gridView.onItemChecked) gridView.onItemChecked = undefined; + if (gridView.onItemAllChecked) + gridView.onItemAllChecked = undefined; + }; + }, [onItemChecked, onItemAllChecked]); + return (
진행상황:{" "} {statusInfo.laeSbmsnSttsCd === null || - statusInfo.laeSbmsnSttsCd === "0" + statusInfo.laeSbmsnSttsCd === "0" ? "미작성" : statusInfo.laeSbmsnSttsCd === "1" - ? "작성중" - : statusInfo.laeSbmsnSttsCd === "2" - ? "제출" - : statusInfo.laeSbmsnSttsCd === "3" - ? "승인" - : statusInfo.laeSbmsnSttsCd === "4" - ? "반려" - : "검증중"} + ? "작성중" + : statusInfo.laeSbmsnSttsCd === "2" + ? "제출" + : statusInfo.laeSbmsnSttsCd === "3" + ? "승인" + : statusInfo.laeSbmsnSttsCd === "4" + ? "반려" + : "검증중"} 제출일:{" "} {statusInfo.laeSbmsnYmd ? statusInfo.laeSbmsnYmd.length === 8 && - /^\d{8}$/.test(statusInfo.laeSbmsnYmd) + /^\d{8}$/.test(statusInfo.laeSbmsnYmd) ? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}` : statusInfo.laeSbmsnYmd : "미제출"} @@ -1159,7 +1213,7 @@ const DataGrid = ({ )}
-
+
boolean; } ).onSorting = enableSort - ? createOnSortingHandler( - columns, - setRowData, - sortDirRef, - originalRowsRef, - ) - : () => false; + ? createOnSortingHandler( + columns, + setRowData, + sortDirRef, + originalRowsRef, + ) + : () => false; const dataProvider = gridRef.current ?.dataProvider as unknown as | { - setFields?(f: unknown[]): void; - setRows?( - r: Record[], - ): void; - getJsonRow?( - r: number, - ): Record; - } + setFields?(f: unknown[]): void; + setRows?( + r: Record[], + ): void; + getJsonRow?( + r: number, + ): Record; + } | undefined; if (dataProvider?.setFields) { dataProvider.setFields( @@ -1346,14 +1400,14 @@ const DataGrid = ({ })} footer={ column.footer && - !Array.isArray(column.footer) + !Array.isArray(column.footer) ? { - text: column.footer.text, - expression: - column.footer.expression, - numberFormat: - column.footer.numberFormat, - } + text: column.footer.text, + expression: + column.footer.expression, + numberFormat: + column.footer.numberFormat, + } : undefined } mergeRule={ diff --git a/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx b/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx index b9baaa018..237bc8457 100644 --- a/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx +++ b/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx @@ -1,10 +1,34 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import type { Dispatch, SetStateAction } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; -import { useAlertStore } from "@/shared/lib"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import dayjs from "dayjs"; + +import { useAlertStore, useMutationWrapper, useUserStore } from "@/shared/lib"; +import { CodeSelect } from "@/entities/code"; +import { + type ApprovalRequest, + statisticsApprovalApi, + useStAnlsSidoCombo, + useStAnlsSigunguCombo, + useStatsVrfcRptData, + useStatsVrfcRsltData, +} from "@/entities/statistics"; +import type { VerifyResultItem } from "@/entities/statistics/model/general-verification-interface"; +import type { StatsInputStatusList } from "@/entities/statistics/model/input/statistics-input-status-interface"; import { Button, + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxContent, + ComboboxEmpty, + ComboboxItem, + ComboboxList, + ComboboxValue, Label, LoadingWithAnimation, Select, @@ -12,259 +36,364 @@ import { SelectItem, SelectTrigger, SelectValue, + useComboboxAnchor, } from "@common/components"; import { statisticsVerificationDetailColumnInfo, statisticsVerificationSummaryColumnInfo, } from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-columns"; -import { - APPROVAL_STATUS_OPTIONS, - DEFAULT_FILTERS, - DEFAULT_SUB_FILTERS, - fetchStatisticsVerificationDetailMock, - fetchStatisticsVerificationSummaryMock, - getSigunguOptions, - getYearOptions, - SIDO_OPTIONS, - STATS_INDICATOR_OPTIONS, - STATUS_OPTIONS, -} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-mock"; import type { - StatisticsVerificationDetailRow, StatisticsVerificationFilterState, StatisticsVerificationSubFilterState, - StatisticsVerificationSummaryRow, } from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-types"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReturnButton from "@/components/button/ReturnButton"; import DataGrid from "@/components/module/DataGrid"; import { toGridRows } from "@/components/util/grid/gridFunction"; +const getYearOptions = () => { + const currentYear = dayjs().year(); + const yearOptionsRange = Array.from( + { length: currentYear - 2017 }, + (_, i) => { + const year = 2017 + i; + return { label: String(year), value: String(year) }; + }, + ); + return yearOptionsRange.map((year) => ({ + label: year.label, + value: year.value, + })); +}; + +function isSameStatsInputStatusRow( + a: StatsInputStatusList, + b: StatsInputStatusList, +): boolean { + return ( + a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.statsCd === b.statsCd + ); +} + +function isSameVerifyResultRow( + a: VerifyResultItem, + b: VerifyResultItem, +): boolean { + if (a.vrfcSn != null && b.vrfcSn != null) return a.vrfcSn === b.vrfcSn; + return ( + a.statsYr === b.statsYr && + a.statsCd === b.statsCd && + a.ctpvNm === b.ctpvNm && + a.sggNm === b.sggNm && + a.vrfcTrgtNm === b.vrfcTrgtNm && + a.vrfcMthdCd === b.vrfcMthdCd + ); +} + +function useGridCheckboxSelection( + sourceRows: T[], + isSameRow: (a: T, b: T) => boolean, + setSelected: Dispatch>, +) { + const handleItemChecked = useCallback( + (itemIndex: number, checked: boolean) => { + const origin = sourceRows[itemIndex]; + if (origin === undefined) return; + setSelected((prev) => { + if (checked) { + if (prev.some((r) => isSameRow(r, origin))) return prev; + return [...prev, origin]; + } + return prev.filter((r) => !isSameRow(r, origin)); + }); + }, + [sourceRows, isSameRow, setSelected], + ); + + const handleAllChecked = useCallback( + (checked: boolean) => { + if (checked) setSelected([...sourceRows]); + else setSelected([]); + }, + [sourceRows, setSelected], + ); + + return { handleItemChecked, handleAllChecked }; +} + +const DEFAULT_FILTERS: StatisticsVerificationFilterState = { + ctpvNm: null, + sggNm: null, + statsYr: String(dayjs().year() - 1), + sewerBzmnCd: null, + statsCd: null, +}; + +const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = { + ...DEFAULT_FILTERS, + prcsYnList: "1,2,3", + pbcpAprvYn: "N", +}; + +const STATUS_OPTIONS = [ + { label: "수정완료", value: "1" }, + { label: "예외통과", value: "2" }, + { label: "반려", value: "3" }, +]; + +const APPROVAL_STATUS_OPTIONS = [ + { label: "승인", value: "Y" }, + { label: "미승인", value: "N" }, +]; + function StatisticsVerificationReport() { + const userInfo = useUserStore((state) => state.userInfo); const { setMessage: alert } = useAlertStore((state) => state); - const yearOptions = useMemo(() => getYearOptions(), []); + const { confirm } = useConfirm(); const [filters, setFilters] = useState(DEFAULT_FILTERS); const [subFilters, setSubFilters] = useState(DEFAULT_SUB_FILTERS); - const [summaryRows, setSummaryRows] = useState< - StatisticsVerificationSummaryRow[] + const [submittedMainFilters, setSubmittedMainFilters] = + useState(DEFAULT_FILTERS); + const [submittedDetailParams, setSubmittedDetailParams] = useState<{ + main: StatisticsVerificationFilterState; + sub: StatisticsVerificationSubFilterState; + }>({ + main: DEFAULT_FILTERS, + sub: DEFAULT_SUB_FILTERS, + }); + const [selectedSummaryRows, setSelectedSummaryRows] = useState< + StatsInputStatusList[] >([]); - const [detailRows, setDetailRows] = useState< - StatisticsVerificationDetailRow[] + const [selectedDetailRows, setSelectedDetailRows] = useState< + VerifyResultItem[] >([]); - const [summaryGridRows, setSummaryGridRows] = useState< - Record[] - >([]); - const [detailGridRows, setDetailGridRows] = useState< - Record[] - >([]); - const [selectedSummaryRow, setSelectedSummaryRow] = - useState(null); - const [selectedDetailRow, setSelectedDetailRow] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - const sigunguOptions = useMemo( - () => getSigunguOptions(filters.sido), - [filters.sido], + const { data: sidoData } = useStAnlsSidoCombo(); + const { data: sigunguData } = useStAnlsSigunguCombo( + filters.sewerBzmnCd ?? "", ); - useEffect(() => { - void handleSearch(filters, subFilters); - }, []); + const statisticsVerificationReportQuery = + useStatsVrfcRptData(submittedMainFilters); + const statisticsVerificationResultQuery = useStatsVrfcRsltData( + submittedDetailParams, + ); - async function handleSearch( + const statisticsVerificationReportData = + statisticsVerificationReportQuery.data ?? []; + const statisticsVerificationReportGridRows = useMemo( + () => toGridRows(statisticsVerificationReportData), + [statisticsVerificationReportData], + ); + const statisticsVerificationResultData = useMemo( + () => + statisticsVerificationResultQuery.data?.pages.flatMap( + (page) => page.verifyResultList ?? [], + ) ?? [], + [statisticsVerificationResultQuery.data], + ); + const statisticsVerificationResultGridRows = useMemo( + () => toGridRows(statisticsVerificationResultData), + [statisticsVerificationResultData], + ); + + const isLoading = + statisticsVerificationReportQuery.isFetching || + statisticsVerificationResultQuery.isFetching; + const isAppendingDetail = + statisticsVerificationResultQuery.isFetchingNextPage; + const canLoadMoreDetail = Boolean( + statisticsVerificationResultQuery.hasNextPage, + ); + + const { + handleItemChecked: handleReportItemChecked, + handleAllChecked: handleReportAllChecked, + } = useGridCheckboxSelection( + statisticsVerificationReportData, + isSameStatsInputStatusRow, + setSelectedSummaryRows, + ); + + const { + handleItemChecked: handleResultItemChecked, + handleAllChecked: handleResultAllChecked, + } = useGridCheckboxSelection( + statisticsVerificationResultData, + isSameVerifyResultRow, + setSelectedDetailRows, + ); + + const { mutateAsync: onApproveStats } = useMutationWrapper({ + mutationFn: (approvalRequests: ApprovalRequest[]) => + statisticsApprovalApi.approveStats(approvalRequests), + onSuccess: (data) => { + alert(data.message ?? "승인이 완료되었습니다."); + }, + }); + + useEffect(() => { + if (!statisticsVerificationReportQuery.isError) return; + alert("통계검증리포트 조회 중 오류가 발생했습니다."); + setSelectedSummaryRows([]); + }, [ + alert, + statisticsVerificationReportQuery.errorUpdatedAt, + statisticsVerificationReportQuery.isError, + ]); + + function handleSearch( nextFilters: StatisticsVerificationFilterState, nextSubFilters: StatisticsVerificationSubFilterState, - summaryId?: number | null, ) { + setSelectedSummaryRows([]); + setSelectedDetailRows([]); + setSubmittedMainFilters({ ...nextFilters }); + setSubmittedDetailParams({ + main: { ...nextFilters }, + sub: { ...nextSubFilters }, + }); + } + + async function handleLoadMoreDetail() { + if (isLoading || isAppendingDetail || !canLoadMoreDetail) return; + try { - setIsLoading(true); - const [summaryData, detailData] = await Promise.all([ - fetchStatisticsVerificationSummaryMock(nextFilters), - fetchStatisticsVerificationDetailMock( - nextFilters, - nextSubFilters, - summaryId, - ), - ]); - setSummaryRows(summaryData); - setDetailRows(detailData); - setSummaryGridRows(toGridRows(summaryData)); - setDetailGridRows(toGridRows(detailData)); - if (summaryData.length === 0) - alert("조회된 상단 목록 데이터가 없습니다."); - if (detailData.length === 0) - alert("조회된 상세 목록 데이터가 없습니다."); + await statisticsVerificationResultQuery.fetchNextPage(); } catch { - alert("통계검증리포트 조회 중 오류가 발생했습니다."); - } finally { - setIsLoading(false); + alert("추가 데이터를 불러오는 중 오류가 발생했습니다."); } } + function handleDetailSearchByAreaC() { + setSelectedDetailRows([]); + setSubmittedDetailParams({ + main: { ...filters }, + sub: { ...subFilters }, + }); + } + function handleFilterChange< K extends keyof StatisticsVerificationFilterState, - >(key: K, value: StatisticsVerificationFilterState[K]) { - if (key === "sido") { - setFilters((prev) => ({ ...prev, sido: value, sigungu: "ALL" })); - return; - } - setFilters((prev) => ({ ...prev, [key]: value })); + >(key: K) { + return ( + value?: StatisticsVerificationFilterState[K], + optionData?: { label: string; value: string }, + ) => { + if (key === "sewerBzmnCd") { + setFilters((prev) => ({ + ...prev, + ctpvNm: optionData?.label, + sewerBzmnCd: optionData?.value, + sggNm: "", + })); + return; + } + setFilters((prev) => ({ ...prev, [key]: value })); + }; } function handleSubFilterChange< K extends keyof StatisticsVerificationSubFilterState, - >(key: K, value: StatisticsVerificationSubFilterState[K]) { - setSubFilters((prev) => ({ ...prev, [key]: value })); + >(key: K) { + return (value?: StatisticsVerificationSubFilterState[K]) => { + setSubFilters((prev) => ({ ...prev, [key]: value })); + }; } function handleReset() { setFilters(DEFAULT_FILTERS); setSubFilters(DEFAULT_SUB_FILTERS); - setSelectedSummaryRow(null); - setSelectedDetailRow(null); - void handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS); + setSelectedSummaryRows([]); + setSelectedDetailRows([]); + handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS); } - function handleApprove() { - if (!selectedSummaryRow) { - alert("승인할 상단 목록 행을 먼저 선택해주세요."); - return; + async function handleApprove() { + const commonParam = { + statsYr: filters.statsYr, + sewerBzmnCd: filters.sewerBzmnCd ?? "", + statsCd: filters.statsCd ?? "", + mdfrId: userInfo?.userUnqId ?? "", + }; + const approvalRequests = selectedSummaryRows.map((row) => ({ + ...commonParam, + rjctOpnnCn: row.rjctOpnnCn ?? "", + lastCfmtnPicNm: row.laePicNm ?? "", + lastCfmtnPicTelNo: row.laePicTelNo ?? "", + lastCfmtnPicDeptNm: row.laeKbrdrDeptNm ?? "", + lastCfmtnPicJbgdNm: row.laeKbrdrJbgdNm ?? "", + })); + try { + if (await confirm("승인하시겠습니까?")) { + await onApproveStats(approvalRequests); + } + } catch { + alert("승인 중 오류가 발생했습니다."); } - alert( - `승인 API TBD: ${selectedSummaryRow.sido} ${selectedSummaryRow.sigungu} (${selectedSummaryRow.reportType})`, - ); } - function handleReject() { - if (!selectedDetailRow) { - alert("반려할 상세 항목 행을 먼저 선택해주세요."); - return; - } - alert( - `반려 API TBD: ${selectedDetailRow.sido} ${selectedDetailRow.sigungu} ${selectedDetailRow.detailItem}`, - ); - } - - function handleDownload() { - if (detailRows.length === 0) { - alert("다운로드할 상세 목록이 없습니다."); - return; - } - - const headers = [ - "시도", - "시군구", - "지표", - "세부항목", - "검증날짜", - "오류내역", - "입력값", - "비교값", - "상태", - "예외사유", - "반려의견", - "승인여부", - ]; - const rows = detailRows.map((row) => - [ - row.sido, - row.sigungu, - row.indicator, - row.detailItem, - row.verifyDate, - row.errorDetail, - String(row.inputValue), - String(row.compareValue), - row.status, - row.exceptionReason, - row.returnOpinion, - row.approvalYn, - ].join(","), - ); - const csv = [headers.join(","), ...rows].join("\n"); - const blob = new Blob([`\uFEFF${csv}`], { - type: "text/csv;charset=utf-8;", - }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = "statistics-verification-report-detail.csv"; - link.click(); - URL.revokeObjectURL(url); - } - - function handleSummaryRowClick(row: Record) { - const summaryId = Number(row.summaryId); - const selected = summaryRows.find( - (item) => item.summaryId === summaryId, - ); - setSelectedSummaryRow(selected ?? null); - if (!summaryId) return; - void handleSearch(filters, subFilters, summaryId); - } - - function handleDetailRowClick(row: Record) { - const detailId = Number(row.detailId); - const selected = detailRows.find((item) => item.detailId === detailId); - setSelectedDetailRow(selected ?? null); - } + function handleReject() {} return (
-
-
+
+
handleFilterChange("sido", value)} - options={SIDO_OPTIONS} + value={filters.sewerBzmnCd ?? ""} + onChange={handleFilterChange("sewerBzmnCd")} + options={ + sidoData?.map((item) => ({ + label: item.adstNm, + value: item.adstCd, + })) ?? [] + } /> - handleFilterChange("sigungu", value) + value={filters.sggNm ?? ""} + onChange={handleFilterChange("sggNm")} + options={ + sigunguData?.map((item) => ({ + label: item.adstNm, + value: item.adstCd, + })) ?? [] } - options={sigunguOptions} /> handleFilterChange("year", value)} - options={yearOptions} + value={filters.statsYr} + onChange={handleFilterChange("statsYr")} + options={getYearOptions()} /> - - handleFilterChange("statsIndicator", value) - } - options={STATS_INDICATOR_OPTIONS} - /> - -
- - +
+ +
-
- -
-
- +
+ + +
@@ -272,78 +401,161 @@ function StatisticsVerificationReport() { columnInfo={statisticsVerificationSummaryColumnInfo} fixedColumns={3} isExcelImport={false} - onRowClick={handleSummaryRowClick} - rowData={summaryGridRows} + isExcelExport={false} + onItemAllChecked={handleReportAllChecked} + onItemChecked={handleReportItemChecked} + rowData={statisticsVerificationReportGridRows} rowStartNum={2} - setRowData={setSummaryGridRows} + setRowData={() => {}} showHeaderCheckbox={true} + gridHeight={30} /> -
-
-
- - handleSubFilterChange("status", value) - } - options={STATUS_OPTIONS} - /> - - handleSubFilterChange("approvalStatus", value) - } - options={APPROVAL_STATUS_OPTIONS} - /> -
-
- - -
+

+ 검증 결과 +

+
+
+ + +
+
+ +
- +
+ {}} + showHeaderCheckbox={true} + /> - {isLoading && ( -
-
- + {(isLoading || isAppendingDetail) && ( +
+
+ +
-
- )} + )} +
); } interface FilterSelectProps { label: string; - onChange: (value: string) => void; + onChange: ( + value?: string, + optionData?: { label: string; value: string }, + ) => void; options: Array<{ label: string; value: string }>; - value: string; + value?: string; +} + +function FilterCombobox({ + label, + onChange, + options, + value, +}: FilterSelectProps) { + const anchor = useComboboxAnchor(); + const items = useMemo( + () => options.map((option) => option.value), + [options], + ); + const selectedValues = useMemo(() => { + if (!value) return []; + return value.split(",").filter(Boolean); + }, [value]); + + return ( +
+ + { + if (next == null) { + onChange(undefined); + return; + } + const list = Array.isArray(next) ? next : [next]; + onChange(list.length > 0 ? list.join(",") : undefined); + }} + > + + + {(values: string[]) => ( + <> + {values.map((v) => ( + + {options.find((o) => o.value === v) + ?.label ?? v} + + ))} + + + )} + + + + 검색 결과가 없습니다. + + {(item: string) => ( + + {options.find((o) => o.value === item)?.label ?? + item} + + )} + + + +
+ ); } function FilterSelect({ label, onChange, options, value }: FilterSelectProps) { return ( -
- - + onChange( + value, + options.find((option) => option.value === value), + ) + } + > + + {options.map((option) => ( @@ -357,5 +569,4 @@ function FilterSelect({ label, onChange, options, value }: FilterSelectProps) { ); } -export { StatisticsVerificationReport }; export default StatisticsVerificationReport; diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index befcbe0fc..c07eacf6e 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -18,30 +18,30 @@ export type RealGridColumnInfo = { mergeRule?: { criteria: "value" | string }; sortable?: boolean; footer?: - | { - text?: string; - expression?: "sum" | "avg" | "count" | "max" | "min"; - numberFormat?: string; - valueCallback?: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: ColumnFooter, - value: number | string | undefined, - ) => number | string; - } - | Array<{ - text?: string; - expression?: "sum" | "avg" | "count" | "max" | "min"; - numberFormat?: string; - valueCallback?: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: ColumnFooter, - value: number | string | undefined, - ) => number | string; - }>; + | { + text?: string; + expression?: "sum" | "avg" | "count" | "max" | "min"; + numberFormat?: string; + valueCallback?: ( + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: ColumnFooter, + value: number | string | undefined, + ) => number | string; + } + | Array<{ + text?: string; + expression?: "sum" | "avg" | "count" | "max" | "min"; + numberFormat?: string; + valueCallback?: ( + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: ColumnFooter, + value: number | string | undefined, + ) => number | string; + }>; }; /** setColumns 메서드가 있는 GridView */ @@ -110,21 +110,21 @@ export type DataFieldValueCallback = ( export type ComputedFieldConfig = | { fields: string[]; type: "sum" | "avg" } | { - numeratorFields: string[]; - denominatorFields: string[]; - type: "pblswrgPopltnSuprtRate"; - } + numeratorFields: string[]; + denominatorFields: string[]; + type: "pblswrgPopltnSuprtRate"; + } | { - numeratorFields: string[]; - denominatorFields: string[]; - type: "adtrtPopltnSuprtRate"; - } + numeratorFields: string[]; + denominatorFields: string[]; + type: "adtrtPopltnSuprtRate"; + } | { - numeratorFields: string[]; - denominatorFields: string[]; - thirdFields: string[]; - type: "swrgInstlrtRate"; - }; + numeratorFields: string[]; + denominatorFields: string[]; + thirdFields: string[]; + type: "swrgInstlrtRate"; + }; export type ColumnConfig = { name: string; @@ -147,31 +147,31 @@ export type ColumnConfig = { valueType?: ValueType; dataFormat?: string; footer?: - | { - text?: string; - expression?: "sum" | "avg" | "count" | "max" | "min"; - numberFormat?: string; - valueCallback?: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: ColumnFooter, - value: number | string | undefined, - ) => number | string; - multiRow?: boolean; // 여러 행 footer 사용 여부 - } - | Array<{ - text?: string; - expression?: "sum" | "avg" | "count" | "max" | "min"; - numberFormat?: string; - valueCallback?: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: ColumnFooter, - value: number | string | undefined, - ) => number | string; - }>; // RealGrid에서 footer를 배열로 설정할 수 있음 + | { + text?: string; + expression?: "sum" | "avg" | "count" | "max" | "min"; + numberFormat?: string; + valueCallback?: ( + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: ColumnFooter, + value: number | string | undefined, + ) => number | string; + multiRow?: boolean; // 여러 행 footer 사용 여부 + } + | Array<{ + text?: string; + expression?: "sum" | "avg" | "count" | "max" | "min"; + numberFormat?: string; + valueCallback?: ( + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: ColumnFooter, + value: number | string | undefined, + ) => number | string; + }>; // RealGrid에서 footer를 배열로 설정할 수 있음 }; export type FooterUserSpan = { @@ -183,16 +183,16 @@ export type FooterUserSpan = { export type ColumnLayoutItem = | string | { - name: string; - hideChildHeaders?: boolean; - header: { text: string }; - items: (string | ColumnLayoutItem)[]; - footerUserSpans?: FooterUserSpan[]; // footer 병합 설정 - } + name: string; + hideChildHeaders?: boolean; + header: { text: string }; + items: (string | ColumnLayoutItem)[]; + footerUserSpans?: FooterUserSpan[]; // footer 병합 설정 + } | { - column: string; // 컬럼 이름 (footerUserSpans 사용 시) - footerUserSpans?: FooterUserSpan[]; // footer 병합 설정 - }; + column: string; // 컬럼 이름 (footerUserSpans 사용 시) + footerUserSpans?: FooterUserSpan[]; // footer 병합 설정 + }; export type ColumnInfo = { columns: ColumnConfig[]; @@ -202,7 +202,10 @@ export type ColumnInfo = { export type GridTopButtonConfig = { visible?: boolean; text: string; - onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void; + onClick?: (helpers: { + openDialog: (title?: string) => void; + closeDialog: () => void; + }) => void; }; export type ColumnInfoList = { @@ -225,11 +228,18 @@ export type CellStyleCallbackParams = { row: Record; }; -export type CellStyleCallback = (params: CellStyleCallbackParams) => string | undefined; +export type CellStyleCallback = ( + params: CellStyleCallbackParams, +) => string | undefined; /** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */ -export type CellTooltipCallbackParams = { row: Record; fieldName: string }; -export type CellTooltipCallback = (params: CellTooltipCallbackParams) => string | undefined; +export type CellTooltipCallbackParams = { + row: Record; + fieldName: string; +}; +export type CellTooltipCallback = ( + params: CellTooltipCallbackParams, +) => string | undefined; export type DataGridProps = { columnInfo: ColumnInfo; @@ -252,6 +262,11 @@ export type DataGridProps = { current: (() => Record[]) | null; }; onRowClick?: (rowData: Record) => void; // 행 클릭 이벤트 + /** CheckBar 행 체크 변경 */ + onItemChecked?: (itemIndex: number, checked: boolean) => void; + /** CheckBar 헤더 전체 선택/해제 */ + onItemAllChecked?: (checked: boolean) => void; + onScrollToBottom?: () => void; statusInfo?: SewerageSupplyRateStatusInfo | null; /** 강조 및 스크롤할 행의 ID */ highlightedRowId?: string | number | null; @@ -263,6 +278,7 @@ export type DataGridProps = { compactFullWidth?: boolean; /** CompactDataGrid 전용: 그리드 영역 높이 Tailwind 클래스 */ compactGridHeightClass?: string; + gridHeight?: number; }; export type TabDataGridProps = { diff --git a/apps/st/src/entities/statistics/api/general-verification-service.ts b/apps/st/src/entities/statistics/api/general-verification-service.ts index 96ff08d39..95c96b467 100644 --- a/apps/st/src/entities/statistics/api/general-verification-service.ts +++ b/apps/st/src/entities/statistics/api/general-verification-service.ts @@ -1,127 +1,169 @@ import { stClient } from "@/shared/api/client"; import { ENDPOINT } from "@/shared/config"; -import type { +import type { + CheckVerificationNameRequest, + CheckVerificationNameResponse, + CopyVerifyDataRequest, + DeleteVrfcMasterResponse, + GeneralverificationDetailResponse, + GeneralverificationInfoResponse, GeneralverificationRequest, GeneralverificationResponse, GeneralverificationService, - GeneralverificationInfoResponse, - CheckVerificationNameRequest, - CheckVerificationNameResponse, - UpdateVrfcMasterRequest, - UpdateVrfcMasterResponse, RegistVrfcMasterRequest, RegistVrfcMasterResponse, - DeleteVrfcMasterResponse, - VrfcOperatorsResponse, - GeneralverificationDetailResponse, + UpdateVrfcMasterRequest, + UpdateVrfcMasterResponse, VerifyColumnListRequest, VerifyColumnListResponse, VerifyDetailRegistRequest, VerifyDetailUpdateRequest, - CopyVerifyDataRequest, + VerifyResultListRequest, + VerifyResultListResponse, + VrfcOperatorsResponse, } from "../model/general-verification-interface"; export class GeneralverificationApiService implements GeneralverificationService { - public async getGeneralverificationList(param: GeneralverificationRequest): Promise { - const response = await stClient.get ( + public async getGeneralverificationList( + param: GeneralverificationRequest, + ): Promise { + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`, { params: param }, ); return response.data; } - public async getGeneralverificationInfo(param: number): Promise { - const response = await stClient.get ( + public async getGeneralverificationInfo( + param: number, + ): Promise { + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param}`, ); return response.data; } - public async checkVerificationName(param: CheckVerificationNameRequest): Promise { - const response = await stClient.get ( + public async checkVerificationName( + param: CheckVerificationNameRequest, + ): Promise { + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/check-name`, { params: param }, ); return response.data; } - public async UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise { - const response = await stClient.put ( + public async UpdateVrfcMaster( + param: UpdateVrfcMasterRequest, + ): Promise { + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param.vrfcSn}`, param, ); return response.data; } - - public async RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise { - const response = await stClient.post ( + + public async RegistVrfcMaster( + param: RegistVrfcMasterRequest, + ): Promise { + const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/master`, param, ); return response.data; } - public async DeleteVrfcMaster(param: number): Promise { - const response = await stClient.delete ( + public async DeleteVrfcMaster( + param: number, + ): Promise { + const response = await stClient.delete( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`, ); return response.data; } public async getVrfcOperators(): Promise { - const response = await stClient.get ( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/operators`, ); return response.data; } - public async getVrfcDetail(param: number): Promise { - const response = await stClient.get ( + public async getVrfcDetail( + param: number, + ): Promise { + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`, ); return response.data; } - public async getVerifyColumnList(param: VerifyColumnListRequest): Promise { - const response = await stClient.get ( - param.refStatsCd ? - `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}` - : - `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`, + public async getVerifyColumnList( + param: VerifyColumnListRequest, + ): Promise { + const response = await stClient.get( + param.refStatsCd + ? `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}` + : `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`, ); return response.data; } - public async getSelectedVerifyColumnList(param: string): Promise { - const response = await stClient.get ( + public async getSelectedVerifyColumnList( + param: string, + ): Promise { + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param}`, ); return response.data; } - public async RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise { - const response = await stClient.post ( + public async RegistVrfcDetail( + param: VerifyDetailRegistRequest, + ): Promise { + const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`, - param + param, ); return response.data; } - public async UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise { - const response = await stClient.put ( + public async UpdateVrfcDetail( + param: VerifyDetailUpdateRequest, + ): Promise { + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`, - param + param, ); return response.data; } - public async CopyVrfcData(param: CopyVerifyDataRequest): Promise { - const response = await stClient.post ( + public async CopyVrfcData( + param: CopyVerifyDataRequest, + ): Promise { + const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/copy?targetStatsCd=${param.targetStatsCd}&rgtrId=${param.rgtrId}`, ); return response.data; } + + public async getVerifyResultList( + param: VerifyResultListRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`, + { + params: { + ...param.searchDto, + page: param.page, + size: param.size, + }, + }, + ); + return response.data; + } } -export const generalverificationApiService = new GeneralverificationApiService(); \ No newline at end of file +export const generalverificationApiService = + new GeneralverificationApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-status-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-status-service.ts index bed214f5b..20e446d41 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-status-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-status-service.ts @@ -1,14 +1,16 @@ -import { ENDPOINT } from "@/shared/config"; import { stClient } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; import type { StatisticsInputStatusService, - StatsInputStatusListRequestParams, StatsInputStatusListApiResponse, + StatsInputStatusListRequestParams, } from "../../model/input/statistics-input-status-interface"; export class StatisticsInputStatusApiService implements StatisticsInputStatusService { - public async getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise { + public async getStatisticsInputStatusList( + param: StatsInputStatusListRequestParams, + ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status`, { params: param }, @@ -17,4 +19,4 @@ export class StatisticsInputStatusApiService implements StatisticsInputStatusSer } } -export const StatisticsInputStatusApi = new StatisticsInputStatusApiService(); \ No newline at end of file +export const StatisticsInputStatusApi = new StatisticsInputStatusApiService(); diff --git a/apps/st/src/entities/statistics/api/queries.ts b/apps/st/src/entities/statistics/api/queries.ts index 0f0a9b0fb..a729cbbe9 100644 --- a/apps/st/src/entities/statistics/api/queries.ts +++ b/apps/st/src/entities/statistics/api/queries.ts @@ -1,10 +1,24 @@ -import { useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; +import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service"; +import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service"; +import type { VerifyResultPageData } from "@/entities/statistics/model/general-verification-interface"; +import type { + StatisticsVerificationFilterState, + StatisticsVerificationSubFilterState, +} from "@/entities/statistics/model/statistics-verification-report/statistics-verification-report-types"; import { stAnlsMstApi } from "./statistics-analysis-master-api"; import { stAnlsRsltApi } from "./statistics-analysis-result-api"; +const DETAIL_PAGE_SIZE = 100; + +interface StatisticsVerificationDetailSubmittedParams { + main: StatisticsVerificationFilterState; + sub: StatisticsVerificationSubFilterState; +} + export const useStAnlsSidoCombo = () => { return useQuery({ queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"], @@ -58,3 +72,88 @@ export const useStAnlsChartData = ( enabled: !!anlsSn && !!vrfcTrgtSn, }); }; + +export const useStatsVrfcRptData = ( + submittedMainFilters: StatisticsVerificationFilterState | null, +) => { + return useQuery({ + queryKey: [ + ENDPOINT.ST_SERVICE.INPUT, + "statistics-input-status", + submittedMainFilters?.statsYr ?? "", + submittedMainFilters?.ctpvNm ?? "", + submittedMainFilters?.sggNm ?? "", + submittedMainFilters?.sewerBzmnCd ?? "", + submittedMainFilters?.statsCd ?? "", + ], + queryFn: async () => { + if (!submittedMainFilters) return []; + const response = + await StatisticsInputStatusApi.getStatisticsInputStatusList( + submittedMainFilters, + ); + if (response.status !== "success") + throw new Error(response.message); + return response.data ?? []; + }, + enabled: Boolean(submittedMainFilters), + refetchOnWindowFocus: false, + }); +}; + +export const useStatsVrfcRsltData = ( + submittedDetailParams: StatisticsVerificationDetailSubmittedParams | null, +) => { + return useInfiniteQuery({ + queryKey: [ + ENDPOINT.ST_SERVICE.INPUTVERIFY, + "verify-results", + submittedDetailParams?.main.statsYr ?? "", + submittedDetailParams?.main.ctpvNm ?? "", + submittedDetailParams?.main.sggNm ?? "", + submittedDetailParams?.main.sewerBzmnCd ?? "", + submittedDetailParams?.main.statsCd ?? "", + submittedDetailParams?.sub.prcsYnList ?? "", + submittedDetailParams?.sub.pbcpAprvYn ?? "", + ], + initialPageParam: 1, + queryFn: async ({ pageParam }) => { + if (!submittedDetailParams) + return { + verifyResultList: [], + pageInfo: { + currentPage: 0, + size: DETAIL_PAGE_SIZE, + totalElements: 0, + totalPages: 0, + }, + } satisfies VerifyResultPageData; + + const response = + await generalverificationApiService.getVerifyResultList({ + page: pageParam, + size: DETAIL_PAGE_SIZE, + searchDto: { + statsYr: submittedDetailParams.main.statsYr, + ctpvNm: submittedDetailParams.main.ctpvNm, + sggNm: submittedDetailParams.main.sggNm, + sewerBzmnCd: submittedDetailParams.main.sewerBzmnCd, + statsCd: submittedDetailParams.main.statsCd, + prcsYnList: submittedDetailParams.sub.prcsYnList, + pbcpAprvYn: submittedDetailParams.sub.pbcpAprvYn, + }, + }); + + if (response.status !== "success") + throw new Error(response.message); + return response.data; + }, + getNextPageParam: (lastPage) => { + const { currentPage, totalPages } = lastPage.pageInfo; + if (currentPage + 1 <= totalPages) return currentPage + 1; + return undefined; + }, + enabled: Boolean(submittedDetailParams), + refetchOnWindowFocus: false, + }); +}; diff --git a/apps/st/src/entities/statistics/api/statistics-approval-service.ts b/apps/st/src/entities/statistics/api/statistics-approval-service.ts new file mode 100644 index 000000000..bbfb9f943 --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-approval-service.ts @@ -0,0 +1,22 @@ +import { stClient } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; + +import type { + ApprovalRequest, + ApprovalResponse, + StatisticsApprovalService, +} from "../model/statistics-approval-interface"; + +class StatisticsApprovalApiService implements StatisticsApprovalService { + public async approveStats( + params: ApprovalRequest[], + ): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.APPROVAL}?type=VRFC&action=APPROVE`, + params, + ); + return response.data; + } +} + +export const statisticsApprovalApi = new StatisticsApprovalApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index ef8bbbf2e..755ec96c9 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -1,19 +1,19 @@ +import { client, stClient } from "@/shared/api/client"; import { ENDPOINT } from "@/shared/config"; -import { stClient, client } from "@/shared/api/client"; import { - type StatisticsService, - type SearchCondition, type ApiResponse, - type Sgg, type cancelSubmitRequest, type cancelSubmitResponse, type ImrRequest, - type SubmitExceptionRequest, - type SubmitExceptionResponse, + type SearchCondition, + type Sgg, + type StatisticsLogDetailResponse, type StatisticsLogRequest, type StatisticsLogResponse, - type StatisticsLogDetailResponse, + type StatisticsService, + type SubmitExceptionRequest, + type SubmitExceptionResponse, } from "../model/statistics-interface"; export class StatisticsAPiService implements StatisticsService { @@ -41,10 +41,12 @@ export class StatisticsAPiService implements StatisticsService { } // 입력 - 제출 취소 - public async cancelSubmit(param: cancelSubmitRequest): Promise { + public async cancelSubmit( + param: cancelSubmitRequest, + ): Promise { const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/cancel-submit`, - param + param, ); return response.data; } @@ -52,21 +54,25 @@ export class StatisticsAPiService implements StatisticsService { public async callImrService(param: ImrRequest): Promise { const response = await client.post( `${ENDPOINT.STATVERIFY_SERVICE.IMR}`, - param + param, ); return response.data; } // 예외 제출 - public async submitException(param: SubmitExceptionRequest): Promise { + public async submitException( + param: SubmitExceptionRequest, + ): Promise { const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/submit-exception`, - param + param, ); return response.data; } - public async getLogList(param: StatisticsLogRequest): Promise { + public async getLogList( + param: StatisticsLogRequest, + ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/logs`, { params: param }, @@ -74,7 +80,9 @@ export class StatisticsAPiService implements StatisticsService { return response.data; } - public async getLogDetail(param: string): Promise { + public async getLogDetail( + param: string, + ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/logs/${param}`, ); @@ -82,4 +90,4 @@ export class StatisticsAPiService implements StatisticsService { } } -export const statisticsAPi = new StatisticsAPiService(); \ No newline at end of file +export const statisticsAPi = new StatisticsAPiService(); diff --git a/apps/st/src/entities/statistics/index.ts b/apps/st/src/entities/statistics/index.ts index 4c331287a..3b28503b0 100644 --- a/apps/st/src/entities/statistics/index.ts +++ b/apps/st/src/entities/statistics/index.ts @@ -1,3 +1,10 @@ +export { + useStAnlsSidoCombo, + useStAnlsSigunguCombo, + useStatsVrfcRptData, + useStatsVrfcRsltData, +} from "./api/queries"; +export { statisticsApprovalApi } from "./api/statistics-approval-service"; export { type AnalysisHistoryListApiResponse, type AnalysisRequestPayload, @@ -54,6 +61,10 @@ export { } from "./model/statistics-analysis/statistics-analysis-result-interface"; export { verificationHistoryColumns } from "./model/statistics-analysis/verification-history-columns"; export { getVerificationListColumns } from "./model/statistics-analysis/verification-list-columns"; +export { + type ApprovalRequest, + type ApprovalResponse, +} from "./model/statistics-approval-interface"; export { hbArticleColumnInfo, imrArticleColumnInfo, diff --git a/apps/st/src/entities/statistics/model/general-verification-interface.ts b/apps/st/src/entities/statistics/model/general-verification-interface.ts index daf822113..b63f20259 100644 --- a/apps/st/src/entities/statistics/model/general-verification-interface.ts +++ b/apps/st/src/entities/statistics/model/general-verification-interface.ts @@ -86,7 +86,7 @@ export interface VrfcOperatorsResponse { opertSn: string; opertSymbl: string; opertKornNm: string; - }[] + }[]; } /** 검증 상세 - 공통 정보 */ @@ -165,48 +165,133 @@ export interface SelectedVerifyColumnListResponseData { export interface SelectedVerifyColumnListResponse { status: string; message?: string; - data: VerifyColumnListResponseData + data: VerifyColumnListResponseData; } export interface VerifyDetailRegistData { trgtStatsCd: string; refStatsCd: string | null; - kornArtopnEqtnCn: string, - engArtopnEqtnCn: string, + kornArtopnEqtnCn: string; + engArtopnEqtnCn: string; cmprOpert: string | null; - eqtnCombSeq: number + eqtnCombSeq: number; } export interface VerifyDetailRegistRequest { vrfcSn: number | null; - detailList: VerifyDetailRegistData[] + detailList: VerifyDetailRegistData[]; rgtrId: string; } export interface VerifyDetailUpdateRequest { vrfcSn: number | null; - detailList: VerifyDetailRegistData[] - mdfrId: string; + detailList: VerifyDetailRegistData[]; + mdfrId: string; } export interface CopyVerifyDataRequest { vrfcSn?: number | null; - targetStatsCd : string; + targetStatsCd: string; rgtrId: string; } +export interface VerifyResultSearchRequest { + sewerBzmnCd?: string | null; + ctpvNm?: string | null; + sggNm?: string | null; + statsYr: string; + statsCd?: string | null; + vrfcMthdCd?: string; + prcsYnList?: string; // 검증 상태 리스트 (0: 미검증, 1: 수정완료, 2:예외통과, 3: 반려) + pbcpAprvYn?: string; // 공단 승인 여부 (Y/N) +} + +export interface VerifyResultListRequest { + searchDto: VerifyResultSearchRequest; + page?: number; + size?: number; +} + +export interface VerifyResultItem { + statsYr?: string; + statsCd?: string; + statsNm?: string; + sewerBzmnCd?: string; + ctpvNm?: string; + sggNm?: string; + admdstNm?: string; + vrfcSn?: number; + vrfcMthdCd?: string; + vrfcMthdNm?: string; + vrfcTrgtNm?: string; + regDt?: string; + rsnExpln?: string; + nowVlCn?: string; + compVlCn?: string; + prcsYn?: string; + prcsOpnnExpln?: string; + prcrId?: string; + rjctOpnnCn?: string; + pbcpAprvYn?: string; + mdfrId?: string; + mdfcnDt?: string; +} + +export interface VerifyResultPageInfo { + totalElements: number; + totalPages: number; + currentPage: number; + size: number; +} + +export interface VerifyResultPageData { + verifyResultList: VerifyResultItem[]; + pageInfo: VerifyResultPageInfo; +} + +export interface VerifyResultListResponse { + status: string; + message?: string; + data: VerifyResultPageData; + code?: string; + httpStatus?: number; +} + export interface GeneralverificationService { - getGeneralverificationList(param: GeneralverificationRequest): Promise; - getGeneralverificationInfo(param: number): Promise; - checkVerificationName(param: CheckVerificationNameRequest): Promise; - UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise; - RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise; + getGeneralverificationList( + param: GeneralverificationRequest, + ): Promise; + getGeneralverificationInfo( + param: number, + ): Promise; + checkVerificationName( + param: CheckVerificationNameRequest, + ): Promise; + UpdateVrfcMaster( + param: UpdateVrfcMasterRequest, + ): Promise; + RegistVrfcMaster( + param: RegistVrfcMasterRequest, + ): Promise; DeleteVrfcMaster(param: number): Promise; getVrfcOperators(): Promise; getVrfcDetail(param: number): Promise; - getVerifyColumnList(param: VerifyColumnListRequest): Promise; - getSelectedVerifyColumnList(param: string): Promise; - RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise; - UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise; - CopyVrfcData(param: CopyVerifyDataRequest): Promise; -} \ No newline at end of file + getVerifyColumnList( + param: VerifyColumnListRequest, + ): Promise; + getSelectedVerifyColumnList( + param: string, + ): Promise; + RegistVrfcDetail( + param: VerifyDetailRegistRequest, + ): Promise; + UpdateVrfcDetail( + param: VerifyDetailUpdateRequest, + ): Promise; + CopyVrfcData( + param: CopyVerifyDataRequest, + ): Promise; + getVerifyResultList( + param: VerifyResultListRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-status-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-status-interface.ts index ea6c281fe..4e77492fe 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-status-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-status-interface.ts @@ -1,12 +1,11 @@ import type { ApiResponse } from "../statistics-interface"; export interface StatsInputStatusListRequestParams { - ctpvNm?: string; - sggNm?: string; - sewerBzmnCd?: string; - statsYr?: string; - mode?: string; - statsCd?: string; + ctpvNm?: string | null; + sggNm?: string | null; + sewerBzmnCd?: string | null; + statsYr: string; + statsCd?: string | null; } export interface StatsInputStatusList { diff --git a/apps/st/src/entities/statistics/model/statistics-approval-interface.ts b/apps/st/src/entities/statistics/model/statistics-approval-interface.ts new file mode 100644 index 000000000..9c0947967 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-approval-interface.ts @@ -0,0 +1,22 @@ +export interface ApprovalRequest { + statsYr: string; + sewerBzmnCd: string; + statsCd: string; + mdfrId: string; + rjctOpnnCn: string; + lastCfmtnPicNm: string; + lastCfmtnPicTelNo: string; + lastCfmtnPicDeptNm: string; + lastCfmtnPicJbgdNm: string; +} + +export interface ApprovalResponse { + resultCode: string; + message: string; + updatedStts: string; + exceptionCnt: number; +} + +export interface StatisticsApprovalService { + approveStats(params: ApprovalRequest[]): Promise; +} diff --git a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts index 7ee24a013..3ed4f115c 100644 --- a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts +++ b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts @@ -4,68 +4,217 @@ const READ_ONLY_STYLE = "rg-cell-readonly"; export const statisticsVerificationSummaryColumnInfo: ColumnInfo = { columns: [ - { name: "summaryId", fieldName: "summaryId", header: { text: "summaryId" }, visible: false }, - { name: "sido", fieldName: "sido", header: { text: "시도" }, styleName: READ_ONLY_STYLE }, - { name: "sigungu", fieldName: "sigungu", header: { text: "시군구" }, styleName: READ_ONLY_STYLE }, - { name: "reportType", fieldName: "reportType", header: { text: "구분" }, styleName: READ_ONLY_STYLE }, - { name: "progressStatus", fieldName: "progressStatus", header: { text: "진행상태" }, width: 120, styleName: READ_ONLY_STYLE }, - { name: "writerName", fieldName: "writerName", header: { text: "작성자" }, styleName: READ_ONLY_STYLE }, - { name: "writerContact", fieldName: "writerContact", header: { text: "작성자연락처" }, width: 130, styleName: READ_ONLY_STYLE }, - { name: "writerDept", fieldName: "writerDept", header: { text: "부서" }, styleName: READ_ONLY_STYLE }, - { name: "writerPosition", fieldName: "writerPosition", header: { text: "직급" }, styleName: READ_ONLY_STYLE }, - { name: "approverName", fieldName: "approverName", header: { text: "승인자" }, styleName: READ_ONLY_STYLE }, - { name: "approverDept", fieldName: "approverDept", header: { text: "부서" }, styleName: READ_ONLY_STYLE }, - { name: "approverPosition", fieldName: "approverPosition", header: { text: "직급" }, styleName: READ_ONLY_STYLE }, - { name: "returnReason", fieldName: "returnReason", header: { text: "반려사유" }, width: 220, styleName: READ_ONLY_STYLE }, + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "menuTabName", + fieldName: "menuTabName", + header: { text: "구분" }, + width: 150, + styleName: READ_ONLY_STYLE, + }, + { + name: "locgStatusNm", + fieldName: "locgStatusNm", + header: { text: "진행상태" }, + width: 120, + styleName: READ_ONLY_STYLE, + }, + { + name: "laePicNm", + fieldName: "laePicNm", + header: { text: "작성자" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "laePicTelNo", + fieldName: "laePicTelNo", + header: { text: "작성자연락처" }, + width: 130, + styleName: READ_ONLY_STYLE, + }, + { + name: "laeKbrdrDeptNm", + fieldName: "laeKbrdrDeptNm", + header: { text: "부서" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "laeKbrdrJbgdNm", + fieldName: "laeKbrdrJbgdNm", + header: { text: "직급" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "laeIdfrNm", + fieldName: "laeIdfrNm", + header: { text: "승인자" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "laeIdfrDeptNm", + fieldName: "laeIdfrDeptNm", + header: { text: "부서" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "laeIdfrJbgdNm", + fieldName: "laeIdfrJbgdNm", + header: { text: "직급" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "rjctOpnnCn", + fieldName: "rjctOpnnCn", + header: { text: "반려사유" }, + width: 220, + styleName: READ_ONLY_STYLE, + }, + { + name: "sewerBzmnCd", + fieldName: "sewerBzmnCd", + header: { text: "sewerBzmnCd" }, + visible: false, + }, + { + name: "statsCd", + fieldName: "statsCd", + header: { text: "statsCd" }, + visible: false, + }, ], columnLayout: [ - "summaryId", - "sido", - "sigungu", - "reportType", - "progressStatus", - "writerName", - "writerContact", - "writerDept", - "writerPosition", - "approverName", - "approverDept", - "approverPosition", - "returnReason", + "ctpvNm", + "sggNm", + "menuTabName", + "locgStatusNm", + "laePicNm", + "laePicTelNo", + "laeKbrdrDeptNm", + "laeKbrdrJbgdNm", + "laeIdfrNm", + "laeIdfrDeptNm", + "laeIdfrJbgdNm", + "rjctOpnnCn", + "sewerBzmnCd", + "statsCd", ], }; export const statisticsVerificationDetailColumnInfo: ColumnInfo = { columns: [ - { name: "detailId", fieldName: "detailId", header: { text: "detailId" }, visible: false }, - { name: "summaryId", fieldName: "summaryId", header: { text: "summaryId" }, visible: false }, - { name: "sido", fieldName: "sido", header: { text: "시도" }, styleName: READ_ONLY_STYLE }, - { name: "sigungu", fieldName: "sigungu", header: { text: "시군구" }, styleName: READ_ONLY_STYLE }, - { name: "indicator", fieldName: "indicator", header: { text: "지표" }, width: 140, styleName: READ_ONLY_STYLE }, - { name: "detailItem", fieldName: "detailItem", header: { text: "세부항목" }, width: 180, styleName: READ_ONLY_STYLE }, - { name: "verifyDate", fieldName: "verifyDate", header: { text: "검증날짜" }, width: 110, styleName: READ_ONLY_STYLE }, - { name: "errorDetail", fieldName: "errorDetail", header: { text: "오류내역" }, width: 200, styleName: READ_ONLY_STYLE }, - { name: "inputValue", fieldName: "inputValue", header: { text: "입력값" }, width: 90, styleName: READ_ONLY_STYLE }, - { name: "compareValue", fieldName: "compareValue", header: { text: "비교값" }, width: 90, styleName: READ_ONLY_STYLE }, - { name: "status", fieldName: "status", header: { text: "상태" }, width: 90, styleName: READ_ONLY_STYLE }, - { name: "exceptionReason", fieldName: "exceptionReason", header: { text: "예외사유" }, width: 170, styleName: READ_ONLY_STYLE }, - { name: "returnOpinion", fieldName: "returnOpinion", header: { text: "반려의견" }, width: 170, styleName: READ_ONLY_STYLE }, - { name: "approvalYn", fieldName: "approvalYn", header: { text: "승인여부" }, width: 90, styleName: READ_ONLY_STYLE }, + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + styleName: READ_ONLY_STYLE, + }, + { + name: "statsNm", + fieldName: "statsNm", + header: { text: "지표" }, + width: 140, + styleName: READ_ONLY_STYLE, + }, + { + name: "vrfcTrgtNm", + fieldName: "vrfcTrgtNm", + header: { text: "세부항목" }, + width: 180, + styleName: READ_ONLY_STYLE, + }, + { + name: "regDt", + fieldName: "regDt", + header: { text: "검증날짜" }, + width: 150, + styleName: READ_ONLY_STYLE, + }, + { + name: "rsnExpln", + fieldName: "rsnExpln", + header: { text: "오류내역" }, + width: 200, + styleName: READ_ONLY_STYLE, + }, + { + name: "nowVlCn", + fieldName: "nowVlCn", + header: { text: "입력값" }, + width: 90, + styleName: READ_ONLY_STYLE, + }, + { + name: "compVlCn", + fieldName: "compVlCn", + header: { text: "비교값" }, + width: 90, + styleName: READ_ONLY_STYLE, + }, + { + name: "prcsYn", + fieldName: "prcsYn", + header: { text: "상태" }, + width: 90, + styleName: READ_ONLY_STYLE, + }, + { + name: "prcsOpnnExpln", + fieldName: "prcsOpnnExpln", + header: { text: "예외사유" }, + width: 170, + styleName: READ_ONLY_STYLE, + }, + { + name: "rjctOpnnCn", + fieldName: "rjctOpnnCn", + header: { text: "반려의견" }, + width: 170, + styleName: READ_ONLY_STYLE, + }, + { + name: "pbcpAprvYn", + fieldName: "pbcpAprvYn", + header: { text: "승인여부" }, + width: 90, + styleName: READ_ONLY_STYLE, + }, + { + name: "vrfcSn", + fieldName: "vrfcSn", + header: { text: "vrfcSn" }, + visible: false, + }, ], columnLayout: [ - "detailId", - "summaryId", - "sido", - "sigungu", - "indicator", - "detailItem", - "verifyDate", - "errorDetail", - "inputValue", - "compareValue", - "status", - "exceptionReason", - "returnOpinion", - "approvalYn", + "ctpvNm", + "sggNm", + "statsNm", + "vrfcTrgtNm", + "regDt", + "rsnExpln", + "nowVlCn", + "compVlCn", + "prcsYn", + "prcsOpnnExpln", + "rjctOpnnCn", + "pbcpAprvYn", + "vrfcSn", ], }; diff --git a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-types.ts b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-types.ts index 2cbcd04d9..d81ee98ee 100644 --- a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-types.ts +++ b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-types.ts @@ -1,14 +1,10 @@ -export interface StatisticsVerificationFilterState { - sido: string; - sigungu: string; - year: string; - statsIndicator: string; -} +import type { VerifyResultSearchRequest } from "../general-verification-interface"; +import type { StatsInputStatusListRequestParams } from "../input/statistics-input-status-interface"; -export interface StatisticsVerificationSubFilterState { - status: string; - approvalStatus: string; -} +export type StatisticsVerificationFilterState = + StatsInputStatusListRequestParams; + +export type StatisticsVerificationSubFilterState = VerifyResultSearchRequest; export interface StatisticsVerificationOption { label: string; diff --git a/apps/st/src/shared/config/api.ts b/apps/st/src/shared/config/api.ts index 12da1ddd5..984135262 100644 --- a/apps/st/src/shared/config/api.ts +++ b/apps/st/src/shared/config/api.ts @@ -72,7 +72,8 @@ export const ENDPOINT = { REVIEW: "/st-management-service/api/v1/review", INPUTCOMMON: "/st-input-service/api/v1/common", INPUTVERIFY: "/st-input-service/api/v1/verify", - TOTAL: "/st-management-service/api/v1/stats" + TOTAL: "/st-management-service/api/v1/stats", + APPROVAL: "/st-management-service/api/stats/approval", }, STATVERIFY_SERVICE: { VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng", diff --git a/apps/st/tsconfig.json b/apps/st/tsconfig.json index 2061c7cb4..7e8241dc4 100644 --- a/apps/st/tsconfig.json +++ b/apps/st/tsconfig.json @@ -1,44 +1,44 @@ { - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "verbatimModuleSyntax": true, - "strictNullChecks": true, - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "strictNullChecks": true, + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": "./src", + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + ".eslintrc.*", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.js", + "**/*.cjs", + "**/*.mjs", + "**/*.jsx", + "**/*.json", + ".next/types/**/*.ts" ], - "baseUrl": "./src", - "paths": { - "@/*": ["./*"] - } - }, - "include": [ - ".eslintrc.*", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.mts", - "**/*.js", - "**/*.cjs", - "**/*.mjs", - "**/*.jsx", - "**/*.json", - ".next/types/**/*.ts" - ], - "exclude": ["**/node_modules", "**/.*/*"] + "exclude": ["**/node_modules", "**/.*/*"] } diff --git a/packages/common/components/package.json b/packages/common/components/package.json index f3485aac2..949f00e99 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -26,6 +26,7 @@ "clean": "rm -rf dist" }, "dependencies": { + "@base-ui/react": "^1.4.0", "@common/assets": "workspace:*", "@common/auth": "workspace:*", "@common/config": "workspace:*", diff --git a/packages/common/components/src/ui/combobox/index.tsx b/packages/common/components/src/ui/combobox/index.tsx new file mode 100644 index 000000000..64298bc4e --- /dev/null +++ b/packages/common/components/src/ui/combobox/index.tsx @@ -0,0 +1,315 @@ +"use client"; + +import * as React from "react"; +import { + Combobox as ComboboxPrimitive, + type ComboboxRootProps, +} from "@base-ui/react"; +import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"; + +import { cn } from "../../lib/utils"; +import { Button } from "../button"; +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "../input-group"; + +const Combobox = ({ ...props }: ComboboxRootProps) => { + return ; +}; + +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { + return ; +} + +function ComboboxTrigger({ + className, + children, + ...props +}: ComboboxPrimitive.Trigger.Props) { + return ( + + {children} + + + ); +} + +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { + return ( + + + + } + /> + ); +} + +function ComboboxInput({ + className, + children, + disabled = false, + showTrigger = true, + showClear = false, + ...props +}: ComboboxPrimitive.Input.Props & { + showTrigger?: boolean; + showClear?: boolean; +}) { + return ( + + } + {...props} + /> + + {showTrigger && ( + + )} + {showClear && } + + {children} + + ); +} + +function ComboboxContent({ + className, + side = "bottom", + sideOffset = 6, + align = "start", + alignOffset = 0, + anchor, + ...props +}: ComboboxPrimitive.Popup.Props & + Pick< + ComboboxPrimitive.Positioner.Props, + "side" | "align" | "sideOffset" | "alignOffset" | "anchor" + >) { + return ( + + + + + + ); +} + +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { + return ( + + ); +} + +function ComboboxItem({ + className, + children, + ...props +}: ComboboxPrimitive.Item.Props) { + return ( + + {children} + + + + } + /> + + ); +} + +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { + return ( + + ); +} + +function ComboboxLabel({ + className, + ...props +}: ComboboxPrimitive.GroupLabel.Props) { + return ( + + ); +} + +function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { + return ( + + ); +} + +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { + return ( +