238 lines
8.0 KiB
TypeScript
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 };
|
|
}
|