Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev

This commit is contained in:
2026-02-05 10:02:39 +09:00
17 changed files with 316 additions and 93 deletions
@@ -178,6 +178,7 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false,
editable: col.editable,
valueType: col.valueType,
dataFormat: col.dataFormat,
...(col.styleName != null && { styleName: col.styleName }),
...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }),
};
@@ -270,6 +271,11 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false,
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })}
header={column.header}
editable={column.editable}
{...(column.styleName != null
? { styleName: column.styleName }
: column.editable === false
? { styleName: 'rg-cell-readonly' as const }
: {})}
{...(column.visible === false && { visible: false })}
footer={column.footer && !Array.isArray(column.footer) ? {
text: column.footer.text,
@@ -591,6 +591,7 @@ const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport
editable: col.editable,
valueType: col.valueType,
dataFormat: col.dataFormat,
...(col.styleName != null && { styleName: col.styleName }),
...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }),
};
@@ -706,6 +707,11 @@ const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })}
header={column.header}
editable={column.editable}
{...(column.styleName != null
? { styleName: column.styleName }
: column.editable === false
? { styleName: 'rg-cell-readonly' as const }
: {})}
{...(column.visible === false && { visible: false })}
footer={column.footer && !Array.isArray(column.footer) ? {
text: column.footer.text,
+37 -23
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import useSWR from 'swr';
import {
@@ -18,9 +18,9 @@ from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { type SearchFormProps, type SearchFormValues, defaultSearchFormProps } from "../type/SearchForm.type";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import type { ApiResponse, Sido, Sgg } from "@/entities/statistics/model/statistics-interface";
import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface";
const getSidoListFetcher = async (): Promise<ApiResponse<Sido[]>> => {
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
};
@@ -31,22 +31,11 @@ const getSggListFetcher = async (key: [string, string]): Promise<ApiResponse<Sgg
const SearchForm = (props: SearchFormProps = {}) => {
const { setMessage: alert } = useAlertStore((state) => state);
const mergedProps = { ...defaultSearchFormProps, ...props };
const currentYear = new Date().getFullYear();
const defaultYear = currentYear - 1;
const startYear = currentYear - 10;
const yearOptions = Array.from({ length: currentYear - startYear + 1 }, (_, i) => currentYear - i);
const [values, setValues] = useState<SearchFormValues>({
sido: props.defaultValues?.sido || "",
gugun: props.defaultValues?.gugun || "",
year: props.defaultValues?.year || String(defaultYear),
order: props.defaultValues?.order || "",
fctName: props.defaultValues?.fctName || "",
});
const { data: sidoList } = useSWR<ApiResponse<Sido[]>>(
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
"sido-list",
getSidoListFetcher,
{
@@ -68,6 +57,15 @@ const SearchForm = (props: SearchFormProps = {}) => {
}
);
const [values, setValues] = useState<SearchFormValues>({
sido: props.defaultValues?.sido || '',
gugun: props.defaultValues?.gugun || '',
year: props.defaultValues?.year || defaultYear.toString(),
order: props.defaultValues?.order || '',
fctName: props.defaultValues?.fctName || '',
bsnmCd: props.defaultValues?.bsnmCd || '',
});
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null,
getSggListFetcher,
@@ -98,16 +96,32 @@ const SearchForm = (props: SearchFormProps = {}) => {
const handleReset = () => {
const resetValues: SearchFormValues = {
sido: "",
gugun: "",
sido: '',
gugun: '',
year: String(defaultYear),
order: "",
fctName: "",
order: '',
fctName: '',
bsnmCd: '',
};
setValues(resetValues);
props.onReset?.();
};
const handleSidoChange = (value: string) => {
const selectedBsnm = sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || '';
setValues((prev) => ({
...prev,
sido: value,
gugun: '',
bsnmCd: selectedBsnm,
}));
};
useEffect(() => {
props.onValuesChange?.(values);
}, [values]);
return (
<div className="card card-border">
<form onSubmit={handleSubmit}>
@@ -117,13 +131,13 @@ const SearchForm = (props: SearchFormProps = {}) => {
<Label className="text-label">
시도
</Label>
<Select value={values.sido} onValueChange={(value) => setValues((prev) => ({ ...prev, sido: value, gugun: "" }))}>
<Select value={values.sido} onValueChange={handleSidoChange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="선택"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">선택</SelectItem>
{sidoList?.data?.map((sido) => (
{sidoList?.data?.sidoList?.map((sido) => (
<SelectItem key={sido.adstDstrCd} value={sido.sido}>
{sido.sido}
</SelectItem>
@@ -162,7 +176,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
<SelectValue placeholder="선택"></SelectValue>
</SelectTrigger>
<SelectContent>
{yearOptions.map((year) => (
{sidoList?.data?.yearList.map((year) => (
<SelectItem key={year} value={String(year)}>
{year}년
</SelectItem>
@@ -793,6 +793,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
editable: col.editable,
valueType: col.valueType,
dataFormat: col.dataFormat,
...(col.styleName != null && { styleName: col.styleName }),
...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }),
};
@@ -940,6 +941,11 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.00' })}
header={column.header}
editable={column.editable}
{...(column.styleName != null
? { styleName: column.styleName }
: column.editable === false
? { styleName: 'rg-cell-readonly' as const }
: {})}
{...(column.visible === false && { visible: false })}
footer={column.footer && !Array.isArray(column.footer) ? {
text: (column.footer as { text?: string }).text,
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import SearchForm from "@/components/module/SearchForm";
// import { DataGrid } from "@rsone/components";
@@ -12,18 +12,25 @@ import { LoadingWithAnimation } from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import type { SewerageSupplyRateItem } from "@/entities/statistics/model/statistics-input-interface";
import type {
SewerageSupplyRateItem,
SewerageSupplyRateRequestParams,
} from "@/entities/statistics/model/statistics-input-interface";
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
// API 응답 항목을 DataGrid row 타입으로 변환 (null/undefined → 빈 문자열)
function toGridRows(items: SewerageSupplyRateItem[]): 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;
}
return row;
});
function getSewerageSupplyRateParams(
values: SearchFormValues,
overrides?: { yr?: string; demoYr?: string; mode?: string }
): SewerageSupplyRateRequestParams {
return {
sido: values.sido,
gugun: values.gugun === 'ALL' ? '' : values.gugun,
bsnmCd: values.bsnmCd,
yr: overrides?.yr ?? values.year,
demoYr: overrides?.demoYr ?? values.year,
...(overrides?.mode && { mode: overrides.mode }),
};
}
// 하수도 통계 관리 > 01. 하수도 보급률
@@ -31,38 +38,42 @@ const SeweragePenetrationRateInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [isEnterable, setIsEnterable] = useState<boolean>(true);
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
// 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요.
const columnInfo = useMemo(() => {
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
return {
columnLayout,
columns: isEnterable
? columns
: columns.map((col) => ({ ...col, editable: false as const })),
};
}, [isEnterable]);
const handleReset = () => {
setRowData([]);
setLastSearchedYear(null);
};
const handleSearch = async (values: SearchFormValues) => {
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams) => {
try {
if (values.sido !== '' && values.sido !== 'ALL') {
setIsLoading(true);
setIsLoading(true);
const response = await statisticsInputAPi.getSewerageSupplyRateList(params);
if (response.status === 'success' && response.data) {
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d) => ({
...d,
speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr,
}));
const response = await statisticsInputAPi.getSewerageSupplyRateList({
sido: values.sido,
gugun: values.gugun === 'ALL' ? '' : values.gugun,
bsnmCd: '4139000000',
yr: values.year,
demoYr: values.year,
});
if (response.status === 'success' && response.data) {
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d) => ({
...d,
speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr,
}));
setRowData(toGridRows(convertedData));
} else {
alert(response.message);
}
setRowData(toGridRows(convertedData));
} else {
alert('시도를 선택해주세요.');
alert(response.message);
}
} catch (error) {
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
setRowData([]);
} finally {
@@ -70,12 +81,45 @@ const SeweragePenetrationRateInp = () => {
}
};
const SubmitExceptionFunc = () => {
// 예외 제출 로직
const handleSearch = async (values: SearchFormValues) => {
if (values.sido === '' || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
}
try {
const response = await statisticsInputAPi.checkInputPeriod(values.year || '');
if (response.status === 'success') {
setIsEnterable(response.data);
setLastSearchedYear(values.year || null);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values));
} else {
alert(response.message);
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
}
};
const getLastYearDataFunc = () => {
// 지난연도 데이터 가져오기
const SubmitExceptionFunc = () => {
// 예외 제출 로직
};
const getLastYearDataFunc = async (values: SearchFormValues) => {
if (values.sido === '' || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
}
// 실제 조회된 연도가 없으면 실행 불가
if (!lastSearchedYear) {
alert('먼저 데이터를 조회해주세요.');
return;
}
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }));
};
const submitFunc = () => {
@@ -88,22 +132,29 @@ const SeweragePenetrationRateInp = () => {
return (
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
<LastYearDataButton getLastYearDataFunc={getLastYearDataFunc} />
<SubmitButton submitFunc={submitFunc} />
<SaveButton saveFunc={saveFunc} />
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
{rowData.length > 0 &&
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
{isEnterable && <SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />}
{isEnterable && <LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />}
{isEnterable && <SubmitButton submitFunc={submitFunc} />}
{
// 최고 관리자일땐 저장 가능하게
<SaveButton saveFunc={saveFunc} />
}
</div>
</div>
</div>
}
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>}
<DataGrid
columnInfo={SeweragePenetrationRateInpColumn}
key={isEnterable ? 'enterable' : 'readonly'}
columnInfo={columnInfo}
isExcelImport={(rowData.length > 0) && isEnterable}
rowStartNum={3}
fixedColumns={1}
rowData={rowData}
@@ -10,6 +10,7 @@ export type RealGridColumnInfo = {
editable?: boolean;
valueType?: import("realgrid").ValueType;
dataFormat?: string;
styleName?: string;
mergeRule?: { criteria: 'value' };
footer?: {
text?: string;
@@ -84,6 +85,8 @@ export type ColumnConfig = {
editable?: boolean; // 셀 편집 가능 여부
/** 그리드에서 컬럼 숨김 (수식용 필드 등) */
visible?: boolean;
/** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */
styleName?: string;
mergeCells?: boolean;
sortdisable?: boolean;
sortPreserveGroupOrder?: SortPreserveGroupOrder;
@@ -4,6 +4,7 @@ export type SearchFormValues = {
year?: string;
order?: string;
fctName?: string;
bsnmCd?: string;
};
export type SearchFormProps = {
@@ -15,10 +16,11 @@ export type SearchFormProps = {
userMenual?: boolean;
onSubmit?: (values: SearchFormValues) => void;
onReset?: () => void;
onValuesChange?: (values: SearchFormValues) => void;
defaultValues?: SearchFormValues;
};
export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' | 'onReset' | 'defaultValues'>> = {
export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' | 'onReset' | 'onValuesChange' | 'defaultValues'>> = {
sido: true,
gugun: true,
year: true,
@@ -46,7 +46,8 @@ export const SeweragePenetrationRateInpColumn = {
editable: false,
valueType: ValueType.NUMBER,
dataFormat: '#,##0',
computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], type: 'sum' as const }
computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], type: 'sum' as const },
styleName: 'rg-cell-total',
},
// 하수처리구역 내 > 공공하수처리인구
@@ -58,7 +59,8 @@ export const SeweragePenetrationRateInpColumn = {
editable: false,
valueType: ValueType.NUMBER,
dataFormat: '#,##0',
computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'], type: 'sum' as const }
computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'], type: 'sum' as const },
styleName: 'rg-cell-total',
},
{ name: 'pblswrgPrmTrt', fieldName: 'pblswrgPrmTrt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'pblswrgScdTrt', fieldName: 'pblswrgScdTrt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
@@ -73,7 +75,8 @@ export const SeweragePenetrationRateInpColumn = {
editable: false,
valueType: ValueType.NUMBER,
dataFormat: '#,##0',
computed: { fields: ['wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], type: 'sum' as const }
computed: { fields: ['wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], type: 'sum' as const },
styleName: 'rg-cell-total',
},
{ name: 'wstrPrmTrt', fieldName: 'wstrPrmTrt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'wstrScdTrt', fieldName: 'wstrScdTrt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
@@ -94,7 +97,8 @@ export const SeweragePenetrationRateInpColumn = {
editable: false,
valueType: ValueType.NUMBER,
dataFormat: '#,##0',
computed: { fields: ['townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], type: 'sum' as const }
computed: { fields: ['townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], type: 'sum' as const },
styleName: 'rg-cell-total',
},
{ name: 'townUnprocPopltn', fieldName: 'townUnprocPopltn', header: { text: '미처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'townSwttPopltn', fieldName: 'townSwttPopltn', header: { text: '오수처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
@@ -114,7 +118,8 @@ export const SeweragePenetrationRateInpColumn = {
numeratorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'],
denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'],
type: 'pblswrgPopltnSuprtRate' as const
}
},
styleName: 'rg-cell-total',
},
// 고도처리 인구보급률 (고도처리 인구 / 전체 인구 * 100)
@@ -130,7 +135,8 @@ export const SeweragePenetrationRateInpColumn = {
numeratorFields: ['pblswrgTertTrt', 'wstrTertTrt'],
denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'],
type: 'adtrtPopltnSuprtRate' as const
}
},
styleName: 'rg-cell-total',
},
// 하수도 설치율
@@ -147,7 +153,8 @@ export const SeweragePenetrationRateInpColumn = {
denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'],
thirdFields: ['piplSupprt'],
type: 'swrgInstlrtRate' as const
}
},
styleName: 'rg-cell-total',
},
// 비고
@@ -1,5 +1,5 @@
import { ValueType } from "realgrid";
import { footerIntArrayByDivision, footerDecimal3ArrayByDivision } from "@/components/util/grid/footerSummary";
import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary";
import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type";
// 하수도 통계 관리 01. 하수도 보급률
@@ -147,7 +147,7 @@ export const SeweragePenetrationRateInfoList = {
{ name: 'specl_cntrpln_ar', fieldName: '특별대책지역', header: { text: '특별대책지역' } },
{ name: 'total_popl', fieldName: '총인구', header: { text: '총인구' }, footer: footerIntArrayByDivision, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'ar_sm', fieldName: '총면적(㎢)', header: { text: '총면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
{ name: '계', fieldName: '계', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: '계', fieldName: '계', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-total' },
//공공하수처리인구
{ name: '합계', fieldName: '합계', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: '1차처리', fieldName: '1차처리', header: { text: '1차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
@@ -166,9 +166,9 @@ export const SeweragePenetrationRateInfoList = {
{ name: 'town_swtt_popltn', fieldName: '오수처리인구', header: { text: '오수처리인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'town_wrrtn_popltn', fieldName: '정화조인구', header: { text: '정화조인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'swge_proc_zone_outer_ar', fieldName: 'swge_proc_zone_outer_ar', header: { text: '면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
{ name: 'pblswrg_popltn_suprt', fieldName: '공공하수처리구역인구보급율(%)', header: { text: '공공하수처리구역인구보급율(%)' } },
{ name: 'adtrt_popltn_suprt', fieldName: '고도처리인구보급율(%)', header: { text: `고도처리인구보급율(%)` } },
{ name: 'swrg_instlrt', fieldName: '하수도설치율(%)', header: { text: '하수도설치율(%)' } },
{ name: 'pblswrg_popltn_suprt', fieldName: '공공하수처리구역인구보급율(%)', header: { text: '공공하수처리구역인구보급율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
{ name: 'adtrt_popltn_suprt', fieldName: '고도처리인구보급율(%)', header: { text: `고도처리인구보급율(%)` }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
{ name: 'swrg_instlrt', fieldName: '하수도설치율(%)', header: { text: '하수도설치율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
{ name: 'rmk', fieldName: '비고', header: { text: '비고' } },
],
columnLayout: [
@@ -107,6 +107,55 @@ export const getFooterSummaryByDivision = (
return sum;
};
// "구분" 필드 값에 따라 필터링하여 평균 계산하는 함수
// divisionIndex: 0 = 첫 번째 고유값, 1 = 두 번째 고유값
export const getFooterAvgSummaryByDivision = (
grid: GridView,
column: DataColumn,
divisionIndex: number,
divisionFieldName: string = '구분'
) => {
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
let sum = 0;
let count = 0;
// "구분" 필드의 고유값 찾기
const divisionValues = getDivisionValues(grid, divisionFieldName);
if (divisionValues.length <= divisionIndex) {
return 0; // 해당 인덱스의 고유값이 없으면 0 반환
}
const targetDivision = divisionValues[divisionIndex];
for (let i = 0; i < rowCount; i++) {
// "구분" 필드 값 확인
const divisionValue = dataSource.getValue(i, divisionFieldName);
if (String(divisionValue) !== targetDivision) {
continue; // 해당 "구분" 값이 아니면 스킵
}
const cellValue = dataSource.getValue(i, column.fieldName);
// 문자열인 경우 숫자로 변환
const numericValue = typeof cellValue === 'string'
? Number(String(cellValue).replace(/,/g, ''))
: Number(cellValue);
if (!isNaN(numericValue) && numericValue !== null && numericValue !== undefined) {
sum += numericValue;
count += 1;
}
}
if (count === 0) {
return 0;
}
const avg = sum / count;
return Math.floor(avg * 10) / 10;
};
// Footer 설정 객체 생성 함수
const createSumFooter = (format: string) => ({
expression: 'sum' as const,
@@ -156,6 +205,38 @@ const createSumFooterArrayByDivision = (format: string) => [
},
];
// "구분" 값에 따라 필터링하는 평균 Footer 배열 생성 함수 (2줄)
const createAvgFooterArrayByDivision = (format: string) => [
{
numberFormat: format,
valueCallback: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: unknown,
value: number | string | undefined
) => {
// footerIndex: 0 = 첫 번째 footer 행 (첫 번째 "구분" 값), 1 = 두 번째 footer 행 (두 번째 "구분" 값)
// footerIndex를 divisionIndex로 사용
return getFooterAvgSummaryByDivision(grid, column, footerIndex);
},
},
{
numberFormat: format,
valueCallback: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: unknown,
value: number | string | undefined
) => {
// footerIndex: 0 = 첫 번째 footer 행 (첫 번째 "구분" 값), 1 = 두 번째 footer 행 (두 번째 "구분" 값)
// footerIndex를 divisionIndex로 사용
return getFooterAvgSummaryByDivision(grid, column, footerIndex);
},
},
];
// 1. 정수형 : #,##0
export const footerInt = createSumFooter('#,##0');
@@ -171,8 +252,17 @@ export const footerDecimal3 = createSumFooter('#,##0.000');
// "구분" 값에 따라 필터링하는 Footer 배열 (정수형, 2줄)
export const footerIntArrayByDivision = createSumFooterArrayByDivision('#,##0');
// "구분" 값에 따라 필터링하는 Footer 배열 (소수점 1자리, 2줄)
export const footerDecimal1ArrayByDivision = createSumFooterArrayByDivision('#,##0.0');
// "구분" 값에 따라 필터링하는 Footer 배열 (소수점 2자리, 2줄)
export const footerDecimal2ArrayByDivision = createSumFooterArrayByDivision('#,##0.00');
// "구분" 값에 따라 필터링하는 Footer 배열 (소수점 3자리, 2줄)
export const footerDecimal3ArrayByDivision = createSumFooterArrayByDivision('#,##0.000');
// 률(%) 평균 값(기본 소수점 1자리)
export const footerAvgDecimal = createAvgFooter('#,##0.0');
export const footerAvgDecimal = createAvgFooter('#,##0.0');
// "구분" 값에 따라 필터링하는 평균 Footer 배열 (소수점 1자리, 2줄)
export const footerAvgDecimalArrayByDivision = createAvgFooterArrayByDivision('#,##0.0');
@@ -0,0 +1,11 @@
import type { SewerageSupplyRateItem } from "@/entities/statistics/model/statistics-input-interface";
export function toGridRows(items: SewerageSupplyRateItem[]): 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;
}
return row;
});
};
@@ -5,6 +5,7 @@ import {
type StatisticsInputService,
type SewerageSupplyRateRequestParams,
type SewerageSupplyRateApiResponse,
type CheckInputPeriodResponse,
} from "../model/statistics-input-interface";
export class StatisticsInputAPiService implements StatisticsInputService {
@@ -18,6 +19,14 @@ export class StatisticsInputAPiService implements StatisticsInputService {
return response.data;
}
public async checkInputPeriod(params: string): Promise<CheckInputPeriodResponse> {
const response = await axios.get<CheckInputPeriodResponse> (
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/check-input-period?year=${params}`,
);
return response.data;
}
}
export const statisticsInputAPi = new StatisticsInputAPiService();
@@ -3,16 +3,16 @@ import { ENDPOINT } from "@/shared/config";
import {
type StatisticsService,
type Sido,
type SearchCondition,
type ApiResponse,
type Sgg
} from "../model/statistics-interface";
export class StatisticsAPiService implements StatisticsService {
public async getSidoList(): Promise<ApiResponse<Sido[]>> {
const response = await axios.get<ApiResponse<Sido[]>>(
`${ENDPOINT.ST_SERVICE.COMMON}/sido`,
public async getSidoList(): Promise<ApiResponse<SearchCondition>> {
const response = await axios.get<ApiResponse<SearchCondition>>(
`${ENDPOINT.ST_SERVICE.COMMON}/search-condition`,
);
return response.data;
@@ -1,6 +1,6 @@
import type { ApiResponse } from "./statistics-interface";
// 하수도 보급률 API 응답의 상태 정보
// 하수도 보급률 데이터 항목
export interface SewerageSupplyRateStatusInfo {
locgStatus: string;
locgDt: string;
@@ -13,7 +13,6 @@ export interface SewerageSupplyRateStatusInfo {
returnOpinionSm: string | null;
}
// 하수도 보급률 데이터 항목
export interface SewerageSupplyRateItem {
sido: string;
gugun: string;
@@ -103,7 +102,7 @@ export interface SewerageSupplyRateResponseData {
export type SewerageSupplyRateApiResponse = ApiResponse<SewerageSupplyRateResponseData> & {
message: string;
};
}
export interface SewerageSupplyRateRequestParams {
sido?: string;
@@ -111,6 +110,14 @@ export interface SewerageSupplyRateRequestParams {
bsnmCd?: string;
yr?: string;
demoYr?: string;
mode?: string;
}
// 하수도 통계 입력 기간 조회
export interface CheckInputPeriodResponse {
status: string;
message: string;
data: boolean;
}
export interface StatisticsInputService {
@@ -1,11 +1,13 @@
export interface Sido {
adstDstrCd: string;
sido: string;
bsnmCd: string;
}
export interface Sgg {
adstDstrCd: string;
gugun: string;
bsnmCd: string;
}
export interface ApiResponse<T> {
+2 -1
View File
@@ -65,8 +65,9 @@ export const ENDPOINT = {
POST_EVALUATION: "/board-service/api/v1/pstEvl",
},
ST_SERVICE: {
COMMON: "/st-input-service/api/v1/common",
COMMON: "/st-management-service/api/v1/common",
INPUT: "/st-input-service/api/v1/input",
INPUTCOMMON: "/st-input-service/api/v1/common",
}
};
@@ -101,5 +101,13 @@
border-right: 1px solid #ccc !important;
}
/* 읽기 전용 cell */
.rg-cell-readonly {
background-color: #f0f0f0 !important;
color: #666 !important;
}
/* 소계, 합계, 율(%) cell */
.rg-cell-total {
background-color: #e3eaf2 !important;
}