diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index 228c16703..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]; } diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index cdc2e570b..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"; @@ -32,33 +32,64 @@ import { } 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 */ @@ -159,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; @@ -184,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, + ); }; }); } @@ -815,6 +868,7 @@ const DataGrid = ({ rowStartNum, excelExportFileSuffix, excelExportSearchContext, + excelExportApplyStyles = false, isExcelImport = true, excelImportSkipNonEditable = false, excelImportSewerBzmnCd, @@ -844,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); @@ -856,6 +914,7 @@ const DataGrid = ({ const cellStyleCallbackRef = useRef( cellStyleCallback, ); + const excelExportApplyStylesRef = useRef(excelExportApplyStyles); const cellTooltipCallbackRef = useRef( cellTooltipCallback, ); @@ -885,6 +944,7 @@ const DataGrid = ({ columnsRef.current = columns; rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; + excelExportApplyStylesRef.current = excelExportApplyStyles; cellTooltipCallbackRef.current = cellTooltipCallback; footerVerificationResultRef.current = footerVerificationResult; onScrollToBottomRef.current = onScrollToBottom; @@ -1039,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( @@ -1047,6 +1108,8 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); (gv as { refresh?: () => void }).refresh?.(); }; @@ -1058,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); @@ -1156,6 +1219,10 @@ useEffect(() => { const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep ? prepareGridColumnsForExcelExport(gridView, columns) : undefined; + const savedFixedColCount = + !excelExportApplyStyles && fixedColumns + ? releaseFixedOptionsForExcelExport(gridView, fixedColumns) + : undefined; const exportBaseName = buildExcelExportFileBaseName( menuName, @@ -1169,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, @@ -1187,7 +1261,7 @@ useEffect(() => { } }, 100); } - alert("내보내기가 완료되었습니다."); + // alert("내보내기가 완료되었습니다."); }, }); }; @@ -1515,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))) { @@ -1705,6 +1788,8 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); gridRef.current.gridView.refresh?.(); } catch (e) { @@ -1794,6 +1879,8 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); // setColumns 호출 후 footer 배열의 valueCallback을 다시 설정 @@ -1847,6 +1934,8 @@ useEffect(() => { columns, rowDataRef, cellStyleCallbackRef, + excelExportApplyStylesRef, + fixedColumnNameSet, ); liveGridView.refresh?.(); }, 50); @@ -1930,6 +2019,8 @@ useEffect(() => { columns, columnLayout, fixedColumns, + fixedColumnNameSet, + excelExportApplyStyles, rowData, showHeaderCheckbox, getGridDataRef, @@ -2732,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 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 && (
-