diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 55b98a48c..06e0882eb 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -24,8 +24,11 @@ import type { import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import { + buildExcelExportFileBaseName, getExcelImportColumnFieldNames, getNonEditableColumnFieldNames, + hasAdmdstCdColumn, + isExcelImportAdmdstCdInvalid, mergeExcelImportWithExistingRows, prepareGridColumnsForExcelExport, restoreGridColumnsAfterExcelExport, @@ -728,8 +731,10 @@ const DataGrid = ({ columnInfo, rowStartNum, excelExportFileSuffix, + excelExportSearchContext, isExcelImport = true, excelImportSkipNonEditable = true, + excelImportSewerBzmnCd, isExcelExport = true, fixedColumns, showHeaderCheckbox = false, @@ -951,9 +956,11 @@ useEffect(() => { ? prepareGridColumnsForExcelExport(gridView, columns) : undefined; - const exportBaseName = excelExportFileSuffix - ? `${menuName}_${excelExportFileSuffix}` - : menuName; + const exportBaseName = buildExcelExportFileBaseName( + menuName, + excelExportFileSuffix, + excelExportSearchContext, + ); gridView.exportGrid({ type: "excel", @@ -1075,11 +1082,18 @@ useEffect(() => { columnLayout, { skipNonEditable: excelImportSkipNonEditable }, ); + const excelColumnFieldNamesAll = getExcelImportColumnFieldNames( + columns, + columnLayout, + { skipNonEditable: false }, + ); // 데이터 행 읽기 const dataRows: Record[] = []; + const admdstCdFromExcel: Array = []; for (let row = dataStartRow; row <= range.e.r; row++) { const rowData: Record = {}; + let rowAdmdstCd: string | number | undefined; let hasData = false; for ( @@ -1093,7 +1107,7 @@ useEffect(() => { XLSX.utils.encode_cell({ r: row, c: col }) ]; const fieldName = excelColumnFieldNames[colIndex]; - if (!fieldName) continue; + const allFieldName = excelColumnFieldNamesAll[colIndex]; let cellValue: string | number = ""; if ( @@ -1125,6 +1139,12 @@ useEffect(() => { } } + if (allFieldName === "admdstCd" && cellValue !== "") { + rowAdmdstCd = cellValue; + } + + if (!fieldName) continue; + if ( cellValue !== "" && cellValue !== null && @@ -1139,8 +1159,21 @@ useEffect(() => { // 데이터가 하나라도 있는 행만 추가 if (hasData) { dataRows.push(rowData); + admdstCdFromExcel.push(rowAdmdstCd); } } + + if ( + excelImportSewerBzmnCd && + hasAdmdstCdColumn(columns) && + isExcelImportAdmdstCdInvalid( + excelImportSewerBzmnCd, + admdstCdFromExcel, + ) + ) { + alert("조회 대상과 업로드한 엑셀 데이터의 행정동 코드가 일치 하지 않습니다."); + return; + } const preserveFieldNames = excelImportSkipNonEditable ? getNonEditableColumnFieldNames(columns, columnLayout) : []; diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index 23b5d916d..3d47b99a8 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -35,8 +35,11 @@ import SplitDataGrid from "./SplitDataGrid"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" import { + buildExcelExportFileBaseName, getExcelImportColumnFieldNames, getNonEditableColumnFieldNames, + hasAdmdstCdColumn, + isExcelImportAdmdstCdInvalid, mergeExcelImportWithExistingRows, prepareGridColumnsForExcelExport, restoreGridColumnsAfterExcelExport, @@ -280,6 +283,8 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, excelImportSkipNonEditable = true, + excelImportSewerBzmnCd, + excelExportSearchContext, isExcelExport = true, rows = [], showHeaderCheckbox = false, @@ -455,10 +460,16 @@ const TabDataGrid = ({ ? prepareGridColumnsForExcelExport(gridView, columns) : undefined; + const exportBaseName = buildExcelExportFileBaseName( + menuName, + tabLabel, + excelExportSearchContext, + ); + gridView.exportGrid({ type: 'excel', target: 'local', - fileName: `${menuName}_${tabLabel}.xlsx`, + fileName: `${exportBaseName}.xlsx`, footer: 'hidden', allColumns: !useExcelColumnVisibilityPrep, done: () => { @@ -557,18 +568,25 @@ const TabDataGrid = ({ columnLayout, { skipNonEditable: excelImportSkipNonEditable }, ); + const excelColumnFieldNamesAll = getExcelImportColumnFieldNames( + columns, + columnLayout, + { skipNonEditable: false }, + ); // 데이터 행 읽기 const dataRows: Record[] = []; + const admdstCdFromExcel: Array = []; for (let row = dataStartRow; row <= range.e.r; row++) { const rowData: Record = {}; + let rowAdmdstCd: string | number | undefined; let hasData = false; for (let colIndex = 0; colIndex < headers.length; colIndex++) { const col = range.s.c + colIndex; const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: col })]; const fieldName = excelColumnFieldNames[colIndex]; - if (!fieldName) continue; + const allFieldName = excelColumnFieldNamesAll[colIndex]; let cellValue: string | number = ''; if (cell && cell.v !== undefined && cell.v !== null) { @@ -587,6 +605,12 @@ const TabDataGrid = ({ } } + if (allFieldName === 'admdstCd' && cellValue !== '') { + rowAdmdstCd = cellValue; + } + + if (!fieldName) continue; + if (cellValue !== '' && cellValue !== null && cellValue !== undefined) { hasData = true; } @@ -597,19 +621,32 @@ const TabDataGrid = ({ // 데이터가 하나라도 있는 행만 추가 if (hasData) { dataRows.push(rowData); + admdstCdFromExcel.push(rowAdmdstCd); } } + if ( + excelImportSewerBzmnCd && + hasAdmdstCdColumn(columns) && + isExcelImportAdmdstCdInvalid( + excelImportSewerBzmnCd, + admdstCdFromExcel, + ) + ) { + alert('조회 대상과 행정동 코드가 일치 하지 않습니다.'); + return; + } + const preserveFieldNames = excelImportSkipNonEditable ? getNonEditableColumnFieldNames(columns, columnLayout) : []; setRowDataMap(prev => { const newMap = new Map(prev); - const existingRows = prev.get(tabKey) ?? []; + const prevRows = prev.get(tabKey) ?? []; newMap.set( tabKey, - mergeExcelImportWithExistingRows(existingRows, dataRows, { + mergeExcelImportWithExistingRows(prevRows, dataRows, { preserveFieldNames, }), ); diff --git a/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx b/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx index ca9872612..2b5eb6dc5 100644 --- a/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx @@ -24,6 +24,7 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -309,6 +310,10 @@ function createEmptyExcretaTreatmentFacilityRow( return row; } +const EXCRETA_TREATMENT_FACILITY_NAME_GUIDE_MESSAGE = + "분뇨처리시설 등록시 분뇨처리시설명이 동일할 경우 같은 데이터로 분류 됩니다.\n\n" + + "분뇨처리시설명이 동일 할 경우 구분을 위해 분뇨처리시설 1, 분뇨처리시설 2 형태로 구분하여 주시길 바랍니다."; + // 하수도 통계 관리 > 07. 분뇨처리시설 const ExcretaTreatmentFacilityInp = () => { const { setMessage: alert } = useAlertStore((state) => state); @@ -320,6 +325,10 @@ const ExcretaTreatmentFacilityInp = () => { const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + useEffect(() => { + alert(EXCRETA_TREATMENT_FACILITY_NAME_GUIDE_MESSAGE); + }, [alert]); + const [rowData, setRowData] = useState[]>([]); const [isNotApplicable, setIsNotApplicable] = useState(false); const [searchValues, setSearchValues] = useState(null); @@ -1059,6 +1068,8 @@ const ExcretaTreatmentFacilityInp = () => { isExcelImport={ searchLoaded && tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} /> diff --git a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx index b2edb8f2c..9b5144819 100644 --- a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx @@ -44,6 +44,7 @@ import SaveButton from "@/components/button/SaveButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import SearchForm from "@/components/module/SearchForm"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -1264,6 +1265,8 @@ const PrivateSewageFacilityInp = () => { tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index 87ba1d8bd..685cea13c 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -38,6 +38,7 @@ import SubmitButton from '@/components/button/SumitButton'; import UpdateOperateDataButton from '@/components/button/UpdateOperateDataButton'; import VerificationResultButton from '@/components/button/VerificationResultButton'; import DataGrid from '@/components/module/DataGrid'; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import SearchForm from '@/components/module/SearchForm'; import SplitDataGrid from '@/components/module/SplitDataGrid'; import { openVerificationResultDialogIfNeeded } from '@/components/module/post-submit-verification-results'; @@ -1346,6 +1347,8 @@ const PublicSewageFacilityInp = () => { tab.tabKey === 'ST00060001' ? st60001GridGetRowsRef : undefined } isExcelImport={excelToolbarEnabledForTab} + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport showHeaderCheckbox={false} enableSort={false} diff --git a/apps/st/src/components/statistics/input/PumpingStationInp.tsx b/apps/st/src/components/statistics/input/PumpingStationInp.tsx index 67d359d86..09910a3df 100644 --- a/apps/st/src/components/statistics/input/PumpingStationInp.tsx +++ b/apps/st/src/components/statistics/input/PumpingStationInp.tsx @@ -24,6 +24,7 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -304,6 +305,10 @@ function createEmptyPumpingStationRow( return row; } +const PUMPING_STATION_FACILITY_NAME_GUIDE_MESSAGE = + "펌프장 등록시 시설명이 동일할 경우 같은 데이터로 분류 됩니다.\n\n" + + "시설명이 동일 할 경우 구분을 위해 펌프장 1, 펌프장 2 형태로 구분하여 주시기 바랍니다."; + // 하수도 통계 관리 > 05. 펌프장 (SewageReservoirPumpInp 단일 탭 패턴) const PumpingStationInp = () => { const { setMessage: alert } = useAlertStore((state) => state); @@ -315,6 +320,10 @@ const PumpingStationInp = () => { const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + useEffect(() => { + alert(PUMPING_STATION_FACILITY_NAME_GUIDE_MESSAGE); + }, [alert]); + const [rowData, setRowData] = useState[]>([]); const [isNotApplicable, setIsNotApplicable] = useState(false); const [searchValues, setSearchValues] = useState(null); @@ -1043,6 +1052,8 @@ const PumpingStationInp = () => { isExcelImport={ searchLoaded && tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} /> diff --git a/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx index addeda5c0..974b47556 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx @@ -16,6 +16,7 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -2056,6 +2057,8 @@ const SewageExcretaRelatedInp = () => { tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport getGridDataRef={ activeTab === tabKey ? getGridDataRef : undefined diff --git a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx index 885d00f8b..90ad9e99c 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx @@ -24,6 +24,7 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -2147,6 +2148,8 @@ const SewageExcretaSlurgeInp = () => { enableSort={false} statusInfo={tabStatusInfo} isExcelImport={tabRowDataExists(rowDataMap, tabKey)} + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport getGridDataRef={ activeTab === tabKey ? getGridDataRef : undefined diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 7b6ff77e1..d49ba291a 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -53,13 +53,14 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import GridTopActionButton from "@/components/module/GridTopActionButton"; import SearchForm from "@/components/module/SearchForm"; import SplitDataGrid from "@/components/module/SplitDataGrid"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; -import type { ColumnConfig,ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList, ColumnLayoutItem } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { doList } from "@/components/util/column/input/DoList"; import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn"; @@ -110,28 +111,85 @@ const getPipeTypeLastDataYear = () => { const PIPE_TYPE_STATUS_TAB_KEY = "ST00020003"; -/** 조회 연도(statsYr)에 해당하는 연도별 매설연장 필드명 (y{year}LyngPrlg) */ -const resolvePipeTypeEditableLyngField = (statsYr: string | null | undefined): string | null => { +/** 조회 연도(statsYr)에 해당하는 연도 (y{year}* 필드용) */ +const resolvePipeTypeEditableYear = (statsYr: string | null | undefined): number | null => { if (!statsYr) return null; const year = Number(statsYr); if (!Number.isFinite(year)) return null; - const clamped = Math.min(getPipeTypeLastDataYear(), Math.max(1995, year)); - return `y${clamped}LyngPrlg`; + return Math.min(getPipeTypeLastDataYear(), Math.max(1995, year)); }; -/** ST00020003: 입력기간 통과 시 조회 연도의 매설연장(y{year}LyngPrlg)만 편집 가능 */ +/** 조회 연도(statsYr)에 해당하는 연도별 매설연장 필드명 (y{year}LyngPrlg) */ +const resolvePipeTypeEditableLyngField = (statsYr: string | null | undefined): string | null => { + const year = resolvePipeTypeEditableYear(statsYr); + return year === null ? null : `y${year}LyngPrlg`; +}; + +const pipeTypeYearDmltnField = (year: number) => `y${year}DmltnPrlg`; + +const shouldHidePipeTypeEditableYearDmltn = ( + canEdit: boolean, + periodEnterable: boolean, + statsYr: string | null, +): number | null => { + if (!canEdit || !periodEnterable) return null; + return resolvePipeTypeEditableYear(statsYr); +}; + +const applyPipeTypeStatusColumnLayout = ( + layout: ColumnLayoutItem[] | undefined, + hideDmltnYear: number | null, +): ColumnLayoutItem[] => { + if (hideDmltnYear === null) return layout ?? []; + const hideField = pipeTypeYearDmltnField(hideDmltnYear); + + const walk = (items: ColumnLayoutItem[]): ColumnLayoutItem[] => { + const result: ColumnLayoutItem[] = []; + for (const item of items) { + if (typeof item === "string") { + if (item !== hideField) result.push(item); + continue; + } + if ("column" in item) { + if (item.column !== hideField) result.push(item); + continue; + } + const nextItems = walk(item.items as ColumnLayoutItem[]); + if (nextItems.length > 0) { + result.push({ ...item, items: nextItems }); + } + } + return result; + }; + + return walk(layout ?? []); +}; + +/** ST00020003: 입력기간 통과 시 조회 연도의 매설연장(y{year}LyngPrlg)만 편집 가능, 해당 연도 철거연장 컬럼 숨김 */ const applyPipeTypeStatusEditableColumns = ( cols: ColumnConfig[] | undefined, canEdit: boolean, periodEnterable: boolean, statsYr: string | null, -): ColumnConfig[] => - (cols ?? []).map((col) => { +): ColumnConfig[] => { + const hideDmltnYear = shouldHidePipeTypeEditableYearDmltn(canEdit, periodEnterable, statsYr); + + return (cols ?? []).map((col) => { if (!canEdit) { return { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" }; } const fieldName = col.fieldName ?? col.name; + + if (hideDmltnYear !== null && fieldName === pipeTypeYearDmltnField(hideDmltnYear)) { + return { + ...col, + visible: false, + editable: false as const, + excludeFromExcelExport: true, + }; + } + const isYearLyngField = fieldName !== "lyngPrlg" && typeof fieldName === "string" && fieldName.endsWith("LyngPrlg"); if (fieldName === "lyngPrlg" || isYearLyngField) { @@ -149,6 +207,7 @@ const applyPipeTypeStatusEditableColumns = ( return col; }); +}; const getManholeLastDataYear = () => { const lastDataYear = new Date().getFullYear() - 1; @@ -576,6 +635,18 @@ const SewagePipeInp = () => { return { ...tab, columns: applyColumnGuides(applyEditable(tab.columns), tab.tabKey), + ...(tab.tabKey === PIPE_TYPE_STATUS_TAB_KEY && tab.columnLayout + ? { + columnLayout: applyPipeTypeStatusColumnLayout( + tab.columnLayout, + shouldHidePipeTypeEditableYearDmltn( + canEdit, + inputPeriodEnterable, + lastSearchedYear, + ), + ), + } + : {}), }; }); }, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable, columnGuidesByTab, lastSearchedYear]); @@ -2136,6 +2207,8 @@ const SewagePipeInp = () => { excelImportSkipNonEditable={ tabKey === PIPE_TYPE_STATUS_TAB_KEY ? !isAdminInfo : true } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport showHeaderCheckbox={false} fixedColumns={info.fixedColumns} diff --git a/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx b/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx index 64ea20096..3c2e86d24 100644 --- a/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx @@ -46,6 +46,7 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import SearchForm from "@/components/module/SearchForm"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -1202,6 +1203,8 @@ const SewagePipeMaintainInp = () => { tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} getGridDataRef={ diff --git a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx index 69e9d946b..6d0f8104b 100644 --- a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx +++ b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx @@ -25,6 +25,7 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -97,6 +98,10 @@ const SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS = Array.from( ), ); +const SEWAGE_RESERVOIR_FACILITY_NAME_GUIDE_MESSAGE = + "하수저류시설 등록시 하수저류시설명이 동일할 경우 같은 데이터로 분류됩니다.\n\n" + + "하수저류시설명이 동일 할 경우 구분을 위해 하수저류시설 1, 하수저류시설 2 형태로 구분하여 주시길 바랍니다."; + type GridRowWithVerification = Record & { verificationResult?: SewageReservoirVerificationResult; }; @@ -459,6 +464,10 @@ const SewageReservoirPumpInp = () => { const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + useEffect(() => { + alert(SEWAGE_RESERVOIR_FACILITY_NAME_GUIDE_MESSAGE); + }, [alert]); + const submitFunc = () => { void (async () => { const tabKey = activeTabRef.current; @@ -1655,6 +1664,8 @@ const SewageReservoirPumpInp = () => { tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} /> diff --git a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx index 058eaf784..6c1d8fff7 100644 --- a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx @@ -48,6 +48,7 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -1330,6 +1331,8 @@ const SewerageFinanceInp = () => { tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 61ae738a9..51ed15270 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -40,6 +40,7 @@ import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; // import { DataGrid } from "@rsone/components"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import SearchForm from "@/components/module/SearchForm"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -807,6 +808,8 @@ const SeweragePenetrationRateInp = () => { key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'} columnInfo={columnInfo} isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)} + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} rowStartNum={3} fixedColumns={1} rowData={rowData} diff --git a/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx b/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx index d9b8c46bc..55e3aac24 100644 --- a/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx @@ -13,6 +13,7 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -1240,6 +1241,8 @@ const SewerageServiceLevelInp = () => { tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} diff --git a/apps/st/src/components/statistics/input/WaterReuseInp.tsx b/apps/st/src/components/statistics/input/WaterReuseInp.tsx index a1735f454..d3ef54a83 100644 --- a/apps/st/src/components/statistics/input/WaterReuseInp.tsx +++ b/apps/st/src/components/statistics/input/WaterReuseInp.tsx @@ -25,6 +25,7 @@ import SubmitButton from "@/components/button/SumitButton"; import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; +import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; @@ -2194,6 +2195,8 @@ const WaterReuseInp = () => { tabEnterableForGrid && gugunOk } + excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} + excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 02b7e3b1a..ea4769dcb 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -374,6 +374,12 @@ export type DataCellClickCallback = ( params: DataCellClickCallbackParams, ) => void; +export type ExcelExportSearchContext = { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; +}; + export type DataGridProps = { columnInfo: ColumnInfo; rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주) @@ -382,6 +388,10 @@ export type DataGridProps = { isExcelImport?: boolean; // 엑셀 가져오기 유무 /** 엑셀 가져오기 시 editable:false 컬럼 값 무시 (기본 true) */ excelImportSkipNonEditable?: boolean; + /** 엑셀 import 시 admdstCd 앞 2자리와 비교할 조회 sewerBzmnCd(SearchForm bsnmCd) */ + excelImportSewerBzmnCd?: string; + /** 엑셀보내기 파일명에 조회 시도·시군구·연도 접미사 추가 */ + excelExportSearchContext?: ExcelExportSearchContext; isExcelExport?: boolean; onRowsChange?: (rows: Record[]) => void; fixedColumns?: number; // 고정할 컬럼 개수 (좌측부터) @@ -436,6 +446,10 @@ export type TabDataGridProps = { isExcelImport?: boolean; // 엑셀 가져오기 유무 /** 엑셀 가져오기 시 editable:false 컬럼 값 무시 (기본 true) */ excelImportSkipNonEditable?: boolean; + /** 엑셀 import 시 admdstCd 앞 2자리와 비교할 조회 sewerBzmnCd(SearchForm bsnmCd) */ + excelImportSewerBzmnCd?: string; + /** 엑셀보내기 파일명에 조회 시도·시군구·연도 접미사 추가 */ + excelExportSearchContext?: ExcelExportSearchContext; isExcelExport?: boolean; onRowsChange?: (rows: Record[]) => void; showHeaderCheckbox?: boolean; // 헤더 체크박스 표시 여부 (기본값: true) diff --git a/apps/st/src/components/util/gridExcelExport.ts b/apps/st/src/components/util/gridExcelExport.ts index 60978c41e..d48395ba4 100644 --- a/apps/st/src/components/util/gridExcelExport.ts +++ b/apps/st/src/components/util/gridExcelExport.ts @@ -1,6 +1,7 @@ import type { ColumnConfig, ColumnLayoutItem, + ExcelExportSearchContext, } from "@/components/type/DataGrid.type"; import type { GridView } from "realgrid"; @@ -33,6 +34,27 @@ export type ExcelImportColumnFieldNamesOptions = { skipNonEditable?: boolean; }; +const ADMDST_CD_FIELD = "admdstCd"; + +export function hasAdmdstCdColumn(columns: ColumnConfig[]): boolean { + return columns.some((col) => col.fieldName === ADMDST_CD_FIELD); +} + +/** 엑셀에서 읽은 admdstCd만 검증 — 빈 값 또는 sewerBzmnCd 앞 2자리 불일치 시 true(실패) */ +export function isExcelImportAdmdstCdInvalid( + sewerBzmnCd: string | null | undefined, + admdstCdFromExcel: ReadonlyArray, +): boolean { + const expectedPrefix = sewerBzmnCd?.trim().slice(0, 2) ?? ""; + if (expectedPrefix.length < 2) return false; + + return admdstCdFromExcel.some((raw) => { + const admdstCd = String(raw ?? "").trim(); + if (!admdstCd) return true; + return admdstCd.slice(0, 2) !== expectedPrefix; + }); +} + /** 엑셀 가져오기 — N번째 열을 그리드 N번째 fieldName에 매핑 (보내기 컬럼 순서와 동일) */ export function getExcelImportColumnFieldNames( columns: ColumnConfig[], @@ -170,3 +192,46 @@ export function restoreGridColumnsAfterExcelExport( export function shouldPrepareColumnsForExcelExport(columns: ColumnConfig[]): boolean { return columns.some((c) => c.excludeFromExcelExport || c.visible === false); } + +const INVALID_EXCEL_FILE_NAME_CHARS = /[\\/:*?"<>|]/g; + +function sanitizeExcelFileNamePart(value: string): string { + return value.replace(INVALID_EXCEL_FILE_NAME_CHARS, "").trim(); +} + +/** 엑셀 파일명: `{메뉴명}_{탭접미사}_{시도}_{시군구}_{연도}` */ +export function buildExcelExportFileBaseName( + menuName: string, + tabSuffix?: string, + searchContext?: ExcelExportSearchContext | null, +): string { + const parts: string[] = [sanitizeExcelFileNamePart(menuName)]; + + if (tabSuffix?.trim()) { + parts.push(sanitizeExcelFileNamePart(tabSuffix.trim())); + } + + const ctpvNm = searchContext?.ctpvNm?.trim(); + const sggNm = searchContext?.sggNm?.trim(); + const statsYr = searchContext?.statsYr?.trim(); + + if (ctpvNm) parts.push(sanitizeExcelFileNamePart(ctpvNm)); + if (sggNm && sggNm !== "ALL") parts.push(sanitizeExcelFileNamePart(sggNm)); + if (statsYr) parts.push(sanitizeExcelFileNamePart(statsYr)); + + return parts.filter(Boolean).join("_"); +} + +export function toExcelExportSearchContextFromSearchForm( + values: { sido?: string; gugun?: string; year?: string } | null | undefined, +): ExcelExportSearchContext | undefined { + if (!values) return undefined; + + const ctpvNm = values.sido?.trim(); + const sggNm = values.gugun === "ALL" ? "" : (values.gugun?.trim() ?? ""); + const statsYr = values.year?.trim(); + + if (!ctpvNm && !sggNm && !statsYr) return undefined; + + return { ctpvNm, sggNm, statsYr }; +}