Files
frontend/apps/st/src/components/util/gridExcelExport.ts
T

238 lines
8.0 KiB
TypeScript

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<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[],
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<string>();
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<string>();
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<string, string | number>[],
importedRows: Record<string, string | number>[],
options?: { preserveFieldNames?: readonly string[] },
): Record<string, string | number>[] {
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<string, string | number> = { ...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<string, boolean>;
/** 엑셀보내기 직전: 제외 컬럼 숨김, 그리드용 숨김(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<string, boolean>();
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 };
}