diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index d99127482..55b98a48c 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -25,6 +25,8 @@ import type { import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import { getExcelImportColumnFieldNames, + getNonEditableColumnFieldNames, + mergeExcelImportWithExistingRows, prepareGridColumnsForExcelExport, restoreGridColumnsAfterExcelExport, shouldPrepareColumnsForExcelExport, @@ -727,6 +729,7 @@ const DataGrid = ({ rowStartNum, excelExportFileSuffix, isExcelImport = true, + excelImportSkipNonEditable = true, isExcelExport = true, fixedColumns, showHeaderCheckbox = false, @@ -1070,6 +1073,7 @@ useEffect(() => { const excelColumnFieldNames = getExcelImportColumnFieldNames( columns, columnLayout, + { skipNonEditable: excelImportSkipNonEditable }, ); // 데이터 행 읽기 @@ -1137,7 +1141,14 @@ useEffect(() => { dataRows.push(rowData); } } - setRowData(dataRows); + const preserveFieldNames = excelImportSkipNonEditable + ? getNonEditableColumnFieldNames(columns, columnLayout) + : []; + setRowData((prev) => + mergeExcelImportWithExistingRows(prev, dataRows, { + preserveFieldNames, + }), + ); } catch (error) { console.error("엑셀 파일 파싱 오류:", error); alert("엑셀 파일 파싱 중 오류가 발생했습니다."); @@ -1263,6 +1274,11 @@ useEffect(() => { "editable", column.editable !== false, ); + gv.setColumnProperty?.( + column.name, + "styleName", + column.styleName ?? "", + ); applyColumnListEditorProperties(gv, column); }); if (columns.some((c) => isDropDownEditorColumn(c))) { @@ -1481,12 +1497,10 @@ useEffect(() => { fieldName: col.fieldName, header: realGridHeaderWithTooltip(col.header), editable: col.editable, + styleName: col.styleName ?? "", valueType: col.valueType, dataFormat: col.dataFormat, ...(col.width != null && { width: col.width }), - ...(col.styleName != null && { - styleName: col.styleName, - }), ...(col.mergeCells && { mergeRule: { criteria: "value" as const }, }), diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index 6b7eacc87..23b5d916d 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -36,6 +36,8 @@ import SplitDataGrid from "./SplitDataGrid"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" import { getExcelImportColumnFieldNames, + getNonEditableColumnFieldNames, + mergeExcelImportWithExistingRows, prepareGridColumnsForExcelExport, restoreGridColumnsAfterExcelExport, shouldPrepareColumnsForExcelExport, @@ -277,6 +279,7 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string const TabDataGrid = ({ columnInfo, isExcelImport = true, + excelImportSkipNonEditable = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, @@ -552,6 +555,7 @@ const TabDataGrid = ({ const excelColumnFieldNames = getExcelImportColumnFieldNames( columns, columnLayout, + { skipNonEditable: excelImportSkipNonEditable }, ); // 데이터 행 읽기 @@ -596,9 +600,19 @@ const TabDataGrid = ({ } } + const preserveFieldNames = excelImportSkipNonEditable + ? getNonEditableColumnFieldNames(columns, columnLayout) + : []; + setRowDataMap(prev => { const newMap = new Map(prev); - newMap.set(tabKey, dataRows); + const existingRows = prev.get(tabKey) ?? []; + newMap.set( + tabKey, + mergeExcelImportWithExistingRows(existingRows, dataRows, { + preserveFieldNames, + }), + ); return newMap; }); } catch (error) { diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index 8b24a86eb..87ba1d8bd 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -555,7 +555,7 @@ const PublicSewageFacilityInp = () => { sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', userId: userInfo.userUnqId, - // superAdmin: isAdminInfo ? 'superAdmin' : '', + superAdmin: isAdminInfo ? 'superAdmin' : '', noApplicable, saveList, }; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index a042a51f8..7b6ff77e1 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -108,6 +108,48 @@ const getPipeTypeLastDataYear = () => { return Math.max(1995, lastDataYear); }; +const PIPE_TYPE_STATUS_TAB_KEY = "ST00020003"; + +/** 조회 연도(statsYr)에 해당하는 연도별 매설연장 필드명 (y{year}LyngPrlg) */ +const resolvePipeTypeEditableLyngField = (statsYr: string | null | undefined): string | 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`; +}; + +/** ST00020003: 입력기간 통과 시 조회 연도의 매설연장(y{year}LyngPrlg)만 편집 가능 */ +const applyPipeTypeStatusEditableColumns = ( + cols: ColumnConfig[] | undefined, + canEdit: boolean, + periodEnterable: boolean, + statsYr: string | null, +): ColumnConfig[] => + (cols ?? []).map((col) => { + if (!canEdit) { + return { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" }; + } + + const fieldName = col.fieldName ?? col.name; + const isYearLyngField = + fieldName !== "lyngPrlg" && typeof fieldName === "string" && fieldName.endsWith("LyngPrlg"); + if (fieldName === "lyngPrlg" || isYearLyngField) { + const editableLyngField = periodEnterable ? resolvePipeTypeEditableLyngField(statsYr) : null; + const isTargetYear = editableLyngField !== null && fieldName === editableLyngField; + if (isTargetYear) { + const { styleName: _readonlyStyle, ...rest } = col; + return { + ...rest, + editable: true as const, + }; + } + return { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" }; + } + + return col; + }); + const getManholeLastDataYear = () => { const lastDataYear = new Date().getFullYear() - 1; return Math.max(2014, lastDataYear); @@ -507,11 +549,22 @@ const SewagePipeInp = () => { const canEdit = computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo; const applyEditable = (cols: ColumnConfig[] | undefined) => - (cols ?? []).map((col: ColumnConfig) => - canEdit - ? col - : { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" } - ); + tab.tabKey === PIPE_TYPE_STATUS_TAB_KEY + ? applyPipeTypeStatusEditableColumns( + cols, + canEdit, + inputPeriodEnterable, + lastSearchedYear, + ) + : (cols ?? []).map((col: ColumnConfig) => + canEdit + ? col + : { + ...col, + editable: false as const, + styleName: col.styleName ?? "rg-cell-readonly", + }, + ); if (tab.left && tab.right) { return { @@ -525,7 +578,7 @@ const SewagePipeInp = () => { columns: applyColumnGuides(applyEditable(tab.columns), tab.tabKey), }; }); - }, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable, columnGuidesByTab]); + }, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable, columnGuidesByTab, lastSearchedYear]); const commitToolbarSnapshotForLoadedTab = ( tabKey: string, @@ -1546,7 +1599,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - // superAdmin: isAdminInfo ? "superAdmin" : "", + superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -1626,7 +1679,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - // superAdmin: isAdminInfo ? "superAdmin" : "", + superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -1711,7 +1764,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - // superAdmin: isAdminInfo ? "superAdmin" : "", + superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -1774,7 +1827,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - // superAdmin: isAdminInfo ? "superAdmin" : "", + superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -1853,7 +1906,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - // superAdmin: isAdminInfo ? "superAdmin" : "", + superAdmin: isAdminInfo ? "superAdmin" : "", saveList, }; @@ -2080,6 +2133,9 @@ const SewagePipeInp = () => { }); }} isExcelImport={excelToolbarEnabledForTab} + excelImportSkipNonEditable={ + tabKey === PIPE_TYPE_STATUS_TAB_KEY ? !isAdminInfo : true + } isExcelExport showHeaderCheckbox={false} fixedColumns={info.fixedColumns} diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index fa4b09ee7..61ae738a9 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -353,9 +353,9 @@ const SeweragePenetrationRateInp = () => { statsCd: statsCd, }; - // if (isAdminInfo) { - // params.superAdmin = 'superAdmin'; - // } + if (isAdminInfo) { + params.superAdmin = 'superAdmin'; + } try { setIsLoading(true); @@ -682,9 +682,9 @@ const SeweragePenetrationRateInp = () => { sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계 }; - // if (isAdminInfo) { - // requestData.superAdmin = 'superAdmin'; - // } + if (isAdminInfo) { + requestData.superAdmin = 'superAdmin'; + } const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData); diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 00e4fbb01..02b7e3b1a 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -380,6 +380,8 @@ export type DataGridProps = { /** 있으면보내기 파일명: `{메뉴명}_{suffix}.xlsx` (TabDataGrid의 탭 라벨 접미사와 동일) */ excelExportFileSuffix?: string; isExcelImport?: boolean; // 엑셀 가져오기 유무 + /** 엑셀 가져오기 시 editable:false 컬럼 값 무시 (기본 true) */ + excelImportSkipNonEditable?: boolean; isExcelExport?: boolean; onRowsChange?: (rows: Record[]) => void; fixedColumns?: number; // 고정할 컬럼 개수 (좌측부터) @@ -432,6 +434,8 @@ export type TabDataGridProps = { columnInfo: ColumnInfoList[]; rows?: Record[]; isExcelImport?: boolean; // 엑셀 가져오기 유무 + /** 엑셀 가져오기 시 editable:false 컬럼 값 무시 (기본 true) */ + excelImportSkipNonEditable?: boolean; 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 983d4ba5a..60978c41e 100644 --- a/apps/st/src/components/util/gridExcelExport.ts +++ b/apps/st/src/components/util/gridExcelExport.ts @@ -28,14 +28,22 @@ function flattenColumnLayoutNames(layout: ColumnLayoutItem[]): string[] { return result; } +export type ExcelImportColumnFieldNamesOptions = { + /** false면 editable:false 컬럼도 import (기본 true — 읽기전용·계산 컬럼은 엑셀 값 무시) */ + skipNonEditable?: boolean; +}; + /** 엑셀 가져오기 — N번째 열을 그리드 N번째 fieldName에 매핑 (보내기 컬럼 순서와 동일) */ export function getExcelImportColumnFieldNames( columns: ColumnConfig[], columnLayout?: ColumnLayoutItem[], + options?: ExcelImportColumnFieldNamesOptions, ): string[] { + const skipNonEditable = options?.skipNonEditable !== false; const excludedNames = new Set( columns.filter((c) => c.excludeFromExcelExport).map((c) => c.name), ); + const columnByName = new Map(columns.map((c) => [c.name, c])); const fieldByName = new Map( columns .filter((c) => c.fieldName) @@ -54,11 +62,72 @@ export function getExcelImportColumnFieldNames( const fieldName = fieldByName.get(name); if (!fieldName) continue; seen.add(name); + const col = columnByName.get(name); + if (skipNonEditable && col?.editable === false) { + fieldNames.push(""); + continue; + } fieldNames.push(fieldName); } return fieldNames; } +/** 엑셀 import 시 기존 값을 유지할 fieldName 목록 (editable:false) */ +export function getNonEditableColumnFieldNames( + columns: ColumnConfig[], + columnLayout?: ColumnLayoutItem[], +): string[] { + const excludedNames = new Set( + columns.filter((c) => c.excludeFromExcelExport).map((c) => c.name), + ); + const columnByName = new Map(columns.map((c) => [c.name, c])); + + const layoutNames = + columnLayout && columnLayout.length > 0 + ? flattenColumnLayoutNames(columnLayout) + : columns.map((c) => c.name); + + const seen = new Set(); + const fieldNames: string[] = []; + for (const name of layoutNames) { + if (excludedNames.has(name) || seen.has(name)) continue; + const col = columnByName.get(name); + const fieldName = col?.fieldName; + if (!fieldName || col.editable !== false) continue; + seen.add(name); + fieldNames.push(fieldName); + } + return fieldNames; +} + +/** 엑셀 import 결과를 기존 행과 병합 — preserveFieldNames는 기존 그리드 값 유지 */ +export function mergeExcelImportWithExistingRows( + existingRows: Record[], + importedRows: Record[], + options?: { preserveFieldNames?: readonly string[] }, +): Record[] { + const preserveSet = + options?.preserveFieldNames && options.preserveFieldNames.length > 0 + ? new Set(options.preserveFieldNames) + : null; + + return importedRows.map((importedRow, index) => { + const existingRow = existingRows[index]; + if (!existingRow) return { ...importedRow }; + + const merged: Record = { ...existingRow, ...importedRow }; + if (preserveSet) { + for (const fieldName of preserveSet) { + const preserved = existingRow[fieldName]; + if (preserved !== undefined) { + merged[fieldName] = preserved; + } + } + } + return merged; + }); +} + export type ExcelExportVisibilitySnapshot = Map; /** 엑셀보내기 직전: 제외 컬럼 숨김, 그리드용 숨김(visible:false) 컬럼은 export에만 표시 */