From b9cb8959ee4984e8f89cf461808b134579d802aa Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 3 Feb 2026 17:13:38 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EA=B7=9C=EC=B9=99=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=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/.gitignore | 3 +- apps/st/.prettierrc | 3 +- .../ManagementVerificationRule.tsx | 460 +++++++++++++++++- 3 files changed, 463 insertions(+), 3 deletions(-) diff --git a/apps/st/.gitignore b/apps/st/.gitignore index b63718b61..f06d9d40b 100644 --- a/apps/st/.gitignore +++ b/apps/st/.gitignore @@ -38,4 +38,5 @@ yarn-error.log* next-env.d.ts .env* -!.env.example \ No newline at end of file +!.env.example +*.md \ No newline at end of file diff --git a/apps/st/.prettierrc b/apps/st/.prettierrc index b4bfed357..a7963dac2 100644 --- a/apps/st/.prettierrc +++ b/apps/st/.prettierrc @@ -1,3 +1,4 @@ { - "plugins": ["prettier-plugin-tailwindcss"] + "plugins": ["prettier-plugin-tailwindcss"], + "tabWidth": 4 } diff --git a/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx b/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx index 6e9cc3a6e..b0c35ce1a 100644 --- a/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx +++ b/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx @@ -1,11 +1,469 @@ +"use client"; + +import { useState } from "react"; import SearchForm from "@/components/module/SearchForm"; +import { + Button, + Input, + Label, + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, + Checkbox, +} from "@common/components"; + +interface VerificationFormValues { + mu: string; + percentile: string; + a: string; + c: string; + verificationYear: string; + checkboxOptions: { + sidoDistribution: boolean; + countyDistribution: boolean; + countyDistributionUserManagement: boolean; + lowerDistribution: boolean; + sidoServiceAvailability: boolean; + individualService: boolean; + legalServiceUsage: boolean; + excessUse: boolean; + lowerOrgStatistics: boolean; + studentStatistics: boolean; + developmentStatistics: boolean; + lowerManagementEnterprise: boolean; + }; +} const ManagementVerificationRule = () => { + const [formValues, setFormValues] = useState({ + mu: "0", + percentile: "0", + a: "0", + c: "0", + verificationYear: "2025", + checkboxOptions: { + sidoDistribution: false, + countyDistribution: false, + countyDistributionUserManagement: false, + lowerDistribution: false, + sidoServiceAvailability: false, + individualService: false, + legalServiceUsage: false, + excessUse: false, + lowerOrgStatistics: false, + studentStatistics: false, + developmentStatistics: false, + lowerManagementEnterprise: false, + }, + }); + + const handleCheckboxChange = ( + key: keyof VerificationFormValues["checkboxOptions"], + ) => { + setFormValues((prev) => ({ + ...prev, + checkboxOptions: { + ...prev.checkboxOptions, + [key]: !prev.checkboxOptions[key], + }, + })); + }; + + const handleInputChange = ( + key: keyof Omit, + value: string, + ) => { + setFormValues((prev) => ({ + ...prev, + [key]: value, + })); + }; + + const handleSubmit = () => { + console.log("검증 실행:", formValues); + }; + + const handleReset = () => { + console.log("초기화"); + }; + return ( <> + +
+
+ {/* 통계 검증 섹션 */} +
+

+ 통계 검증 +

+ +
+ {/* 왼쪽: 검증 변수 */} +
+

+ 검증 변수 +

+ +
+
+ + + handleInputChange( + "mu", + e.target.value, + ) + } + className="flex-1" + /> +
+ +
+ + + handleInputChange( + "percentile", + e.target.value, + ) + } + className="flex-1" + /> +
+ +
+ + + handleInputChange( + "a", + e.target.value, + ) + } + className="flex-1" + /> +
+ +
+ + + handleInputChange( + "c", + e.target.value, + ) + } + className="flex-1" + /> +
+ +
+ + +
+
+
+ + {/* 오른쪽: 검증 대상 */} +
+

+ 검증 대상 +

+ +
+
+
+ + handleCheckboxChange( + "sidoDistribution", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "countyDistribution", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "countyDistributionUserManagement", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "lowerDistribution", + ) + } + /> + +
+
+ +
+
+ + handleCheckboxChange( + "sidoServiceAvailability", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "individualService", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "sidoServiceAvailability", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "individualService", + ) + } + /> + +
+
+ +
+
+ + handleCheckboxChange( + "lowerOrgStatistics", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "studentStatistics", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "lowerOrgStatistics", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "studentStatistics", + ) + } + /> + +
+
+ +
+
+ + handleCheckboxChange( + "lowerManagementEnterprise", + ) + } + /> + +
+
+
+
+
+
+ + {/* 버튼 */} +
+ + +
+
+
); }; -export default ManagementVerificationRule; \ No newline at end of file +export default ManagementVerificationRule; From 0e90277e90b339c08dcbe57d79f66fe0ecfca491 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 5 Feb 2026 11:04:25 +0900 Subject: [PATCH 2/4] =?UTF-8?q?st:=20fix:=20=EC=9E=85=EB=A0=A5-=ED=95=98?= =?UTF-8?q?=EC=88=98=EB=8F=84=EB=B3=B4=EA=B8=89=EB=A5=A0=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/column/input/SeweragePenetrationRateInpColumn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts index e2cb0ca12..182d2d50d 100644 --- a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts +++ b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts @@ -13,7 +13,7 @@ export const SeweragePenetrationRateInpColumn = { // 수계 및 권역 { name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false }, - { name: 'bstrm', fieldName: 'bstrm', header: { text: '지류' }, editable: false }, + { name: 'bstrm', fieldName: 'bstrm', header: { text: '지류' } }, // 수계영향권별분류 { name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false }, From 4b0b08761cbeca37e0f6e253e6024bcf57365422 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 5 Feb 2026 14:47:47 +0900 Subject: [PATCH 3/4] =?UTF-8?q?st:=20=EA=B2=80=ED=86=A0=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20grid=20footer=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EB=B6=80=EB=B6=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/module/DataGrid.tsx | 8 +- apps/st/src/components/module/SearchForm.tsx | 14 +++- .../review/SeweragePenetrationRateRvw.tsx | 45 ++++++---- .../src/components/util/grid/footerSummary.ts | 84 ++++++++----------- .../src/components/util/grid/gridFunction.ts | 13 ++- .../statistics/model/statistics-interface.ts | 7 +- .../model/statistics-management-interface.ts | 2 +- 7 files changed, 97 insertions(+), 76 deletions(-) diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index cdf0e286d..520a6c47f 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -155,16 +155,16 @@ function updateHeaderSortIndicator( function createOnSortingHandler( columns: { fieldName?: string; name: string; mergeCells?: boolean; sortdisable?: boolean; valueType?: ValueType }[], setRowData: React.Dispatch[]>>, - sortDirRef: React.MutableRefObject>, - originalRowsRef: React.MutableRefObject[]>> + sortDirRef: { current: Record }, + originalRowsRef: { current: Record[]> } ) { const fieldNames = [...new Set(columns.map((c) => c.fieldName).filter((x): x is string => typeof x === "string"))]; // mergeCells: true인 컬럼 찾기 (시군구 등) - 그룹 필드로 사용 // 시군구를 우선으로 찾고, 없으면 첫 번째 mergeCells 컬럼 사용 const mergeCellCols = columns.filter((c) => c.mergeCells === true && c.fieldName); - const mergeCellCol = mergeCellCols.find((c) => c.fieldName === '시군구') || mergeCellCols[0]; + const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'gugun') || mergeCellCols[0]; const groupField = mergeCellCol?.fieldName || null; - const orderByField = '구분'; // 고정값 또는 설정 가능하게 할 수 있음 + const orderByField = 'rgnNm'; // 고정값 또는 설정 가능하게 할 수 있음 const hasMergeCells = mergeCellCols.length > 0; return function onSorting(g: unknown, fields: unknown[], _directions: unknown[]) { diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 0b50ecf89..91d823ed6 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -118,6 +118,18 @@ const SearchForm = (props: SearchFormProps = {}) => { })); }; + const handleSggChange = (value: string) => { + const selectedBsnm = value === 'ALL' + ? (sidoList?.data.sidoList.find((v) => v.sido === values.sido)?.bsnmCd || '') + : (sggList?.data?.find((v) => v.gugun === value)?.bsnmCd || ''); + + setValues((prev) => ({ + ...prev, + gugun: value, + bsnmCd: selectedBsnm, + })); + }; + useEffect(() => { props.onValuesChange?.(values); }, [values]); @@ -151,7 +163,7 @@ const SearchForm = (props: SearchFormProps = {}) => { - diff --git a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx index 950f43988..036af18b8 100644 --- a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx +++ b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; -import { DataGrid } from "@rsone/components"; +import { useState, useMemo } from "react"; +// import { DataGrid } from "@rsone/components"; +import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; import SaveButton from "@/components/button/SaveButton"; import ApproveButton from "@/components/button/ApproveButton"; @@ -12,12 +13,27 @@ import { sewerageSupplyRateReviewApi } from "@/entities/statistics/api/statistic import type { SewerageSupplyRateReviewItem } from "@/entities/statistics/model/statistics-management-interface"; import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; // 하수도 통계 검토 01. 하수도 보급률 const SeweragePenetrationRateRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); - -const [rowData, setRowData] = useState([]); + + const columnInfo = useMemo(() => { + const { columns, columnLayout, fixedColumns, rowStartNum } = SeweragePenetrationRateInfoList; + return { + columnLayout, + fixedColumns, + rowStartNum, + columns: columns.map((col) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + }; + }, []); + + const [rowData, setRowData] = useState([]); const [isLoading, setIsLoading] = useState(false); const handleReset = () => { @@ -29,10 +45,10 @@ const [rowData, setRowData] = useState([]); setIsLoading(true); const response = await sewerageSupplyRateReviewApi.getSewerageSupplyRateReviewList({ - sido: "경기도", - gugun: "과천시", - yr: 2024, - bsnmCd: "4129000000" + sido: values.sido, + gugun: values.gugun === 'ALL' ? '' : values.gugun, + bsnmCd: values.bsnmCd, + yr: values.year, }); console.log("네트워크 응답 전체:", response); @@ -42,14 +58,13 @@ const [rowData, setRowData] = useState([]); console.log("받아온 리스트:", rawList); - const convertedData: SewerageSupplyRateReviewItem[] = rawList.map((item) => ({ ...item, speclCntrplnAr: !item.speclCntrplnAr ? '해당없음' : item.speclCntrplnAr, })); - setRowData(convertedData); + setRowData(toGridRows(convertedData)); } else { setRowData([]); @@ -80,12 +95,12 @@ const [rowData, setRowData] = useState([]); ); diff --git a/apps/st/src/components/util/grid/footerSummary.ts b/apps/st/src/components/util/grid/footerSummary.ts index cbf60db84..39825a0dd 100644 --- a/apps/st/src/components/util/grid/footerSummary.ts +++ b/apps/st/src/components/util/grid/footerSummary.ts @@ -48,8 +48,8 @@ export const getFooterAvgSummary = (grid: GridView, column: DataColumn) => { return Math.floor(avg * 10) / 10; } -// "구분" 필드의 고유값들을 찾는 함수 (첫 번째, 두 번째) -const getDivisionValues = (grid: GridView, divisionFieldName: string = '구분'): string[] => { +// 구분 필드의 고유값들을 찾는 함수 (첫 번째, 두 번째). 기본 필드명 'rgnNm' +const getDivisionValues = (grid: GridView, divisionFieldName: string = 'rgnNm'): string[] => { const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); const uniqueValues = new Set(); @@ -65,13 +65,13 @@ const getDivisionValues = (grid: GridView, divisionFieldName: string = '구분') return Array.from(uniqueValues); }; -// "구분" 필드 값에 따라 필터링하여 합계 계산하는 함수 +// 구분 필드 값에 따라 필터링하여 합계 계산하는 함수. 기본 필드명 'rgnNm' // divisionIndex: 0 = 첫 번째 고유값, 1 = 두 번째 고유값 export const getFooterSummaryByDivision = ( - grid: GridView, - column: DataColumn, + grid: GridView, + column: DataColumn, divisionIndex: number, - divisionFieldName: string = '구분' + divisionFieldName: string = 'rgnNm' ) => { const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -107,13 +107,13 @@ export const getFooterSummaryByDivision = ( return sum; }; -// "구분" 필드 값에 따라 필터링하여 평균 계산하는 함수 +// 구분 필드 값에 따라 필터링하여 평균 계산하는 함수. 기본 필드명 'rgnNm' // divisionIndex: 0 = 첫 번째 고유값, 1 = 두 번째 고유값 export const getFooterAvgSummaryByDivision = ( - grid: GridView, - column: DataColumn, + grid: GridView, + column: DataColumn, divisionIndex: number, - divisionFieldName: string = '구분' + divisionFieldName: string = 'rgnNm' ) => { const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -173,67 +173,51 @@ const createAvgFooter = (format: string) => ({ }, }); -// "구분" 값에 따라 필터링하는 Footer 배열 생성 함수 (2줄) -const createSumFooterArrayByDivision = (format: string) => [ +// 구분 값에 따라 필터링하는 Footer 배열 생성 함수 (2줄). 기본 필드명 'rgnNm' +const createSumFooterArrayByDivision = (format: string, divisionFieldName: string = 'rgnNm') => [ { numberFormat: format, valueCallback: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: unknown, + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: unknown, value: number | string | undefined - ) => { - // footerIndex: 0 = 첫 번째 footer 행 (첫 번째 "구분" 값), 1 = 두 번째 footer 행 (두 번째 "구분" 값) - // footerIndex를 divisionIndex로 사용 - return getFooterSummaryByDivision(grid, column, footerIndex); - }, + ) => getFooterSummaryByDivision(grid, column, footerIndex, divisionFieldName), }, { numberFormat: format, valueCallback: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: unknown, + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: unknown, value: number | string | undefined - ) => { - // footerIndex: 0 = 첫 번째 footer 행 (첫 번째 "구분" 값), 1 = 두 번째 footer 행 (두 번째 "구분" 값) - // footerIndex를 divisionIndex로 사용 - return getFooterSummaryByDivision(grid, column, footerIndex); - }, + ) => getFooterSummaryByDivision(grid, column, footerIndex, divisionFieldName), }, ]; -// "구분" 값에 따라 필터링하는 평균 Footer 배열 생성 함수 (2줄) -const createAvgFooterArrayByDivision = (format: string) => [ +// 구분 값에 따라 필터링하는 평균 Footer 배열 생성 함수 (2줄). 기본 필드명 'rgnNm' +const createAvgFooterArrayByDivision = (format: string, divisionFieldName: string = 'rgnNm') => [ { numberFormat: format, valueCallback: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: unknown, + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: unknown, value: number | string | undefined - ) => { - // footerIndex: 0 = 첫 번째 footer 행 (첫 번째 "구분" 값), 1 = 두 번째 footer 행 (두 번째 "구분" 값) - // footerIndex를 divisionIndex로 사용 - return getFooterAvgSummaryByDivision(grid, column, footerIndex); - }, + ) => getFooterAvgSummaryByDivision(grid, column, footerIndex, divisionFieldName), }, { numberFormat: format, valueCallback: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: unknown, + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: unknown, value: number | string | undefined - ) => { - // footerIndex: 0 = 첫 번째 footer 행 (첫 번째 "구분" 값), 1 = 두 번째 footer 행 (두 번째 "구분" 값) - // footerIndex를 divisionIndex로 사용 - return getFooterAvgSummaryByDivision(grid, column, footerIndex); - }, + ) => getFooterAvgSummaryByDivision(grid, column, footerIndex, divisionFieldName), }, ]; diff --git a/apps/st/src/components/util/grid/gridFunction.ts b/apps/st/src/components/util/grid/gridFunction.ts index 173a7ac6d..28755a7f8 100644 --- a/apps/st/src/components/util/grid/gridFunction.ts +++ b/apps/st/src/components/util/grid/gridFunction.ts @@ -1,10 +1,15 @@ -import type { SewerageSupplyRateItem } from "@/entities/statistics/model/statistics-input-interface"; - -export function toGridRows(items: SewerageSupplyRateItem[]): Record[] { +/** + * 객체 배열을 그리드 row 형식으로 변환 (null/undefined → ""). + * 어떤 엔티티 타입이든 공통 사용 가능. + */ +export function toGridRows( + items: T[] +): Record[] { return items.map((item) => { const row: Record = {}; for (const [key, value] of Object.entries(item)) { - row[key] = value === null || value === undefined ? "" : value; + row[key] = + value === null || value === undefined ? "" : (value as string | number); } return row; }); diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index 5ff8a5ed7..b909eba92 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -4,6 +4,11 @@ export interface Sido { bsnmCd: string; } +export interface SearchCondition { + yearList: string[]; + sidoList: Sido[]; +} + export interface Sgg { adstDstrCd: string; gugun: string; @@ -17,6 +22,6 @@ export interface ApiResponse { } export interface StatisticsService { - getSidoList(): Promise>; + getSidoList(): Promise>; getSggList(sidoName: string): Promise>; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-management-interface.ts b/apps/st/src/entities/statistics/model/statistics-management-interface.ts index 858859b28..b8078a186 100644 --- a/apps/st/src/entities/statistics/model/statistics-management-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-management-interface.ts @@ -83,7 +83,7 @@ export type SewerageSupplyRateReviewApiResponse = ApiResponse Date: Thu, 5 Feb 2026 15:14:54 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=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 | 3 +- .../ManagementVerificationRule.tsx | 711 +++++++++--------- .../model/verification-history-column.ts | 120 +++ .../model/verification-history-interface.ts | 27 + .../model/verification-history-mock.ts | 67 ++ pnpm-lock.yaml | 52 +- 6 files changed, 606 insertions(+), 374 deletions(-) create mode 100644 apps/st/src/entities/statistics/model/verification-history-column.ts create mode 100644 apps/st/src/entities/statistics/model/verification-history-interface.ts create mode 100644 apps/st/src/entities/statistics/model/verification-history-mock.ts diff --git a/apps/st/package.json b/apps/st/package.json index a501dde6a..cd951af0d 100644 --- a/apps/st/package.json +++ b/apps/st/package.json @@ -12,7 +12,7 @@ "@common/assets": "workspace:*", "@common/components": "workspace:*", "@common/config": "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", @@ -26,6 +26,7 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@rsone/components": "workspace:*", "@tanstack/react-query": "^5.67.1", "@tanstack/react-query-devtools": "^5.75.5", "@tanstack/react-table": "^8.21.2", diff --git a/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx b/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx index b0c35ce1a..1d5dfd169 100644 --- a/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx +++ b/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { DataGrid } from "@rsone/components"; import SearchForm from "@/components/module/SearchForm"; import { Button, @@ -13,6 +14,8 @@ import { SelectValue, Checkbox, } from "@common/components"; +import { verificationHistoryColumnInfo } from "@/entities/statistics/model/verification-history-column"; +import { verificationHistoryMockData } from "@/entities/statistics/model/verification-history-mock"; interface VerificationFormValues { mu: string; @@ -37,6 +40,11 @@ interface VerificationFormValues { } const ManagementVerificationRule = () => { + const [verificationHistoryRowData, setVerificationHistoryRowData] = + useState[]>( + verificationHistoryMockData, + ); + const [formValues, setFormValues] = useState({ mu: "0", percentile: "0", @@ -90,379 +98,384 @@ const ManagementVerificationRule = () => { }; return ( - <> - +
+
+ {/* 통계 검증 섹션 */} +
+

+ 통계 검증 +

-
-
- {/* 통계 검증 섹션 */} -
-

- 통계 검증 -

+
+ {/* 왼쪽: 검증 변수 */} +
+

+ 검증 변수 +

-
- {/* 왼쪽: 검증 변수 */} -
-

- 검증 변수 -

+
+
+ + + handleInputChange( + "mu", + e.target.value, + ) + } + className="flex-1" + /> +
-
-
- - - handleInputChange( - "mu", - e.target.value, - ) - } - className="flex-1" - /> -
+
+ + + handleInputChange( + "percentile", + e.target.value, + ) + } + className="flex-1" + /> +
-
- - - handleInputChange( - "percentile", - e.target.value, - ) - } - className="flex-1" - /> -
+
+ + + handleInputChange( + "a", + e.target.value, + ) + } + className="flex-1" + /> +
-
- - - handleInputChange( - "a", - e.target.value, - ) - } - className="flex-1" - /> -
+
+ + + handleInputChange( + "c", + e.target.value, + ) + } + className="flex-1" + /> +
-
- - - handleInputChange( - "c", - e.target.value, - ) - } - className="flex-1" - /> -
- -
- - -
+
+ +
+
- {/* 오른쪽: 검증 대상 */} -
-

- 검증 대상 -

+ {/* 오른쪽: 검증 대상 */} +
+

+ 검증 대상 +

-
-
-
- - handleCheckboxChange( - "sidoDistribution", - ) - } - /> - -
- -
- - handleCheckboxChange( - "countyDistribution", - ) - } - /> - -
- -
- - handleCheckboxChange( - "countyDistributionUserManagement", - ) - } - /> - -
- -
- - handleCheckboxChange( - "lowerDistribution", - ) - } - /> - -
+
+
+
+ + handleCheckboxChange( + "sidoDistribution", + ) + } + /> +
-
-
- - handleCheckboxChange( - "sidoServiceAvailability", - ) - } - /> - -
- -
- - handleCheckboxChange( - "individualService", - ) - } - /> - -
- -
- - handleCheckboxChange( - "sidoServiceAvailability", - ) - } - /> - -
- -
- - handleCheckboxChange( - "individualService", - ) - } - /> - -
+
+ + handleCheckboxChange( + "countyDistribution", + ) + } + /> +
-
-
- - handleCheckboxChange( - "lowerOrgStatistics", - ) - } - /> - -
- -
- - handleCheckboxChange( - "studentStatistics", - ) - } - /> - -
- -
- - handleCheckboxChange( - "lowerOrgStatistics", - ) - } - /> - -
- -
- - handleCheckboxChange( - "studentStatistics", - ) - } - /> - -
+
+ + handleCheckboxChange( + "countyDistributionUserManagement", + ) + } + /> +
-
-
- - handleCheckboxChange( - "lowerManagementEnterprise", - ) - } - /> - -
+
+ + handleCheckboxChange( + "lowerDistribution", + ) + } + /> + +
+
+ +
+
+ + handleCheckboxChange( + "sidoServiceAvailability", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "individualService", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "sidoServiceAvailability", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "individualService", + ) + } + /> + +
+
+ +
+
+ + handleCheckboxChange( + "lowerOrgStatistics", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "studentStatistics", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "lowerOrgStatistics", + ) + } + /> + +
+ +
+ + handleCheckboxChange( + "studentStatistics", + ) + } + /> + +
+
+ +
+
+ + handleCheckboxChange( + "lowerManagementEnterprise", + ) + } + /> +
+
- {/* 버튼 */} -
- - -
+ {/* 버튼 */} +
+ + +
+ + {/* 검증 이력 섹션 */} +
+

+ 검증 이력 +

+
- +
); }; diff --git a/apps/st/src/entities/statistics/model/verification-history-column.ts b/apps/st/src/entities/statistics/model/verification-history-column.ts new file mode 100644 index 000000000..aea9b7f4f --- /dev/null +++ b/apps/st/src/entities/statistics/model/verification-history-column.ts @@ -0,0 +1,120 @@ +import { ValueType } from "realgrid"; +import type { ColumnInfo } from "@rsone/components"; + +/** 검증 이력 그리드 컬럼 설정 */ +export const verificationHistoryColumnInfo: ColumnInfo = { + columns: [ + { + name: "executedAt", + fieldName: "executedAt", + header: { text: "검증 실행 일시" }, + editable: false, + sortdisable: false, + }, + { + name: "progressStatus", + fieldName: "progressStatus", + header: { text: "진행 상태" }, + editable: false, + sortdisable: false, + }, + { + name: "endedAt", + fieldName: "endedAt", + header: { text: "종료 일시" }, + editable: false, + sortdisable: false, + }, + { + name: "verificationYear", + fieldName: "verificationYear", + header: { text: "검증년도" }, + editable: false, + sortdisable: false, + }, + { + name: "mu", + fieldName: "mu", + header: { text: "μ" }, + editable: false, + sortdisable: false, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.###", + }, + { + name: "percentile", + fieldName: "percentile", + header: { text: "Percentile" }, + editable: false, + sortdisable: false, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.###", + }, + { + name: "a", + fieldName: "a", + header: { text: "A" }, + editable: false, + sortdisable: false, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.###", + }, + { + name: "c", + fieldName: "c", + header: { text: "C" }, + editable: false, + sortdisable: false, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.###", + }, + { + name: "target", + fieldName: "target", + header: { text: "검증 대상" }, + editable: false, + sortdisable: false, + }, + { + name: "normalCount", + fieldName: "normalCount", + header: { text: "정상건수" }, + editable: false, + sortdisable: false, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + }, + { + name: "excludedCount", + fieldName: "excludedCount", + header: { text: "제외건수" }, + editable: false, + sortdisable: false, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + }, + { + name: "abnormalCount", + fieldName: "abnormalCount", + header: { text: "이상건수" }, + editable: false, + sortdisable: false, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + }, + ], + columnLayout: [ + "executedAt", + "progressStatus", + "endedAt", + "verificationYear", + "mu", + "percentile", + "a", + "c", + "target", + "normalCount", + "excludedCount", + "abnormalCount", + ], +}; diff --git a/apps/st/src/entities/statistics/model/verification-history-interface.ts b/apps/st/src/entities/statistics/model/verification-history-interface.ts new file mode 100644 index 000000000..869399cba --- /dev/null +++ b/apps/st/src/entities/statistics/model/verification-history-interface.ts @@ -0,0 +1,27 @@ +/** 검증 이력 한 건 (그리드 행 데이터) */ +export interface VerificationHistoryItem { + /** 검증 실행 일시 */ + executedAt: string; + /** 진행 상태 */ + progressStatus: string; + /** 종료 일시 */ + endedAt: string; + /** 검증년도 */ + verificationYear: string; + /** μ */ + mu: number; + /** Percentile */ + percentile: number; + /** A */ + a: number; + /** C */ + c: number; + /** 검증 대상 */ + target: string; + /** 정상건수 */ + normalCount: number; + /** 제외건수 */ + excludedCount: number; + /** 이상건수 */ + abnormalCount: number; +} diff --git a/apps/st/src/entities/statistics/model/verification-history-mock.ts b/apps/st/src/entities/statistics/model/verification-history-mock.ts new file mode 100644 index 000000000..90c152be7 --- /dev/null +++ b/apps/st/src/entities/statistics/model/verification-history-mock.ts @@ -0,0 +1,67 @@ +import { fakerKO as faker } from "@faker-js/faker"; +import type { VerificationHistoryItem } from "./verification-history-interface"; + +const VERIFICATION_TARGETS = [ + "하수도 보급률", + "하수관로", + "하수관로 유지관리", + "하수저류시설 및 빗물펌프장", + "펌프장", + "공공하수처리시설", + "분뇨처리시설", + "개인하수처리시설", + "하수도재정", + "물 재이용", + "하수도 서비스 수준", +]; + +const PROGRESS_STATUSES = ["대기", "진행중", "완료", "실패"]; + +function formatDateTime(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + const h = String(date.getHours()).padStart(2, "0"); + const min = String(date.getMinutes()).padStart(2, "0"); + return `${y}-${m}-${d} ${h}:${min}`; +} + +/** 검증 이력 목데이터 생성 (그리드 행 형식) */ +export function createVerificationHistoryMock( + count = 10, +): Record[] { + faker.seed(42); + return Array.from({ length: count }, () => { + const executedAt = faker.date.recent({ days: 90 }); + const durationMinutes = faker.number.int({ min: 1, max: 120 }); + const endedAt = new Date( + executedAt.getTime() + durationMinutes * 60 * 1000, + ); + const status = faker.helpers.arrayElement(PROGRESS_STATUSES); + const normalCount = faker.number.int({ min: 0, max: 500 }); + const excludedCount = faker.number.int({ min: 0, max: 50 }); + const abnormalCount = faker.number.int({ min: 0, max: 30 }); + const item: VerificationHistoryItem = { + executedAt: formatDateTime(executedAt), + progressStatus: status, + endedAt: formatDateTime(endedAt), + verificationYear: String(executedAt.getFullYear()), + mu: faker.number.float({ min: 0, max: 1, fractionDigits: 3 }), + percentile: faker.number.float({ + min: 0, + max: 100, + fractionDigits: 2, + }), + a: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }), + c: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }), + target: faker.helpers.arrayElement(VERIFICATION_TARGETS), + normalCount, + excludedCount, + abnormalCount, + }; + return { ...item } as Record; + }); +} + +/** 검증 이력 기본 목데이터 (10건) */ +export const verificationHistoryMockData = createVerificationHistoryMock(10); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab7fd8d4a..2a3d2ab29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,10 +170,10 @@ importers: version: 5.28.5 eslint: specifier: ^9.0.0 - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) eslint-config-next: specifier: 15.4.0 - version: 15.4.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + version: 15.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) postcss: specifier: ^8.4.49 version: 8.5.6 @@ -897,6 +897,9 @@ importers: '@common/config': specifier: workspace:* version: link:../../packages/common/config + '@faker-js/faker': + specifier: ^9.9.0 + version: 9.9.0 '@fontsource-variable/noto-sans-kr': specifier: ^5.2.5 version: 5.2.9 @@ -1035,10 +1038,10 @@ importers: version: 5.28.5 eslint: specifier: ^9 - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) eslint-config-next: specifier: 15.1.7 - version: 15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 15.1.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) postcss: specifier: ^8 version: 8.5.6 @@ -5036,6 +5039,7 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -10316,6 +10320,26 @@ snapshots: optionalDependencies: source-map: 0.7.6 + eslint-config-next@15.1.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 15.1.7 + '@rushstack/eslint-patch': 1.15.0 + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@1.21.7)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-config-next@15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.1.7 @@ -10336,26 +10360,6 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-next@15.4.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): - dependencies: - '@next/eslint-plugin-next': 15.4.0 - '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.2(jiti@1.21.7) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@1.21.7)) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-next@15.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.4.0