From b6e11b4be41c08a0392f0685cb342756262ac9f7 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 12 Mar 2026 15:20:42 +0900 Subject: [PATCH] =?UTF-8?q?[st]=20feat:=20=EC=B0=A8=ED=8A=B8=20=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EB=93=9C=20=ED=8F=AC=EC=BB=A4=EC=8B=B1=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=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 | 362 ++++++++++++++---- .../detail/HbVerificationDetail.tsx | 4 + .../verification/detail/RegionalGrid.tsx | 4 + .../verification/detail/ScatterChart.tsx | 23 ++ apps/st/src/components/type/DataGrid.type.ts | 4 + 5 files changed, 313 insertions(+), 84 deletions(-) diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index a3f2be7c1..f914be8a8 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -1,24 +1,25 @@ -import { useRef, useEffect, useLayoutEffect } from "react"; -import { ValueType } from "realgrid"; +import { useEffect, useLayoutEffect, useRef } from "react"; -import RealGrid, { GridFitStyle, type DisplayOptions } from "realgrid"; -import { RealGridReact, RGDataColumn } from "realgrid-react"; -import type { - DataGridProps, - RealGridColumnInfo, - GridViewWithSetColumns, - FooterItemWithCallback, - ColumnLayoutItem, - CellStyleCallback, - CellTooltipCallback, -} from "../type/DataGrid.type"; -import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import JSZip from "jszip"; +import { ValueType } from "realgrid"; +import RealGrid, { type DisplayOptions, GridFitStyle } from "realgrid"; +import { RealGridReact, RGDataColumn } from "realgrid-react"; import * as XLSX from "xlsx"; import { useAlertStore } from "@/shared/lib"; - import { Button } from "@common/components"; + +import type { + CellStyleCallback, + CellTooltipCallback, + ColumnLayoutItem, + DataGridProps, + FooterItemWithCallback, + GridViewWithSetColumns, + RealGridColumnInfo, +} from "../type/DataGrid.type"; + +import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import { useTabStore } from "@/store/tab-store"; // RealGrid 타입 확장 @@ -321,19 +322,43 @@ const DataGrid = ({ cellTooltipCallback, onRowClick, statusInfo, + highlightedRowId, + idFieldName = "id", }: DataGridProps) => { const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); const fileInputRef = useRef(null); const sortDirRef = useRef>({}); - const originalRowsRef = useRef[]>>({}); + const originalRowsRef = useRef< + Record[]> + >({}); const rowDataRef = useRef[]>(rowData); - const cellStyleCallbackRef = useRef(cellStyleCallback); - const cellTooltipCallbackRef = useRef(cellTooltipCallback); + const cellStyleCallbackRef = useRef( + cellStyleCallback, + ); + const cellTooltipCallbackRef = useRef( + cellTooltipCallback, + ); rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; cellTooltipCallbackRef.current = cellTooltipCallback; + // highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택 + useEffect(() => { + if (!gridRef.current?.gridView || highlightedRowId == null) return; + + const gridView = gridRef.current.gridView; + const index = rowData.findIndex( + (row) => String(row[idFieldName]) === String(highlightedRowId), + ); + + if (index !== -1) { + // 해당 행으로 포커스 이동 및 스크롤 + gridView.setCurrent({ itemIndex: index }); + gridView.setFocus(); + } + }, [highlightedRowId, rowData, idFieldName]); + useLayoutEffect(() => { if (!cellStyleCallback || !gridRef.current?.gridView) return; gridRef.current.gridView.refresh(); @@ -532,10 +557,10 @@ const DataGrid = ({ ) { const cell = worksheet[ - XLSX.utils.encode_cell({ - r: rowIndex, - c: col, - }) + XLSX.utils.encode_cell({ + r: rowIndex, + c: col, + }) ]; if ( cell && @@ -611,7 +636,7 @@ const DataGrid = ({ const col = range.s.c + colIndex; const cell = worksheet[ - XLSX.utils.encode_cell({ r: row, c: col }) + XLSX.utils.encode_cell({ r: row, c: col }) ]; const headerName = headers[colIndex] || `Column${colIndex + 1}`; @@ -909,7 +934,7 @@ const DataGrid = ({ ) { ( column.footer[ - index + index ] as FooterItemWithCallback ).valueCallback = footerItem.valueCallback; @@ -962,11 +987,11 @@ const DataGrid = ({ getGridDataRef.current = () => { const gv = gridRef.current?.gridView as | { - getDataSource?: () => { - getRowCount(): number; - getValue(r: number, f: string): unknown; - }; - } + getDataSource?: () => { + getRowCount(): number; + getValue(r: number, f: string): unknown; + }; + } | undefined; if (!gv?.getDataSource) return []; const ds = gv.getDataSource(); @@ -1001,23 +1026,57 @@ const DataGrid = ({ if (!gridView || (!onRowClick && !cellTooltipCallback)) return; gridView.onCellClicked = (grid, clickData) => { - if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return; + if ( + clickData.cellType !== "data" || + clickData.itemIndex === undefined + ) + return; try { const row = rowDataRef.current[clickData.itemIndex]; - const cellIndex = (grid as { getCurrent?: () => { itemIndex: number; column?: string; fieldName?: string } }).getCurrent?.(); + const cellIndex = ( + grid as { + getCurrent?: () => { + itemIndex: number; + column?: string; + fieldName?: string; + }; + } + ).getCurrent?.(); // cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex) if (cellTooltipCallbackRef.current && row && cellIndex) { - const fieldName = cellIndex.fieldName ?? cellIndex.column ?? ""; - const msg = cellTooltipCallbackRef.current({ row: row as Record, fieldName }); + const fieldName = + cellIndex.fieldName ?? cellIndex.column ?? ""; + const msg = cellTooltipCallbackRef.current({ + row: row as Record, + fieldName, + }); if (msg) { - const colName = columns.find((c) => c.fieldName === fieldName || c.name === cellIndex.column)?.name ?? cellIndex.column ?? fieldName; - (gridView as { showMessage?: (opts: { message: string; hideDuration?: number }, idx: { itemIndex: number; column: string }) => void }).showMessage?.( + const colName = + columns.find( + (c) => + c.fieldName === fieldName || + c.name === cellIndex.column, + )?.name ?? + cellIndex.column ?? + fieldName; + ( + gridView as { + showMessage?: ( + opts: { + message: string; + hideDuration?: number; + }, + idx: { itemIndex: number; column: string }, + ) => void; + } + ).showMessage?.( { message: msg, hideDuration: 5000 }, - { itemIndex: cellIndex.itemIndex, column: colName } + { itemIndex: cellIndex.itemIndex, column: colName }, ); } } - if (onRowClick) onRowClick((row ?? {}) as Record); + if (onRowClick) + onRowClick((row ?? {}) as Record); } catch (error) { console.error("행 데이터 가져오기 오류:", error); } @@ -1038,26 +1097,42 @@ const DataGrid = ({ onChange={handleFileChange} />
- {statusInfo && + {statusInfo && (
- 진행상황: { - statusInfo.laeSbmsnSttsCd === null || statusInfo.laeSbmsnSttsCd === '0' ? - '미작성' - : - statusInfo.laeSbmsnSttsCd === '1' ? '작성중' : statusInfo.laeSbmsnSttsCd === '2' ? '제출' - : - statusInfo.laeSbmsnSttsCd === '3' ? '승인' : '반려' - } + + 진행상황:{" "} + {statusInfo.laeSbmsnSttsCd === null || + statusInfo.laeSbmsnSttsCd === "0" + ? "미작성" + : statusInfo.laeSbmsnSttsCd === "1" + ? "작성중" + : statusInfo.laeSbmsnSttsCd === "2" + ? "제출" + : statusInfo.laeSbmsnSttsCd === "3" + ? "승인" + : "반려"} + + + 제출일:{" "} + {statusInfo.laeSbmsnYmd + ? statusInfo.laeSbmsnYmd.length === 8 && + /^\d{8}$/.test(statusInfo.laeSbmsnYmd) + ? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}` + : statusInfo.laeSbmsnYmd + : "미제출"} + + + 담당자:{" "} + {statusInfo.laePicNm ? statusInfo.laePicNm : "-"} + + + 연락처:{" "} + {statusInfo.laePicTelNo + ? statusInfo.laePicTelNo + : "-"} - 제출일: {statusInfo.laeSbmsnYmd - ? (statusInfo.laeSbmsnYmd.length === 8 && /^\d{8}$/.test(statusInfo.laeSbmsnYmd) - ? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}` - : statusInfo.laeSbmsnYmd) - : '미제출'} - 담당자: {statusInfo.laePicNm ? statusInfo.laePicNm : '-'} - 연락처: {statusInfo.laePicTelNo ? statusInfo.laePicTelNo : '-'}
- } + )} {isExcelImport && (
@@ -88,6 +91,7 @@ const HbVerificationDetail: React.FC = ({ anlsSn={anlsSn} vrfcTrgtSn={selectedVrfcTrgtSn} selectedSido={selectedSido} + onPointClick={setSelectedRowId} />
diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 72f344291..7b2b42a07 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -28,6 +28,7 @@ interface RegionalGridProps { vrfcTrgtNm: string; selectedSido: string | null; onSidoSelect: (sido: string | null) => void; + selectedRowId?: number | null; } interface AppliedFilter { @@ -43,6 +44,7 @@ const RegionalGrid: React.FC = ({ vrfcTrgtNm, selectedSido, onSidoSelect, + selectedRowId, }) => { const [selectedGugun, setSelectedGugun] = useState(""); const [selectedStatus, setSelectedStatus] = useState(""); @@ -369,6 +371,8 @@ const RegionalGrid: React.FC = ({ setRowData={setGridRowData} isExcelExport={false} isExcelImport={false} + highlightedRowId={selectedRowId} + idFieldName="rsltSn" /> )} diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx index 1b2fb5127..46aab674a 100644 --- a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -41,6 +41,7 @@ interface ScatterChartProps { anlsSn: number; vrfcTrgtSn: string | null; selectedSido: string | null; + onPointClick?: (rsltSn: number | null) => void; } interface LinePoint { @@ -56,6 +57,7 @@ const ScatterChart: React.FC = ({ anlsSn, vrfcTrgtSn, selectedSido, + onPointClick, }) => { const { data: regionCodes } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], @@ -157,6 +159,27 @@ const ScatterChart: React.FC = ({ const options: ChartOptions<"scatter"> = { responsive: true, maintainAspectRatio: false, + onClick: (event, elements) => { + if (elements.length > 0) { + const element = elements[0]; + if (!element) return; + const datasetIndex = element.datasetIndex; + const index = element.index; + const dataset = data.datasets[datasetIndex]; + + if (!dataset) return; + + // 상한선/하한선 클릭 시 무시 + if (dataset.showLine) return; + + const pt = dataset.data[index] as DataPoint; + if (pt?.item?.rsltSn) { + onPointClick?.(pt.item.rsltSn); + } + } else { + onPointClick?.(null); + } + }, plugins: { zoom: { pan: { diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 1423c7940..18523b0c4 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -244,6 +244,10 @@ export type DataGridProps = { }; onRowClick?: (rowData: Record) => void; // 행 클릭 이벤트 statusInfo?: SewerageSupplyRateStatusInfo | null; + /** 강조 및 스크롤할 행의 ID */ + highlightedRowId?: string | number | null; + /** ID로 사용할 필드명 (기본값: 'id') */ + idFieldName?: string; }; export type TabDataGridProps = {