From 5b12f81b8fb2832ed33a9869f8e689d5198568d4 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 18 Aug 2026 17:44:22 +0900 Subject: [PATCH] =?UTF-8?q?[ST]=20-=20fix:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84=ED=91=9C=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=20=EC=88=98=EC=A0=95,=20=EC=97=91=EC=85=80?= =?UTF-8?q?=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=81=EC=9A=A9,=20=EC=9A=94=EC=95=BD=ED=91=9C?= =?UTF-8?q?=20=EB=A7=88=EC=9A=B0=EC=8A=A4=20=ED=88=B4=ED=8C=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20/=20=EC=9E=85=EB=A0=A5=EB=8B=A8=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=EB=8B=A8=20=EC=BB=AC=EB=9F=BC=20=EC=84=A4=EC=A0=95=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/css/DataGrid.css | 20 ++ apps/st/src/components/module/DataGrid.tsx | 146 +++++++-- apps/st/src/components/module/SearchForm.tsx | 29 +- apps/st/src/components/module/TabDataGrid.tsx | 148 +++++++-- .../input/ExcretaTreatmentFacilityInp.tsx | 24 +- .../input/SewageExcretaRelatedInp.tsx | 26 +- .../input/SewageExcretaSlurgeInp.tsx | 30 +- .../statistics/input/SewagePipeInp.tsx | 20 +- .../input/SewagePipeMaintainInp.tsx | 1 + .../statistics/input/SewerageFinanceInp.tsx | 6 +- .../input/SewerageServiceLevelInp.tsx | 14 +- .../statistics/input/WaterReuseInp.tsx | 32 +- .../statistics/review/SewagePipeRvw.tsx | 30 +- .../total/ExcretaTreatmentFacilityTot.tsx | 3 +- .../total/PrivateSewageFacilityTot.tsx | 3 +- .../total/PublicSewageFacilityTot.tsx | 3 +- .../statistics/total/PumpingStationTot.tsx | 3 +- .../total/SewageExcretaRelatedTot.tsx | 3 +- .../total/SewageExcretaSlurgeTot.tsx | 3 +- .../total/SewagePipeMaintainTot.tsx | 3 +- .../statistics/total/SewagePipeTot.tsx | 14 +- .../total/SewageReservoirPumpTot.tsx | 3 +- .../total/SewerageAggregateSummary.tsx | 1 + .../statistics/total/SewerageFinanceTot.tsx | 3 +- .../total/SeweragePenetrationRateTot.tsx | 3 +- .../total/SewerageServiceLevelTot.tsx | 3 +- .../statistics/total/WaterReuseTot.tsx | 3 +- .../OperatorsComponents.tsx | 37 ++- apps/st/src/components/type/DataGrid.type.ts | 4 + .../st/src/components/type/SearchForm.type.ts | 4 +- .../column/input/ExcretaTreatFactInpColumn.ts | 6 +- .../PublicSewageFactColumn.ts | 6 +- .../column/input/PumpingStationInpColumn.ts | 8 +- .../input/SeweragePenetrationRateInpColumn.ts | 6 +- .../PrivateSewageDesigStatusInpColumn.ts | 4 +- .../SepticTankStatusInpColumn.ts | 4 +- .../SewageTreatFactStatusInpColumn.ts | 4 +- .../ExcretaRelatedIndustryInpColumn.ts | 4 +- ...vateSewageTreatRelatedIndustryInpColumn.ts | 4 +- ...blicInstitutionExcretaIndustryInpColumn.ts | 4 +- ...PublicInstitutionSewerIndustryInpColumn.ts | 4 +- .../DigestorOperatingStatusInpColumn.ts | 6 +- .../ExcretaSlurgeProcessingStatusInpColumn.ts | 6 +- ...ewageSlurgeProcessingFacStatusInpColumn.ts | 4 +- .../SlurgeProcessingFacStatusInpColumn.ts | 6 +- .../sewagePipe/FactInstallStatusInpColumn.ts | 8 +- .../sewagePipe/ManholeStatusInpColumn.ts | 6 +- .../sewagePipe/PipeDiameterStatusInpColumn.ts | 7 +- .../sewagePipe/PipeTypeStatusInpColumn.ts | 7 +- .../sewagePipe/RegionalFactStatusInpColumn.ts | 6 +- .../PipeDredgingStatusInpColumn.ts | 4 +- .../RepairStatusInpColumn.ts | 4 +- .../RainfallPumpStationInpColumn.ts | 6 +- .../SewageReservoirInpColumn.ts | 6 +- .../sewerageFinance/CauserBurdenInpColumn.ts | 4 +- .../SewerageChargeInpColumn.ts | 4 +- .../SewerageFinanceSettleInpColumn.ts | 4 +- .../SewerageUsageFeeInpColumn.ts | 4 +- .../ComplaintInpColumn.ts | 4 +- .../ResidentFriendlyFactInpColumn.ts | 8 +- .../PublicSewageTreatWaterReuseInpColumn.ts | 4 +- .../RainFallUtilFactStatusInpColumn.ts | 4 +- .../RecycledWaterStatusInpColumn.ts | 4 +- ...atWaterReuseBusinessApprStatusInpColumn.ts | 4 +- ...eatWaterReuseDesignConstStatusInpColumn.ts | 4 +- .../util/column/review/SewagePipeRvwColumn.ts | 310 ++++++++++-------- .../util/column/total/SewagePipeTotColumn.ts | 94 +++++- .../st/src/components/util/gridExcelExport.ts | 102 ++++++ ...statistics-sewage-pipe-review-interface.ts | 92 +----- .../statistics-sewage-pipe-total-interface.ts | 116 +------ 70 files changed, 944 insertions(+), 570 deletions(-) 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 && (
-