[st] - fix: 2-3 컬럼 로직 수정, 입력단 excel 업로드 시 행정동코드 매핑 검사

This commit is contained in:
2026-07-07 17:01:36 +09:00
parent e3a9851de6
commit d25f9f3a01
17 changed files with 298 additions and 16 deletions
+37 -4
View File
@@ -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<string, string | number>[] = [];
const admdstCdFromExcel: Array<string | number | undefined> = [];
for (let row = dataStartRow; row <= range.e.r; row++) {
const rowData: Record<string, string | number> = {};
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)
: [];
+41 -4
View File
@@ -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<string, string | number>[] = [];
const admdstCdFromExcel: Array<string | number | undefined> = [];
for (let row = dataStartRow; row <= range.e.r; row++) {
const rowData: Record<string, string | number> = {};
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,
}),
);
@@ -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<Record<string, string | number>[]>([]);
const [isNotApplicable, setIsNotApplicable] = useState(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
@@ -1059,6 +1068,8 @@ const ExcretaTreatmentFacilityInp = () => {
isExcelImport={
searchLoaded && tabEnterableForGrid && gugunOk
}
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle}
cellTooltipCallback={verificationCellTooltip}
/>
@@ -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}
@@ -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}
@@ -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<Record<string, string | number>[]>([]);
const [isNotApplicable, setIsNotApplicable] = useState(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
@@ -1043,6 +1052,8 @@ const PumpingStationInp = () => {
isExcelImport={
searchLoaded && tabEnterableForGrid && gugunOk
}
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle}
cellTooltipCallback={verificationCellTooltip}
/>
@@ -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
@@ -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
@@ -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}
@@ -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={
@@ -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<string, string | number> & {
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}
/>
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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<string, string | number>[]) => 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<string, string | number>[]) => void;
showHeaderCheckbox?: boolean; // 헤더 체크박스 표시 여부 (기본값: true)
@@ -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<string | number | undefined>,
): 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 };
}