diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index e3961956f..f602400c7 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -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 { diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index b79effe65..7d7f925ee 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -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(null); const fileInputRef = useRef(null); const sortDirRef = useRef>({}); - const originalRowsRef = useRef< - Record[]> - >({}); + const originalRowsRef = useRef[]>>({}); + const rowDataRef = useRef[]>(rowData); + const cellStyleCallbackRef = useRef(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 = ({ )}
- { - ( - grid as { - onSorting?: ( - g: unknown, - f: unknown[], - d: unknown[], - ) => boolean; + { + 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[]): void; getJsonRow?(r: number): Record } + | 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; + 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 ( + ); - const dataProvider = gridRef.current - ?.dataProvider as unknown as - | { - setFields?(f: unknown[]): void; - setRows?( - r: Record[], - ): void; - getJsonRow?( - r: number, - ): Record; - } - | undefined; - if (dataProvider?.setFields) { - dataProvider.setFields( - buildFieldsFromColumns(columns), - ); - } - if (dataProvider?.setRows && rowData?.length) { - dataProvider.setRows(rowData); - } - }} - > - {columns.map((column) => ( - - ))} + })}
diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 9333b92cb..2c1b15f5e 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -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 & { 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'; + } + }} /> diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 23d34020f..59948afb8 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -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; +}; + +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[]> >; + /** 셀 단위로 styleName을 동적으로 결정 (예: 검증 오류 셀) */ + cellStyleCallback?: CellStyleCallback; /** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */ getGridDataRef?: { current: (() => Record[]) | null; diff --git a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts index 159726976..41a9c5700 100644 --- a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts +++ b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts @@ -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' }, // 총인구 { diff --git a/apps/st/src/entities/statistics/api/statistics-input-service.ts b/apps/st/src/entities/statistics/api/statistics-input-service.ts index bf758e0a8..f8fdc0850 100644 --- a/apps/st/src/entities/statistics/api/statistics-input-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-input-service.ts @@ -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 { + public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { const response = await axios.post ( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`, params diff --git a/apps/st/src/entities/statistics/model/statistics-input-interface.ts b/apps/st/src/entities/statistics/model/statistics-input-interface.ts index 4f36f49b3..3fd0ba0c8 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-input-interface.ts @@ -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; checkInputPeriod(params: string): Promise; - saveSewerageSupplyRate(params: SewerageSupplyRateSaveItem[]): Promise; + saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; } \ No newline at end of file diff --git a/packages/rsone/components/grid/css/DataGrid.css b/packages/rsone/components/grid/css/DataGrid.css index 4469b8490..6aa237533 100644 --- a/packages/rsone/components/grid/css/DataGrid.css +++ b/packages/rsone/components/grid/css/DataGrid.css @@ -110,4 +110,9 @@ /* 소계, 합계, 율(%) cell */ .rg-cell-total { background-color: #e3eaf2 !important; +} + +.rg-cell-verify-error { + background-color: #ffe6e6 !important; + color: #c00 !important; } \ No newline at end of file