import type { ColumnConfig, ColumnLayoutItem, ExcelExportSearchContext, } from "@/components/type/DataGrid.type"; import type { GridView } from "realgrid"; function flattenColumnLayoutNames(layout: ColumnLayoutItem[]): string[] { const result: string[] = []; for (const item of layout) { if (typeof item === "string") { result.push(item); continue; } if (item && typeof item === "object") { if ("column" in item && typeof item.column === "string") { result.push(item.column); continue; } if ("items" in item && Array.isArray(item.items)) { result.push( ...flattenColumnLayoutNames( item.items as ColumnLayoutItem[], ), ); } } } return result; } export type ExcelImportColumnFieldNamesOptions = { /** false면 editable:false 컬럼도 import (기본 true — 읽기전용·계산 컬럼은 엑셀 값 무시) */ 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[], 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) .map((c) => [c.name, c.fieldName as string]), ); 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 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에만 표시 */ export function prepareGridColumnsForExcelExport( gridView: GridView, columns: ColumnConfig[], ): ExcelExportVisibilitySnapshot { const excludedNames = new Set( columns.filter((c) => c.excludeFromExcelExport).map((c) => c.name), ); const snapshot = new Map(); columns.forEach((col) => { const gridCol = gridView.columnByName?.(col.name); if (!gridCol) return; const wasVisible = gridCol.visible !== false; snapshot.set(col.name, wasVisible); if (excludedNames.has(col.name)) { gridCol.visible = false; } else if (col.visible === false) { gridCol.visible = true; } }); return snapshot; } export function restoreGridColumnsAfterExcelExport( gridView: GridView, snapshot: ExcelExportVisibilitySnapshot, ): void { snapshot.forEach((visible, name) => { const gridCol = gridView.columnByName?.(name); if (gridCol) gridCol.visible = visible; }); } 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 }; }