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-19 15:23:26 +09:00
8 changed files with 223 additions and 183 deletions
+10
View File
@@ -10,6 +10,16 @@
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
@apply !bg-white;
}
/* 셀 클래스가 있으면 위 기본색보다 우선 (같은 specificity + 나중 규칙) */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
@apply !bg-[#f0f0f0] !text-[#666];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error {
@apply !bg-[#ffe6e6] !text-[#c00];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-total {
@apply !bg-[#e3eaf2];
}
[data-theme="black"] {
.rg-grid .rg-header .rg-table .rg-header-group-cell {
+78 -87
View File
@@ -1,4 +1,4 @@
import { useRef, useEffect } from "react";
import { useRef, useEffect, useLayoutEffect } from "react";
import { ValueType } from "realgrid";
import RealGrid, { GridFitStyle, type DisplayOptions } from "realgrid";
@@ -9,6 +9,7 @@ import type {
GridViewWithSetColumns,
FooterItemWithCallback,
ColumnLayoutItem,
CellStyleCallback,
} from "../type/DataGrid.type";
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
import JSZip from "jszip";
@@ -413,15 +414,24 @@ const DataGrid = ({
rowData,
setRowData,
getGridDataRef,
cellStyleCallback,
onRowClick,
}: DataGridProps) => {
const { columns, columnLayout } = columnInfo;
const gridRef = useRef<RealGridReact>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const sortDirRef = useRef<Record<string, "asc" | "desc" | "none">>({});
const originalRowsRef = useRef<
Record<string, Record<string, string | number>[]>
>({});
const originalRowsRef = useRef<Record<string, Record<string, string | number>[]>>({});
const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
useLayoutEffect(() => {
if (!cellStyleCallback || !gridRef.current?.gridView) return;
gridRef.current.gridView.refresh();
}, [cellStyleCallback, rowData]);
const { setMessage: alert } = useAlertStore((state) => state);
const activeTabId = useTabStore((state) => state.activeTabId);
const menuList = useTabStore((state) => state.menuList);
@@ -1138,91 +1148,72 @@ const DataGrid = ({
)}
</div>
<div className="h-[65vh]">
<RealGridReact
ref={gridRef}
autoGenerateField={true}
layout={columnLayout}
rows={rowData}
onInitialized={(grid) => {
(
grid as {
onSorting?: (
g: unknown,
f: unknown[],
d: unknown[],
) => boolean;
<RealGridReact
ref={gridRef}
autoGenerateField={true}
layout={columnLayout}
rows={rowData}
onInitialized={(grid) => {
if (!grid) return;
(grid as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef);
const dataProvider = gridRef.current?.dataProvider as unknown as
| { setFields?(f: unknown[]): void; setRows?(r: Record<string, string | number>[]): void; getJsonRow?(r: number): Record<string, unknown> }
| undefined;
if (dataProvider?.setFields) {
dataProvider.setFields(buildFieldsFromColumns(columns));
}
).onSorting = createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
if (dataProvider?.setRows && rowData?.length) {
dataProvider.setRows(rowData);
}
const gridView = gridRef.current?.gridView ?? grid;
if (!gridView?.columnByName) return;
setTimeout(() => {
const gv = gridRef.current?.gridView;
if (!gv?.columnByName) return;
columns.forEach((column) => {
const baseStyleName = column.styleName;
const gridCol = gv.columnByName(column.name) as { styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string } | null;
if (gridCol) {
gridCol.styleCallback = (_g, dataCell) => {
const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
const fieldName = dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName;
const data = rowDataRef.current;
if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? '';
const row = data[itemIndex] as Record<string, string | number>;
const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row });
if (!extra) return baseStyleName ?? '';
return baseStyleName ? `${extra} ${baseStyleName}` : extra;
};
}
});
gv.refresh();
}, 0);
}}
>
{columns.map((column) => {
const baseStyleName = column.styleName;
return (
<RGDataColumn
key={column.name}
name={column.name}
fieldName={column.fieldName}
valueType={column.valueType ? column.valueType : ValueType.TEXT}
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })}
header={column.header}
editable={column.editable}
{...(baseStyleName != null && { styleName: baseStyleName })}
{...(column.visible === false && { visible: false })}
footer={column.footer && !Array.isArray(column.footer) ? {
text: column.footer.text,
expression: column.footer.expression,
numberFormat: column.footer.numberFormat,
} : undefined}
mergeRule={column.mergeCells ? { criteria: 'value' } : undefined}
sortable={!column.sortdisable}
/>
);
const dataProvider = gridRef.current
?.dataProvider as unknown as
| {
setFields?(f: unknown[]): void;
setRows?(
r: Record<string, string | number>[],
): void;
getJsonRow?(
r: number,
): Record<string, unknown>;
}
| undefined;
if (dataProvider?.setFields) {
dataProvider.setFields(
buildFieldsFromColumns(columns),
);
}
if (dataProvider?.setRows && rowData?.length) {
dataProvider.setRows(rowData);
}
}}
>
{columns.map((column) => (
<RGDataColumn
key={column.name}
name={column.name}
fieldName={column.fieldName}
valueType={
column.valueType
? column.valueType
: ValueType.TEXT
}
{...(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,
expression: column.footer.expression,
numberFormat:
column.footer.numberFormat,
}
: undefined
}
mergeRule={
column.mergeCells
? { criteria: "value" }
: undefined
}
sortable={!column.sortdisable}
/>
))}
})}
</RealGridReact>
</div>
</div>
@@ -17,6 +17,8 @@ import type {
SewerageSupplyRateItem,
SewerageSupplyRateRequestParams,
SewerageSupplyRateSaveItem,
SewerageSupplyRateVerificationResult,
SewerageSupplyRateSaveRequest
} from "@/entities/statistics/model/statistics-input-interface";
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
@@ -91,7 +93,13 @@ const SeweragePenetrationRateInp = () => {
speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr,
piplSupprt: commonInfo.piplSupprt,
}));
setRowData(toGridRows(convertedData));
setRowData(
convertedData.map((d) => {
const row = toGridRows([d])[0] as Record<string, string | number> & { verificationResult?: SewerageSupplyRateVerificationResult };
if (d.verificationResult) row.verificationResult = d.verificationResult;
return row;
})
);
setFooterFromApi(
commonInfo
@@ -193,13 +201,9 @@ const SeweragePenetrationRateInp = () => {
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
};
// SaveItem 타입에 정의된 필드만 추출
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
bsnmCd: searchValues?.bsnmCd ?? '',
sido: d.sido as string,
gugun: (d.gugun as string) || null,
yr: searchValues?.year ?? '',
adstCd: d.adstCd as string,
adstDong: d.adstDong as string,
bstrm: d.bstrm as string,
@@ -226,10 +230,17 @@ const SeweragePenetrationRateInp = () => {
pblswrgPopltnSuprt: formatNumber(d.pblswrgPopltnSuprt, 'pblswrgPopltnSuprt'),
adtrtPopltnSuprt: formatNumber(d.adtrtPopltnSuprt, 'adtrtPopltnSuprt'),
swrgInstlrt: formatNumber(d.swrgInstlrt, 'swrgInstlrt'),
userId: userInfo.userName,
}));
const response = await statisticsInputAPi.saveSewerageSupplyRate(convertedData);
const requestData: SewerageSupplyRateSaveRequest = {
bsnmCd: searchValues?.bsnmCd ?? '',
sido: searchValues?.sido ?? '',
yr: searchValues?.year ?? '',
userId: userInfo.userName,
saveList: convertedData,
}
const response = await statisticsInputAPi.saveSewerageSupplyRate(requestData);
console.log(convertedData)
console.log(response);
} else {
@@ -273,6 +284,14 @@ const SeweragePenetrationRateInp = () => {
rowData={rowData}
setRowData={setRowData}
getGridDataRef={getGridDataRef}
cellStyleCallback={({ row, fieldName }) => {
const verification = (row as any).verificationResult as SewerageSupplyRateVerificationResult | undefined;
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
return 'rg-cell-verify-error';
}
}}
/>
</div>
<ConfirmDialog isOpen={false} />
+82 -71
View File
@@ -13,30 +13,30 @@ export type RealGridColumnInfo = {
styleName?: string;
mergeRule?: { criteria: "value" };
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 */
@@ -105,21 +105,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;
@@ -138,31 +138,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 = {
@@ -174,16 +174,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,6 +202,15 @@ export type ColumnInfoList = {
right?: ColumnInfoList; // SplitDataGrid용 오른쪽 그리드 정보
};
// 개별 셀 스타일을 동적으로 결정하기 위한 콜백 (예: 검증 오류 셀 강조)
export type CellStyleCallbackParams = {
rowIndex: number;
fieldName: string;
row: Record<string, string | number>;
};
export type CellStyleCallback = (params: CellStyleCallbackParams) => string | undefined;
export type DataGridProps = {
columnInfo: ColumnInfo;
rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주)
@@ -214,6 +223,8 @@ export type DataGridProps = {
setRowData: React.Dispatch<
React.SetStateAction<Record<string, string | number>[]>
>;
/** 셀 단위로 styleName을 동적으로 결정 (예: 검증 오류 셀) */
cellStyleCallback?: CellStyleCallback;
/** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */
getGridDataRef?: {
current: (() => Record<string, string | number>[]) | null;
@@ -7,29 +7,29 @@ import { ValueType } from "realgrid";
export const SeweragePenetrationRateInpColumn = {
columns: [
// 행정구역
{ name: 'sido', fieldName: 'sido', header: { text: '시도' }, footer: { text: '합계' }, editable: false },
{ name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, editable: false },
{ name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, editable: false },
{ name: 'sido', fieldName: 'sido', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' },
// 수계 및 권역
{ name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false },
{ name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'bstrm', fieldName: 'bstrm', header: { text: '지류' } },
// 수계영향권별분류
{ name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false },
{ name: 'lgbsn', fieldName: 'lgbsn', header: { text: '중권역' }, editable: false },
{ name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false },
{ name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'lgbsn', fieldName: 'lgbsn', header: { text: '중권역' }, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false, styleName: 'rg-cell-readonly' },
// 특별대책지역
{ name: 'speclCntrplnAr', fieldName: 'speclCntrplnAr', header: { text: '특별대책지역' } },
// 행정참고자료인구
{ name: 'totalPopl', fieldName: 'totalPopl', header: { text: '총인구' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'poplSm', fieldName: 'poplSm', header: { text: '내국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'frgnrPoplSm', fieldName: 'frgnrPoplSm', header: { text: '외국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'totalPopl', fieldName: 'totalPopl', header: { text: '총인구' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
{ name: 'poplSm', fieldName: 'poplSm', header: { text: '내국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
{ name: 'frgnrPoplSm', fieldName: 'frgnrPoplSm', header: { text: '외국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
// 행정참고자료면적
{ name: 'area', fieldName: 'area', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
{ name: 'area', fieldName: 'area', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' },
// 총인구
{
@@ -6,7 +6,7 @@ import {
type SewerageSupplyRateRequestParams,
type SewerageSupplyRateApiResponse,
type CheckInputPeriodResponse,
type SewerageSupplyRateSaveItem,
type SewerageSupplyRateSaveRequest,
type SewerageSupplyRateSaveApiResponse,
} from "../model/statistics-input-interface";
@@ -30,7 +30,7 @@ export class StatisticsInputAPiService implements StatisticsInputService {
return response.data;
}
public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveItem[]): Promise<SewerageSupplyRateSaveApiResponse> {
public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse> {
const response = await axios.post<SewerageSupplyRateSaveApiResponse> (
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`,
params
@@ -138,10 +138,7 @@ export interface SewerageSupplyRateRequestParams {
/** 하수도 보급률 저장 API 요청 body (한 행) */
export interface SewerageSupplyRateSaveItem {
bsnmCd: string;
sido: string;
gugun: string | null;
yr: string;
adstCd: string;
adstDong: string;
bstrm: string;
@@ -168,7 +165,14 @@ export interface SewerageSupplyRateSaveItem {
pblswrgPopltnSuprt: number;
adtrtPopltnSuprt: number;
swrgInstlrt: number;
}
export interface SewerageSupplyRateSaveRequest {
yr: string;
sido: string;
bsnmCd: string;
userId: string;
saveList: SewerageSupplyRateSaveItem[];
}
export interface SewerageSupplyRateSaveData {
@@ -190,5 +194,5 @@ export interface CheckInputPeriodResponse {
export interface StatisticsInputService {
getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise<SewerageSupplyRateApiResponse>;
checkInputPeriod(params: string): Promise<CheckInputPeriodResponse>;
saveSewerageSupplyRate(params: SewerageSupplyRateSaveItem[]): Promise<SewerageSupplyRateSaveApiResponse>;
saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse>;
}
@@ -110,4 +110,9 @@
/* 소계, 합계, 율(%) cell */
.rg-cell-total {
background-color: #e3eaf2 !important;
}
.rg-cell-verify-error {
background-color: #ffe6e6 !important;
color: #c00 !important;
}