diff --git a/apps/st/src/components/button/SumitButton.tsx b/apps/st/src/components/button/SumitButton.tsx index 0443889b7..0c0875ed2 100644 --- a/apps/st/src/components/button/SumitButton.tsx +++ b/apps/st/src/components/button/SumitButton.tsx @@ -2,15 +2,17 @@ import { Button } from "@common/components"; interface SubmitButtonProps { submitFunc: () => void; + /** 버튼 문구 (기본: 제출) */ + text?: string; } -const SubmitButton = ({ submitFunc }: SubmitButtonProps) => { +const SubmitButton = ({ submitFunc, text = "제출" }: SubmitButtonProps) => { return ( - ); }; -export default SubmitButton; \ No newline at end of file +export default SubmitButton; diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index f4a2020f0..b15945079 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -6,6 +6,17 @@ white-space: pre-line; } +/* RealGrid Excel export — applyDynamicStyles가 class background-color를 읽을 수 있도록 */ +.rg-cell-rowtype-biz-sum { + background-color: #ffe9cf !important; +} +.rg-cell-rowtype-summary { + background-color: #feee85 !important; +} +.rg-cell-rowtype-other { + background-color: #fff7bf !important; +} + @layer components { .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { @apply !bg-white; @@ -14,6 +25,15 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly { @apply !bg-[#f0f0f0] !text-[#666]; } + /* 고정 컬럼 — readonly와 동일한 화면 스타일 (엑셀 export 시 exportStyleName에서 제외) */ + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-fixed, + .rg-grid .rg-body .rg-data-cell.rg-cell-fixed { + @apply !bg-[#f0f0f0] !text-[#666]; + } + .rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-fixed, + .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-fixed { + @apply !bg-[#f0f0f0] !text-[#666]; + } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr { @apply !bg-white !text-[#333]; } @@ -23,6 +43,15 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr { @apply !bg-[#fff7bf] !text-[#b45309]; } + /* 총계 footer 검증 오류 (commonInfo.verificationResult) */ + .rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error, + .rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error { + @apply !bg-[#ffe6e6] !text-[#c00]; + } + .rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error-imr, + .rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error-imr { + @apply !bg-[#fff7bf] !text-[#b45309]; + } /* 검증결과 내역: 처리여부(prcsYn 0·4 미처리 등) */ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad { @apply !font-semibold !text-[#dc2626]; @@ -405,6 +434,13 @@ background-color: #ffe6e6 !important; color: #c00 !important; } +.rg-grid .rg-footer .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error, +.rg-grid .rg-footers .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error, +.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error, +.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error { + background-color: #ffe6e6 !important; + color: #c00 !important; +} .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, .rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, @@ -412,6 +448,13 @@ background-color: #fff7bf !important; color: #b45309 !important; } +.rg-grid .rg-footer .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, +.rg-grid .rg-footers .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, +.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, +.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr { + background-color: #fff7bf !important; + color: #b45309 !important; +} /* dark theme에서도 검증 오류 색을 유지하고 싶다면 동일 색 유지(가독성 우선) */ [data-theme="black"] .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error, diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 896659c00..b56d7e83d 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useRef, useState, type MutableRefObject } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState, type MutableRefObject } from "react"; import { createPortal } from "react-dom"; import JSZip from "jszip"; @@ -24,35 +24,100 @@ import type { } from "../type/DataGrid.type"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; +import { + formatVerificationTooltip, + getVerificationErrorsForField, + getVerificationStyleName, + type VerificationResultLikeMap, +} from "@/components/util/input/verification-result-display"; import { buildExcelExportFileBaseName, + buildFixedColumnNameSet, getExcelImportColumnFieldNames, getNonEditableColumnFieldNames, hasAdmdstCdColumn, isExcelImportAdmdstCdInvalid, mergeExcelImportWithExistingRows, prepareGridColumnsForExcelExport, + releaseFixedOptionsForExcelExport, + restoreFixedOptionsAfterExcelExport, restoreGridColumnsAfterExcelExport, + resolveColumnStyleName, shouldPrepareColumnsForExcelExport, + stripFixedStylesForExport, } from "@/components/util/gridExcelExport"; import { useTabStore } from "@/store/tab-store"; function mergeCellStyleCallbackResult( baseStyleName: string | undefined, extra: CellStyleCallbackResult | undefined, + options: { + mirrorExportStyle?: boolean; + stripFixedForExport?: boolean; + } = {}, ): CellStyleCallbackResult { + const { mirrorExportStyle = false, stripFixedForExport = false } = options; + + let styleName: string | undefined; + let extraObj: Extract | undefined; + if (!extra) { - return baseStyleName ? { styleName: baseStyleName } : {}; + styleName = baseStyleName || undefined; + } else if (typeof extra === "string") { + styleName = [baseStyleName, extra].filter(Boolean).join(" ") || undefined; + } else { + extraObj = extra; + styleName = + [baseStyleName, extra.styleName].filter(Boolean).join(" ") || + undefined; } - if (typeof extra === "string") { - const styleName = [baseStyleName, extra].filter(Boolean).join(" "); - return styleName ? { styleName } : {}; - } - const styleName = [baseStyleName, extra.styleName].filter(Boolean).join(" "); - return { - ...extra, + + if (!styleName && !extraObj) return {}; + + const result: CellStyleCallbackResult = { + ...extraObj, ...(styleName ? { styleName } : {}), }; + + if (typeof result !== "object") return result; + + if (mirrorExportStyle && styleName) { + result.exportStyleName = extraObj?.exportStyleName ?? styleName; + } else if (stripFixedForExport && styleName) { + const stripped = stripFixedStylesForExport(styleName); + if (stripped) result.exportStyleName = stripped; + else delete result.exportStyleName; + } + + return result; +} + +/** 컬럼 footer.styleName 또는 footerVerificationResult(fieldName) → footer styleName */ +function resolveFooterVerificationStyleName( + col: ColumnConfig, + footerVerificationResult: VerificationResultLikeMap | null | undefined, +): string | undefined { + if (col.footer && !Array.isArray(col.footer) && col.footer.styleName) { + return col.footer.styleName; + } + const errors = getVerificationErrorsForField( + footerVerificationResult, + col.fieldName, + col.name !== col.fieldName ? [col.name] : undefined, + ); + return getVerificationStyleName(errors); +} + +function resolveFooterVerificationTooltip( + col: ColumnConfig, + footerVerificationResult: VerificationResultLikeMap | null | undefined, +): string | undefined { + const errors = getVerificationErrorsForField( + footerVerificationResult, + col.fieldName, + col.name !== col.fieldName ? [col.name] : undefined, + ); + return formatVerificationTooltip(errors); } type GridColumnWithStyleCallback = { @@ -125,19 +190,26 @@ function applyCellStyleCallbacksToColumns( columns: ColumnConfig[], rowDataRef: MutableRefObject[]>, cellStyleCallbackRef: MutableRefObject, + excelExportApplyStylesRef: MutableRefObject, + fixedColumnNameSet: Set, ) { - if (!cellStyleCallbackRef.current || !gridView) return; + if (!gridView) return; const gv = gridView as { columnByName?: (name: string) => GridColumnWithStyleCallback | null | undefined; }; if (typeof gv.columnByName !== "function") return; + const hasUserCellStyleCallback = Boolean(cellStyleCallbackRef.current); + columns.forEach((column) => { - const baseStyleName = column.styleName; + const isFixedColumn = fixedColumnNameSet.has(column.name); + // 고정 컬럼은 styleCallback 필수(고정 pane에 column styleName만으로는 클래스 미반영) + if (!isFixedColumn && !hasUserCellStyleCallback) return; + + const baseStyleName = resolveColumnStyleName(column, fixedColumnNameSet); let gridCol: GridColumnWithStyleCallback | null | undefined; try { - // columnByName은 GridView 인스턴스 메서드 — this 바인딩을 위해 gv에서 직접 호출 gridCol = gv.columnByName!(column.name); } catch { return; @@ -150,17 +222,32 @@ function applyCellStyleCallbacksToColumns( const fieldName = dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName; const data = rowDataRef.current; + const styleOptions = { + mirrorExportStyle: excelExportApplyStylesRef.current, + stripFixedForExport: + isFixedColumn && !excelExportApplyStylesRef.current, + }; if (itemIndex < 0 || itemIndex >= data.length) { - return mergeCellStyleCallbackResult(baseStyleName, undefined); + return mergeCellStyleCallbackResult( + baseStyleName, + undefined, + styleOptions, + ); } const row = data[itemIndex] as Record; - const extra = cellStyleCallbackRef.current?.({ - rowIndex: itemIndex, - fieldName, - row, - grid: grid as CellStyleCallbackParams["grid"], - }); - return mergeCellStyleCallbackResult(baseStyleName, extra); + const extra = hasUserCellStyleCallback + ? cellStyleCallbackRef.current?.({ + rowIndex: itemIndex, + fieldName, + row, + grid: grid as CellStyleCallbackParams["grid"], + }) + : undefined; + return mergeCellStyleCallbackResult( + baseStyleName, + extra, + styleOptions, + ); }; }); } @@ -781,8 +868,9 @@ const DataGrid = ({ rowStartNum, excelExportFileSuffix, excelExportSearchContext, + excelExportApplyStyles = false, isExcelImport = true, - excelImportSkipNonEditable = true, + excelImportSkipNonEditable = false, excelImportSewerBzmnCd, isExcelExport = true, fixedColumns, @@ -793,6 +881,7 @@ const DataGrid = ({ getGridRef, cellStyleCallback, cellTooltipCallback, + footerVerificationResult = null, onCellEdited, onRowClick, onDataCellClick, @@ -809,6 +898,10 @@ const DataGrid = ({ fitStyle = GridFitStyle.EVEN, }: DataGridProps) => { const { columns, columnLayout } = columnInfo; + const fixedColumnNameSet = useMemo( + () => buildFixedColumnNameSet(columns, columnLayout, fixedColumns), + [columns, columnLayout, fixedColumns], + ); const gridRef = useRef(null); const gridHostRef = useRef(null); const fileInputRef = useRef(null); @@ -821,9 +914,13 @@ const DataGrid = ({ const cellStyleCallbackRef = useRef( cellStyleCallback, ); + const excelExportApplyStylesRef = useRef(excelExportApplyStyles); const cellTooltipCallbackRef = useRef( cellTooltipCallback, ); + const footerVerificationResultRef = useRef< + VerificationResultLikeMap | null | undefined + >(footerVerificationResult); const onCellEditedRef = useRef(onCellEdited); const onDataCellClickRef = useRef(onDataCellClick); const onScrollToBottomRef = useRef<(() => void) | undefined>( @@ -847,7 +944,9 @@ const DataGrid = ({ columnsRef.current = columns; rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; + excelExportApplyStylesRef.current = excelExportApplyStyles; cellTooltipCallbackRef.current = cellTooltipCallback; + footerVerificationResultRef.current = footerVerificationResult; onScrollToBottomRef.current = onScrollToBottom; onItemCheckedRef.current = onItemChecked; onItemAllCheckedRef.current = onItemAllChecked; @@ -855,6 +954,7 @@ const DataGrid = ({ onDataCellClickRef.current = onDataCellClick; const hasVerificationErrors = rowsHaveVerificationErrors(rowData); + // 총계 footer(commonInfo.verificationResult)는 행 우선보기 대상이 아님 const showVerifyPriorityToggle = hasVerificationErrors; // 조회 등으로 rowData가 교체되면 우선보기 해제 (자체 setRowData는 스킵) @@ -999,7 +1099,8 @@ useEffect(() => { }, [onCellDblClick]); useLayoutEffect(() => { const gv = gridRef.current?.gridView; - if (!gv || !cellStyleCallback) return; + if (!gv) return; + if (!cellStyleCallback && fixedColumnNameSet.size === 0) return; const apply = () => { applyCellStyleCallbacksToColumns( @@ -1007,6 +1108,8 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); (gv as { refresh?: () => void }).refresh?.(); }; @@ -1018,7 +1121,7 @@ useEffect(() => { return () => { window.clearTimeout(timerId); }; - }, [cellStyleCallback, columns, rowData]); + }, [cellStyleCallback, columns, rowData, excelExportApplyStyles, fixedColumnNameSet, fixedColumns]); const { setMessage: alert } = useAlertStore((state) => state); const activeTabId = useTabStore((state) => state.activeTabId); @@ -1116,6 +1219,10 @@ useEffect(() => { const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep ? prepareGridColumnsForExcelExport(gridView, columns) : undefined; + const savedFixedColCount = + !excelExportApplyStyles && fixedColumns + ? releaseFixedOptionsForExcelExport(gridView, fixedColumns) + : undefined; const exportBaseName = buildExcelExportFileBaseName( menuName, @@ -1129,7 +1236,14 @@ useEffect(() => { fileName: `${exportBaseName}.xlsx`, footer: "hidden", allColumns: !useExcelColumnVisibilityPrep, + ...(excelExportApplyStyles ? { applyDynamicStyles: true } : {}), done: () => { + if (savedFixedColCount !== undefined) { + restoreFixedOptionsAfterExcelExport( + gridView, + savedFixedColCount, + ); + } if (excelExportVisibilitySnapshot) { restoreGridColumnsAfterExcelExport( gridView, @@ -1147,7 +1261,7 @@ useEffect(() => { } }, 100); } - alert("내보내기가 완료되었습니다."); + // alert("내보내기가 완료되었습니다."); }, }); }; @@ -1338,10 +1452,14 @@ useEffect(() => { const preserveFieldNames = excelImportSkipNonEditable ? getNonEditableColumnFieldNames(columns, columnLayout) : []; - setRowData((prev) => - mergeExcelImportWithExistingRows(prev, dataRows, { - preserveFieldNames, - }), + // 우선보기 해제 직후 rowDataRef가 원본 순서 — functional prev는 아직 정렬 상태일 수 있음 + skipVerifyPriorityResetRef.current = true; + setRowData( + mergeExcelImportWithExistingRows( + rowDataRef.current, + dataRows, + { preserveFieldNames }, + ), ); } catch (error) { console.error("엑셀 파일 파싱 오류:", error); @@ -1471,8 +1589,17 @@ useEffect(() => { gv.setColumnProperty?.( column.name, "styleName", - column.styleName ?? "", + resolveColumnStyleName(column, fixedColumnNameSet), ); + if (fixedColumnNameSet.has(column.name) && !excelExportApplyStyles) { + gv.setColumnProperty?.( + column.name, + "exportStyleName", + stripFixedStylesForExport( + resolveColumnStyleName(column, fixedColumnNameSet), + ) || "", + ); + } applyColumnListEditorProperties(gv, column); }); if (columns.some((c) => isDropDownEditorColumn(c))) { @@ -1661,6 +1788,8 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); gridRef.current.gridView.refresh?.(); } catch (e) { @@ -1703,12 +1832,26 @@ useEffect(() => { if (col.footer) { if (Array.isArray(col.footer)) { - colInfo.footer = col.footer; + colInfo.footer = col.footer.map((item) => ({ + ...item, + styleName: + item.styleName ?? + resolveFooterVerificationStyleName( + col, + footerVerificationResultRef.current, + ) ?? + "", + })); } else { colInfo.footer = { text: col.footer.text, expression: col.footer.expression, numberFormat: col.footer.numberFormat, + styleName: + resolveFooterVerificationStyleName( + col, + footerVerificationResultRef.current, + ) ?? "", }; } } @@ -1736,6 +1879,8 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); // setColumns 호출 후 footer 배열의 valueCallback을 다시 설정 @@ -1789,6 +1934,8 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); liveGridView.refresh?.(); }, 50); @@ -1797,7 +1944,7 @@ useEffect(() => { } } - // footer가 단일 객체이고 valueCallback이 있는 경우 + // footer가 단일 객체이고 valueCallback/styleName이 있는 경우 columns.forEach((col) => { if (!col.footer || Array.isArray(col.footer)) return; @@ -1812,6 +1959,13 @@ useEffect(() => { } ).valueCallback = col.footer.valueCallback; } + const styleName = + resolveFooterVerificationStyleName( + col, + footerVerificationResultRef.current, + ) ?? ""; + (column.footer as { styleName?: string }).styleName = + styleName; } catch (e) { console.warn( `Error setting footer valueCallback for column ${col.name}:`, @@ -1865,14 +2019,53 @@ useEffect(() => { columns, columnLayout, fixedColumns, + fixedColumnNameSet, + excelExportApplyStyles, rowData, showHeaderCheckbox, getGridDataRef, headerHeight, fitStyle, enableSort, + footerVerificationResult, ]); + /** footerVerificationResult만 바뀐 경우에도 총계 스타일 즉시 재적용/초기화 */ + useEffect(() => { + const gridView = gridRef.current?.gridView as + | { + columnByName?: (name: string) => { + footer?: { styleName?: string } | Array<{ styleName?: string }>; + } | null; + refresh?: () => void; + } + | undefined; + if (!gridView?.columnByName) return; + + columns.forEach((col) => { + if (!col.footer) return; + try { + const column = gridView.columnByName?.(col.name); + if (!column?.footer) return; + const styleName = + resolveFooterVerificationStyleName( + col, + footerVerificationResult, + ) ?? ""; + if (Array.isArray(column.footer)) { + column.footer.forEach((item) => { + item.styleName = styleName; + }); + } else { + column.footer.styleName = styleName; + } + } catch { + /* ignore */ + } + }); + gridView.refresh?.(); + }, [footerVerificationResult, columns]); + // 행 클릭 이벤트 + 검증 오류 셀 클릭 시 showMessage (공식 문서 방식) useEffect(() => { const gridView = gridRef.current?.gridView; @@ -1960,9 +2153,15 @@ useEffect(() => { }; }, [onRowClick, onDataCellClick, cellTooltipCallback, columns]); - // 빈 셀은 RealGrid hover 툴팁이 안 뜸 → React 오버레이로만 보완 (값 있는 셀은 기존 onShowTooltip) + // RealGrid 네이티브 툴팁은 데이터 셀만 지원 → 빈 셀·총계 footer는 React 오버레이로 보완 useEffect(() => { - if (!cellTooltipCallback) { + const needsOverlayTooltip = + Boolean(cellTooltipCallback) || + Boolean( + footerVerificationResult && + Object.keys(footerVerificationResult).length > 0, + ); + if (!needsOverlayTooltip) { emptyCellHoverKeyRef.current = ""; setEmptyCellHoverTooltip(null); return; @@ -1976,6 +2175,28 @@ useEffect(() => { setEmptyCellHoverTooltip(null); }; + const isFooterCellType = (cellType: string | undefined) => { + if (!cellType) return false; + const normalized = cellType.toLowerCase(); + return ( + normalized === "footer" || + normalized === "summary" || + normalized === "groupfooter" + ); + }; + + const resolveColumnFromIndex = (index: { + column?: string; + fieldName?: string; + }) => + columnsRef.current.find( + (c) => + c.name === index.column || + c.fieldName === index.column || + c.name === index.fieldName || + c.fieldName === index.fieldName, + ); + const handleMouseMove = (ev: MouseEvent) => { if (emptyCellHoverRafRef.current != null) { cancelAnimationFrame(emptyCellHoverRafRef.current); @@ -2012,12 +2233,72 @@ useEffect(() => { ev.clientY - rect.top, ); } catch { + index = null; + } + + const elAtPoint = document.elementFromPoint( + ev.clientX, + ev.clientY, + ); + const footerDom = + elAtPoint instanceof Element + ? elAtPoint.closest( + ".rg-footer-cell, .rg-footers .rg-cell, .rg-footer .rg-cell", + ) + : null; + const isFooter = + Boolean(footerDom) || + isFooterCellType(index?.cellType) || + index?.itemIndex === -1; + + if (isFooter) { + if (!footerVerificationResultRef.current) { + clearEmptyCellHoverTooltip(); + return; + } + const col = index + ? resolveColumnFromIndex(index) + : undefined; + if (!col) { + clearEmptyCellHoverTooltip(); + return; + } + const fieldName = col.fieldName; + const msg = resolveFooterVerificationTooltip( + col, + footerVerificationResultRef.current, + ); + if (!msg) { + clearEmptyCellHoverTooltip(); + return; + } + const normalizedMessage = msg.replace(/\n[ \t]+/g, "\n"); + const key = `footer:${fieldName}`; + emptyCellHoverKeyRef.current = key; + setEmptyCellHoverTooltip({ + message: normalizedMessage, + x: ev.clientX + 12, + y: ev.clientY + 16, + key, + }); + return; + } + + if (!index) { + clearEmptyCellHoverTooltip(); + return; + } + + const col = resolveColumnFromIndex(index); + const fieldName = + col?.fieldName ?? index.fieldName ?? index.column ?? ""; + + if (!cellTooltipCallbackRef.current) { clearEmptyCellHoverTooltip(); return; } if ( - !index || (index.cellType != null && index.cellType !== "data") || index.itemIndex == null || index.itemIndex < 0 @@ -2032,15 +2313,6 @@ useEffect(() => { return; } - const col = columnsRef.current.find( - (c) => - c.name === index.column || - c.fieldName === index.column || - c.name === index.fieldName || - c.fieldName === index.fieldName, - ); - const fieldName = - col?.fieldName ?? index.fieldName ?? index.column ?? ""; if (!fieldName) { clearEmptyCellHoverTooltip(); return; @@ -2061,8 +2333,6 @@ useEffect(() => { return; } - // 값 있는 셀 RealGrid 툴팁과 같이: 본문 다음 줄에 (검증 기준값...)이 오도록 - // 템플릿 리터럴 들여쓰기 공백은 제거 const normalizedMessage = msg.replace(/\n[ \t]+/g, "\n"); const key = `${index.itemIndex}:${fieldName}`; @@ -2096,7 +2366,7 @@ useEffect(() => { } clearEmptyCellHoverTooltip(); }; - }, [cellTooltipCallback]); + }, [cellTooltipCallback, footerVerificationResult]); useEffect(() => { const gridView = gridRef.current?.gridView; @@ -2433,19 +2703,36 @@ useEffect(() => { index: { itemIndex: number; column: string; + cellType?: string; }, value: string, ) => string | undefined; } ).onShowTooltip = (_grid, index, value) => { - const row = - rowDataRef.current[index?.itemIndex]; - if (row == null) return value; const column = index.column; const col = columns.find( (c) => c.name === column, ); const fieldName = col?.fieldName ?? column; + // 총계 footer: cellType footer 또는 itemIndex === -1 + const isFooter = + index?.cellType === "footer" || + index?.itemIndex === -1; + if ( + isFooter && + col && + footerVerificationResultRef.current + ) { + const footerMsg = + resolveFooterVerificationTooltip( + col, + footerVerificationResultRef.current, + ); + if (footerMsg) return footerMsg; + } + const row = + rowDataRef.current[index?.itemIndex]; + if (row == null) return value; const custom = cellTooltipCallbackRef.current?.({ row: row as Record< @@ -2459,6 +2746,33 @@ useEffect(() => { ? custom : undefined; }; + } else if (footerVerificationResultRef.current) { + ( + gv as { + onShowTooltip?: ( + grid: unknown, + index: { + itemIndex: number; + column: string; + cellType?: string; + }, + value: string, + ) => string | undefined; + } + ).onShowTooltip = (_grid, index) => { + const isFooter = + index?.cellType === "footer" || + index?.itemIndex === -1; + if (!isFooter) return undefined; + const col = columns.find( + (c) => c.name === index.column, + ); + if (!col) return undefined; + return resolveFooterVerificationTooltip( + col, + footerVerificationResultRef.current, + ); + }; } columns.forEach((column) => { const gridCol = gv.columnByName( @@ -2487,7 +2801,7 @@ useEffect(() => { gv as GridViewWithSetColumnProperty, column, ); - if (cellTooltipCallbackRef.current) { + if (cellTooltipCallbackRef.current || footerVerificationResultRef.current) { ( gv as { setColumnProperty?: ( @@ -2509,13 +2823,18 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); gv.refresh(); }, 0); }} > {columns.map((column) => { - const baseStyleName = column.styleName; + const baseStyleName = resolveColumnStyleName( + column, + fixedColumnNameSet, + ); const numberFormat = column.dataFormat && column.dataFormat.trim() !== "" ? column.dataFormat @@ -2588,13 +2907,22 @@ useEffect(() => { footer={ column.footer && !Array.isArray(column.footer) - ? { - text: column.footer.text, - expression: - column.footer.expression, - numberFormat: - column.footer.numberFormat, - } + ? (() => { + const footerStyleName = + resolveFooterVerificationStyleName( + column, + footerVerificationResult, + ) ?? ""; + return { + text: column.footer.text, + expression: + column.footer.expression, + numberFormat: + column.footer + .numberFormat, + styleName: footerStyleName, + }; + })() : undefined } mergeRule={ @@ -2610,10 +2938,11 @@ useEffect(() => { {emptyCellHoverTooltip && createPortal(
diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 46c0b50d3..0b6ec3080 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -73,6 +73,21 @@ function buildSearchFormValues( }; } + if (props.defaultRegionAll) { + return { + sido: "ALL", + gugun: "ALL", + year: d?.year || defaultYear.toString(), + order: d?.order || "", + fctName: d?.fctName || "", + bsnmCd: "", + isDetail: false, + isSummary: d?.isSummary || "", + clsfCd: initialClsfCd, + capacityGbn: d?.capacityGbn || "ALL", + }; + } + const first = sidoList?.data?.sidoList?.[0]; const useFirstSido = (props.sido ?? defaultSearchFormProps.sido) && first; @@ -132,6 +147,7 @@ const SearchForm = (props: SearchFormProps = {}) => { // 최초 진입만 시도 API가 늦게 오는 경우 useEffect(() => { + if (props.defaultRegionAll) return; if (props.defaultValues?.sido) return; const first = sidoList?.data?.sidoList?.[0]; if (!first || !mergedProps.sido) return; @@ -145,7 +161,7 @@ const SearchForm = (props: SearchFormProps = {}) => { bsnmCd: first.bsnmCd, }, ); - }, [sidoList, props.defaultValues?.sido, mergedProps.sido]); + }, [sidoList, props.defaultValues?.sido, props.defaultRegionAll, mergedProps.sido]); const sggKey: [string, string, string, string] | null = values.sido && values.sido !== "ALL" @@ -177,6 +193,7 @@ const SearchForm = (props: SearchFormProps = {}) => { // 시도 설정 후 시군구 목록이 오면 첫 번째 항목 자동 선택 useEffect(() => { if (!mergedProps.gugun) return; + if (props.defaultRegionAll) return; if (props.defaultValues?.gugun) return; if (!values.sido || values.sido === "ALL") return; @@ -196,6 +213,7 @@ const SearchForm = (props: SearchFormProps = {}) => { values.sido, mergedProps.gugun, props.defaultValues?.gugun, + props.defaultRegionAll, ]); const handleSubmit = (e: React.FormEvent) => { @@ -210,11 +228,13 @@ const SearchForm = (props: SearchFormProps = {}) => { const handleSidoChange = (value: string) => { const selectedBsnm = - sidoList?.data?.sidoList?.find((v) => v.sido === value)?.bsnmCd || ""; + value === "ALL" + ? "" + : sidoList?.data?.sidoList?.find((v) => v.sido === value)?.bsnmCd || ""; setValues((prev) => ({ ...prev, sido: value, - gugun: "", + gugun: props.defaultRegionAll || value === "ALL" ? "ALL" : "", bsnmCd: selectedBsnm, })); }; @@ -403,7 +423,8 @@ const SearchForm = (props: SearchFormProps = {}) => { )} {mergedProps.isSummary && (
-
-
- -