diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx
index c96f22973..6de651a1d 100644
--- a/apps/st/src/components/module/CompactDataGrid.tsx
+++ b/apps/st/src/components/module/CompactDataGrid.tsx
@@ -71,6 +71,38 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
};
}
+function escapeHtml(value: string): string {
+ return value
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function realGridHeaderWithTooltip(
+ header: T,
+): T {
+ const tip = header.tooltip?.trim();
+ if (!tip) return header;
+ return {
+ ...header,
+ tooltip: tip,
+ showTooltip: header.showTooltip ?? true,
+ template:
+ header.template ??
+ `${escapeHtml(header.text)}`,
+ };
+}
+
+function resolveColumnTooltip(column: {
+ tooltip?: string;
+ header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
+}): string | undefined {
+ const tip = column.tooltip?.trim() || column.header.tooltip?.trim();
+ return tip && tip.length > 0 ? tip : undefined;
+}
+
const CompactDataGrid = ({
columnInfo,
fixedColumns,
@@ -88,8 +120,10 @@ const CompactDataGrid = ({
}: DataGridProps) => {
const { columns, columnLayout } = columnInfo;
const gridRef = useRef(null);
+ const columnsRef = useRef(columns);
const rowDataRef = useRef[]>(rowData);
const cellStyleCallbackRef = useRef(cellStyleCallback);
+ columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
@@ -155,6 +189,66 @@ const CompactDataGrid = ({
});
gridView.header.height = 60;
+ if (columns.some((c) => Boolean(resolveColumnTooltip(c)))) {
+ (gridView.header as { showTooltip?: boolean }).showTooltip = true;
+ }
+ columns.forEach((column) => {
+ (
+ gridView as {
+ setColumnProperty?: (
+ col: string,
+ key: string,
+ value: unknown,
+ ) => void;
+ }
+ ).setColumnProperty?.(
+ column.name,
+ "header",
+ realGridHeaderWithTooltip({
+ ...column.header,
+ tooltip: resolveColumnTooltip(column),
+ }),
+ );
+ });
+ (
+ gridView as {
+ onShowHeaderTooltip?: (
+ grid: unknown,
+ column: {
+ name?: string;
+ fieldName?: string;
+ header?: { tooltip?: string };
+ },
+ value: string,
+ layout?: {
+ name?: string;
+ fieldName?: string;
+ column?: string | { name?: string; fieldName?: string };
+ },
+ isEllipsis?: boolean,
+ ) => string | undefined;
+ }
+ ).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
+ const currentColumns = columnsRef.current;
+ const directTip = headerColumn?.header?.tooltip?.trim();
+ if (directTip) return directTip;
+
+ const layoutColumn =
+ typeof layout?.column === "string"
+ ? layout.column
+ : layout?.column?.name ?? layout?.column?.fieldName;
+
+ const col = currentColumns.find(
+ (c) =>
+ c.name === headerColumn?.name ||
+ c.fieldName === headerColumn?.fieldName ||
+ c.name === layout?.name ||
+ c.fieldName === layout?.fieldName ||
+ c.name === layoutColumn ||
+ c.fieldName === layoutColumn,
+ );
+ return col ? resolveColumnTooltip(col) : undefined;
+ };
// 헤더 체크박스 표시 여부 설정
gridView.setOptions({
@@ -235,7 +329,10 @@ const CompactDataGrid = ({
const colInfo: RealGridColumnInfo = {
name: col.name,
fieldName: col.fieldName,
- header: col.header,
+ header: realGridHeaderWithTooltip({
+ ...col.header,
+ tooltip: resolveColumnTooltip(col),
+ }),
editable: col.editable,
valueType: col.valueType,
dataFormat: col.dataFormat,
@@ -427,7 +524,10 @@ const CompactDataGrid = ({
fieldName={column.fieldName}
valueType={column.valueType ? column.valueType : ValueType.TEXT}
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })}
- header={column.header}
+ header={realGridHeaderWithTooltip({
+ ...column.header,
+ tooltip: resolveColumnTooltip(column),
+ })}
editable={column.editable}
{...(column.styleName != null
? { styleName: column.styleName }
diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx
index acadc607a..d513e90a9 100644
--- a/apps/st/src/components/module/DataGrid.tsx
+++ b/apps/st/src/components/module/DataGrid.tsx
@@ -81,6 +81,42 @@ function createNumberComparer(
};
}
+function escapeHtml(value: string): string {
+ return value
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+/**
+ * RealGrid 헤더 툴팁: `grid.header.showTooltip`이 비어 있으면 `column.header.showTooltip === true`인 컬럼만 표시됨.
+ * `tooltip`이 있는 헤더는 info-circle 아이콘이 함께 보이도록 template도 자동 구성한다.
+ */
+function realGridHeaderWithTooltip(
+ header: T,
+): T {
+ const tip = header.tooltip?.trim();
+ if (!tip) return header;
+ return {
+ ...header,
+ tooltip: tip,
+ showTooltip: header.showTooltip ?? true,
+ template:
+ header.template ??
+ `${escapeHtml(header.text)}`,
+ };
+}
+
+function resolveColumnTooltip(column: {
+ tooltip?: string;
+ header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
+}): string | undefined {
+ const tip = column.tooltip?.trim() || column.header.tooltip?.trim();
+ return tip && tip.length > 0 ? tip : undefined;
+}
+
/** 정렬 필드 값을 숫자로 파싱 (쉼표 제거) */
function toNum(v: unknown): number {
if (v == null) return NaN;
@@ -354,6 +390,7 @@ const DataGrid = ({
const { columns, columnLayout } = columnInfo;
const gridRef = useRef(null);
const fileInputRef = useRef(null);
+ const columnsRef = useRef(columns);
const sortDirRef = useRef>({});
const originalRowsRef = useRef<
Record[]>
@@ -372,6 +409,7 @@ const DataGrid = ({
const onItemCheckedRef = useRef(onItemChecked);
const onItemAllCheckedRef =
useRef(onItemAllChecked);
+ columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
cellTooltipCallbackRef.current = cellTooltipCallback;
@@ -813,6 +851,66 @@ const DataGrid = ({
GridFitStyle.EVEN;
gridView.displayOptions.rowHeight = 30;
gridView.header.height = 60;
+ if (columns.some((c) => Boolean(resolveColumnTooltip(c)))) {
+ (gridView.header as { showTooltip?: boolean }).showTooltip = true;
+ }
+ columns.forEach((column) => {
+ (
+ gridView as {
+ setColumnProperty?: (
+ col: string,
+ key: string,
+ value: unknown,
+ ) => void;
+ }
+ ).setColumnProperty?.(
+ column.name,
+ "header",
+ realGridHeaderWithTooltip({
+ ...column.header,
+ tooltip: resolveColumnTooltip(column),
+ }),
+ );
+ });
+ (
+ gridView as {
+ onShowHeaderTooltip?: (
+ grid: unknown,
+ column: {
+ name?: string;
+ fieldName?: string;
+ header?: { tooltip?: string };
+ },
+ value: string,
+ layout?: {
+ name?: string;
+ fieldName?: string;
+ column?: string | { name?: string; fieldName?: string };
+ },
+ isEllipsis?: boolean,
+ ) => string | undefined;
+ }
+ ).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
+ const currentColumns = columnsRef.current;
+ const directTip = headerColumn?.header?.tooltip?.trim();
+ if (directTip) return directTip;
+
+ const layoutColumn =
+ typeof layout?.column === "string"
+ ? layout.column
+ : layout?.column?.name ?? layout?.column?.fieldName;
+
+ const col = currentColumns.find(
+ (c) =>
+ c.name === headerColumn?.name ||
+ c.fieldName === headerColumn?.fieldName ||
+ c.name === layout?.name ||
+ c.fieldName === layout?.fieldName ||
+ c.name === layoutColumn ||
+ c.fieldName === layoutColumn,
+ );
+ return col ? resolveColumnTooltip(col) : undefined;
+ };
// 헤더 체크박스 표시 여부 설정
gridView.setOptions({
@@ -917,7 +1015,7 @@ const DataGrid = ({
const colInfo: RealGridColumnInfo = {
name: col.name,
fieldName: col.fieldName,
- header: col.header,
+ header: realGridHeaderWithTooltip(col.header),
editable: col.editable,
valueType: col.valueType,
dataFormat: col.dataFormat,
@@ -1334,6 +1432,48 @@ const DataGrid = ({
setTimeout(() => {
const gv = gridRef.current?.gridView;
if (!gv?.columnByName) return;
+ // 공식 문서: 헤더 툴팁 = header.showTooltip + onShowHeaderTooltip
+ (
+ gv as {
+ onShowHeaderTooltip?: (
+ grid: unknown,
+ column: {
+ name?: string;
+ fieldName?: string;
+ header?: {
+ tooltip?: string;
+ };
+ },
+ value: string,
+ layout?: {
+ name?: string;
+ fieldName?: string;
+ column?: string | { name?: string; fieldName?: string };
+ },
+ isEllipsis?: boolean,
+ ) => string | undefined;
+ }
+ ).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
+ const currentColumns = columnsRef.current;
+ const directTip = headerColumn?.header?.tooltip?.trim();
+ if (directTip) return directTip;
+
+ const layoutColumn =
+ typeof layout?.column === "string"
+ ? layout.column
+ : layout?.column?.name ?? layout?.column?.fieldName;
+
+ const col = currentColumns.find(
+ (c) =>
+ c.name === headerColumn?.name ||
+ c.fieldName === headerColumn?.fieldName ||
+ c.name === layout?.name ||
+ c.fieldName === layout?.fieldName ||
+ c.name === layoutColumn ||
+ c.fieldName === layoutColumn,
+ );
+ return col ? resolveColumnTooltip(col) : undefined;
+ };
// 공식 문서: 데이터 영역 툴팁 = renderer.showTooltip + onShowTooltip (displayOptions는 Ellipsis용이라 사용 안 함)
if (cellTooltipCallbackRef.current) {
(
@@ -1387,6 +1527,22 @@ const DataGrid = ({
renderer?: { showTooltip?: boolean };
} | null;
if (gridCol) {
+ (
+ gv as {
+ setColumnProperty?: (
+ col: string,
+ key: string,
+ value: unknown,
+ ) => void;
+ }
+ ).setColumnProperty?.(
+ column.name,
+ "header",
+ realGridHeaderWithTooltip({
+ ...column.header,
+ tooltip: resolveColumnTooltip(column),
+ }),
+ );
if (cellTooltipCallbackRef.current) {
(
gv as {
@@ -1456,7 +1612,10 @@ const DataGrid = ({
? column.dataFormat
: "#,##0.000",
})}
- header={column.header}
+ header={realGridHeaderWithTooltip({
+ ...column.header,
+ tooltip: resolveColumnTooltip(column),
+ })}
editable={column.editable}
{...(column.width != null && {
width: column.width,
diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx
index 1026fd82d..bc4e9d9e9 100644
--- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx
+++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx
@@ -13,6 +13,10 @@ import {
DialogHeader,
DialogTitle,
LoadingWithAnimation,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
@@ -20,6 +24,13 @@ import { VerificationResultHistoryPagination } from "@/components/module/Verific
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
type VerificationResultGridRow = Record;
+type VerificationHistoryTabKey = "NV" | "IMR" | "HR";
+
+const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
+ { value: "NV", label: "일반검증 내역" },
+ { value: "IMR", label: "I-MR 검증내역" },
+ { value: "HR", label: "HR 검증내역" },
+];
function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined {
if (raw === undefined || raw === null) return undefined;
@@ -76,30 +87,40 @@ export function VerificationResultHistoryDialog({
title = "검증 결과 내역",
emptyMessage = "표시할 검증 결과가 없습니다.",
}: VerificationResultHistoryDialogProps) {
+ const [activeTab, setActiveTab] = useState("NV");
const [normalEntries, setNormalEntries] = useState([]);
const [imrEntries, setImrEntries] = useState([]);
+ const [hrEntries, setHrEntries] = useState([]);
const [normalPageIndex, setNormalPageIndex] = useState(0);
const [imrPageIndex, setImrPageIndex] = useState(0);
+ const [hrPageIndex, setHrPageIndex] = useState(0);
const [normalPageCount, setNormalPageCount] = useState(1);
const [imrPageCount, setImrPageCount] = useState(1);
+ const [hrPageCount, setHrPageCount] = useState(1);
const [normalLoading, setNormalLoading] = useState(false);
const [imrLoading, setImrLoading] = useState(false);
+ const [hrLoading, setHrLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const resetState = useCallback(() => {
+ setActiveTab("NV");
setNormalEntries([]);
setImrEntries([]);
+ setHrEntries([]);
setNormalPageIndex(0);
setImrPageIndex(0);
+ setHrPageIndex(0);
setNormalPageCount(1);
setImrPageCount(1);
+ setHrPageCount(1);
setNormalLoading(false);
setImrLoading(false);
+ setHrLoading(false);
setErrorMessage(null);
}, []);
const fetchVerificationResultPage = useCallback(
- async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => {
+ async (vrfcMthdCd: VerificationHistoryTabKey, pageIndex: number) => {
if (!queryContext) return null;
const param: VerificationResultListRequest = {
sewerBzmnCd: queryContext.sewerBzmnCd,
@@ -126,26 +147,34 @@ export function VerificationResultHistoryDialog({
setErrorMessage(null);
setNormalLoading(true);
setImrLoading(true);
+ setHrLoading(true);
setNormalPageIndex(0);
setImrPageIndex(0);
+ setHrPageIndex(0);
try {
- const [nvRes, imrRes] = await Promise.all([
+ const [nvRes, imrRes, hrRes] = await Promise.all([
fetchVerificationResultPage("NV", 0),
fetchVerificationResultPage("IMR", 0),
+ fetchVerificationResultPage("HR", 0),
]);
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
setImrEntries(imrRes?.data?.verifyResultList ?? []);
+ setHrEntries(hrRes?.data?.verifyResultList ?? []);
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
+ setHrPageCount(hrRes?.data?.pageInfo?.totalPages ?? 1);
} catch {
setNormalEntries([]);
setImrEntries([]);
+ setHrEntries([]);
setNormalPageCount(1);
setImrPageCount(1);
+ setHrPageCount(1);
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
} finally {
setNormalLoading(false);
setImrLoading(false);
+ setHrLoading(false);
}
}, [fetchVerificationResultPage, queryContext]);
@@ -187,6 +216,25 @@ export function VerificationResultHistoryDialog({
[fetchVerificationResultPage, imrPageIndex, queryContext],
);
+ const handleHrPageChange = useCallback(
+ async (pageIndex: number) => {
+ if (pageIndex === hrPageIndex || !queryContext) return;
+ setErrorMessage(null);
+ setHrLoading(true);
+ try {
+ const response = await fetchVerificationResultPage("HR", pageIndex);
+ setHrEntries(response?.data?.verifyResultList ?? []);
+ setHrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
+ setHrPageIndex(pageIndex);
+ } catch {
+ setErrorMessage("HR 검증내역 조회 중 오류가 발생했습니다.");
+ } finally {
+ setHrLoading(false);
+ }
+ },
+ [fetchVerificationResultPage, hrPageIndex, queryContext],
+ );
+
useEffect(() => {
if (!open) {
resetState();
@@ -212,6 +260,7 @@ export function VerificationResultHistoryDialog({
const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
+ const hrRows = useMemo(() => mapRows(hrEntries), [hrEntries]);
const noopSetRows = ((_: SetStateAction[]>) => {}) as Dispatch<
SetStateAction[]>
@@ -269,14 +318,24 @@ export function VerificationResultHistoryDialog({
{errorMessage}
)}
-
- 일반검증 내역
- {renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
-
-
- I-MR 검증내역
- {renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
-
+ setActiveTab(value as VerificationHistoryTabKey)} className="w-full" variant="default">
+
+ {verificationHistoryTabs.map((tab) => (
+
+ {tab.label}
+
+ ))}
+
+
+ {renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
+
+
+ {renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
+
+
+ {renderGrid(hrRows, hrPageIndex, hrPageCount, handleHrPageChange, hrLoading)}
+
+
diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
index f0334fee8..da4e74843 100644
--- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
+++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useConfirm } from '@frontend-opensource/use-react-hooks';
@@ -131,6 +131,10 @@ type MainFacilityMappingSaveDraft = {
initialPrxyCtrtCn: string;
};
+const PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS = Array.from(
+ new Set(PublicSewageFactInpColumnList.map((tab) => tab.tabKey).filter(Boolean)),
+);
+
// 하수도 통계 관리 > 06. 공공하수처리시설
const PublicSewageFacilityInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
@@ -149,6 +153,8 @@ const PublicSewageFacilityInp = () => {
const [lastSearchedValues, setLastSearchedValues] = useState(null);
const [tabStatusInfoMap, setTabStatusInfoMap] = useState