Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/review-skeleton-sein

This commit is contained in:
2026-02-05 15:26:13 +09:00
16 changed files with 821 additions and 108 deletions
+4 -4
View File
@@ -155,16 +155,16 @@ function updateHeaderSortIndicator(
function createOnSortingHandler(
columns: { fieldName?: string; name: string; mergeCells?: boolean; sortdisable?: boolean; valueType?: ValueType }[],
setRowData: React.Dispatch<React.SetStateAction<Record<string, string | number>[]>>,
sortDirRef: React.MutableRefObject<Record<string, "asc" | "desc" | "none">>,
originalRowsRef: React.MutableRefObject<Record<string, Record<string, string | number>[]>>
sortDirRef: { current: Record<string, "asc" | "desc" | "none"> },
originalRowsRef: { current: Record<string, Record<string, string | number>[]> }
) {
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[]) {
+13 -1
View File
@@ -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 = {}) => {
<Label className="text-label">
시군구
</Label>
<Select value={values.gugun} onValueChange={(value) => setValues({ ...values, gugun: value })}>
<Select value={values.gugun} onValueChange={handleSggChange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="전체"></SelectValue>
</SelectTrigger>
@@ -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<any[]>([]);
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<any[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleReset = () => {
@@ -29,10 +45,10 @@ const [rowData, setRowData] = useState<any[]>([]);
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<any[]>([]);
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<any[]>([]);
</div>
</div>
<DataGrid
columnInfo={SeweragePenetrationRateInfoList}
isExcelImport={false}
fixedColumns={SeweragePenetrationRateInfoList.fixedColumns}
rowStartNum={SeweragePenetrationRateInfoList.rowStartNum}
columnInfo={columnInfo}
isExcelImport={true}
fixedColumns={columnInfo.fixedColumns}
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
setRowData={setRowData}
/>
</>
);
@@ -1,11 +1,482 @@
"use client";
import { useState } from "react";
import { DataGrid } from "@rsone/components";
import SearchForm from "@/components/module/SearchForm";
import {
Button,
Input,
Label,
Select,
SelectTrigger,
SelectContent,
SelectItem,
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;
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 [verificationHistoryRowData, setVerificationHistoryRowData] =
useState<Record<string, string | number>[]>(
verificationHistoryMockData,
);
const [formValues, setFormValues] = useState<VerificationFormValues>({
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<VerificationFormValues, "checkboxOptions">,
value: string,
) => {
setFormValues((prev) => ({
...prev,
[key]: value,
}));
};
const handleSubmit = () => {
console.log("검증 실행:", formValues);
};
const handleReset = () => {
console.log("초기화");
};
return (
<>
<SearchForm />
</>
<div className="card card-border mt-4">
<div className="p-2">
{/* 통계 검증 섹션 */}
<div className="mb-6">
<h3 className="px-2 text-[1.5rem] font-semibold">
통계 검증
</h3>
<div className="flex gap-2">
{/* 왼쪽: 검증 변수 */}
<div className="card card-border flex-[0.5] rounded p-4">
<h4 className="mb-3 rounded bg-gray-100 p-2 text-[1.4rem] font-medium">
검증 변수
</h4>
<div className="space-y-3">
<div className="flex items-center gap-4">
<Label className="w-32 text-label">μ</Label>
<Input
type="number"
value={formValues.mu}
onChange={(e) =>
handleInputChange(
"mu",
e.target.value,
)
}
className="flex-1"
/>
</div>
<div className="flex items-center gap-4">
<Label className="w-32 text-label">
Percentile
</Label>
<Input
type="number"
value={formValues.percentile}
onChange={(e) =>
handleInputChange(
"percentile",
e.target.value,
)
}
className="flex-1"
/>
</div>
<div className="flex items-center gap-4">
<Label className="w-32 text-label">A</Label>
<Input
type="number"
value={formValues.a}
onChange={(e) =>
handleInputChange(
"a",
e.target.value,
)
}
className="flex-1"
/>
</div>
<div className="flex items-center gap-4">
<Label className="w-32 text-label">C</Label>
<Input
type="number"
value={formValues.c}
onChange={(e) =>
handleInputChange(
"c",
e.target.value,
)
}
className="flex-1"
/>
</div>
<div className="flex items-center gap-4">
<Label className="w-32 text-label">
검증 년도
</Label>
<Select
value={formValues.verificationYear}
onValueChange={(value) =>
handleInputChange(
"verificationYear",
value,
)
}
>
<SelectTrigger className="flex-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2025">
2025년
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 오른쪽: 검증 대상 */}
<div className="card card-border flex-1 rounded p-4">
<h4 className="mb-3 rounded bg-gray-100 p-2 text-[1.4rem] font-medium">
검증 대상
</h4>
<div className="space-y-4 p-2">
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.sidoDistribution
}
onCheckedChange={() =>
handleCheckboxChange(
"sidoDistribution",
)
}
/>
<Label className="cursor-pointer text-label">
하수도 보급률
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.countyDistribution
}
onCheckedChange={() =>
handleCheckboxChange(
"countyDistribution",
)
}
/>
<Label className="cursor-pointer text-label">
하수관로
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.countyDistributionUserManagement
}
onCheckedChange={() =>
handleCheckboxChange(
"countyDistributionUserManagement",
)
}
/>
<Label className="cursor-pointer text-label">
하수관로 유지관리
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.lowerDistribution
}
onCheckedChange={() =>
handleCheckboxChange(
"lowerDistribution",
)
}
/>
<Label className="cursor-pointer text-label">
하수저류시설 및 빗물펌프장
</Label>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.sidoServiceAvailability
}
onCheckedChange={() =>
handleCheckboxChange(
"sidoServiceAvailability",
)
}
/>
<Label className="cursor-pointer text-label">
펌프장
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.individualService
}
onCheckedChange={() =>
handleCheckboxChange(
"individualService",
)
}
/>
<Label className="cursor-pointer text-label">
공공하수처리시설
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.sidoServiceAvailability
}
onCheckedChange={() =>
handleCheckboxChange(
"sidoServiceAvailability",
)
}
/>
<Label className="cursor-pointer text-label">
분뇨처리시설
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.individualService
}
onCheckedChange={() =>
handleCheckboxChange(
"individualService",
)
}
/>
<Label className="cursor-pointer text-label">
개인하수처리시설
</Label>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.lowerOrgStatistics
}
onCheckedChange={() =>
handleCheckboxChange(
"lowerOrgStatistics",
)
}
/>
<Label className="cursor-pointer text-label">
하수도재정
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.studentStatistics
}
onCheckedChange={() =>
handleCheckboxChange(
"studentStatistics",
)
}
/>
<Label className="cursor-pointer text-label">
물 재이용
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.lowerOrgStatistics
}
onCheckedChange={() =>
handleCheckboxChange(
"lowerOrgStatistics",
)
}
/>
<Label className="cursor-pointer text-label">
하수 및 분뇨찌꺼기(슬러지) 처리
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.studentStatistics
}
onCheckedChange={() =>
handleCheckboxChange(
"studentStatistics",
)
}
/>
<Label className="cursor-pointer text-label">
하수 및 분뇨관련산업
</Label>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.lowerManagementEnterprise
}
onCheckedChange={() =>
handleCheckboxChange(
"lowerManagementEnterprise",
)
}
/>
<Label className="cursor-pointer text-label">
하수도 서비스 수준
</Label>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 버튼 */}
<div className="mt-6 flex justify-center gap-4 pt-4">
<Button
color="green"
className="h-[3.6rem] w-[120px]"
onClick={handleSubmit}
>
검증 실행
</Button>
<Button
color="gray"
className="h-[3.6rem] w-[120px]"
onClick={handleReset}
>
초기화
</Button>
</div>
{/* 검증 이력 섹션 */}
<div className="mt-8 border-t pt-6">
<h3 className="mb-4 px-2 text-[1.5rem] font-semibold">
검증 이력
</h3>
<DataGrid
columnInfo={verificationHistoryColumnInfo}
rowData={verificationHistoryRowData}
setRowData={setVerificationHistoryRowData}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
/>
</div>
</div>
</div>
);
};
export default ManagementVerificationRule;
export default ManagementVerificationRule;
@@ -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 },
@@ -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<string>();
@@ -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),
},
];
@@ -1,10 +1,15 @@
import type { SewerageSupplyRateItem } from "@/entities/statistics/model/statistics-input-interface";
export function toGridRows(items: SewerageSupplyRateItem[]): Record<string, string | number>[] {
/**
* 객체 배열을 그리드 row 형식으로 변환 (null/undefined → "").
* 어떤 엔티티 타입이든 공통 사용 가능.
*/
export function toGridRows<T extends object>(
items: T[]
): Record<string, string | number>[] {
return items.map((item) => {
const row: Record<string, string | number> = {};
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;
});