Files
frontend/apps/as/src/pages/code-management/lib/grid-utils.ts
T

255 lines
7.4 KiB
TypeScript

import type {
BaseSeDto,
DisplaySlot,
SeDraftBase,
SeGridConfig,
SeGridState,
SeRowState,
} from "./grid-types";
export function createLoadedRowKey(index: number, code: string): string {
return `loaded-${index}-${code}`;
}
export function createDraftRowKey(): string {
return `draft-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
export function createSeGridUtils<
TDto extends BaseSeDto,
TBaseEntry extends TDto & { rowKey: string },
TRow extends TBaseEntry & SeRowState,
TDraft extends SeDraftBase,
TPatch extends Partial<TDto>,
TSaveDto,
>(config: SeGridConfig<TDto, TBaseEntry, TRow, TDraft, TPatch, TSaveDto>) {
function createEmptyGridState(): SeGridState<TDto, TBaseEntry, TDraft, TPatch> {
return {
baseByKey: {},
structure: [],
drafts: {},
patches: {},
deletedKeys: new Set(),
};
}
function toInitialRow(entry: TBaseEntry): TRow {
return {
...entry,
isNew: false,
isDirty: false,
isDeleted: false,
} as unknown as TRow;
}
function buildStructureFromBaseRows(baseByKey: Record<string, TBaseEntry>): DisplaySlot[] {
const rows = Object.values(baseByKey).map(toInitialRow);
const display = config.buildDisplayRows(rows);
const cdToRowKey = new Map(rows.map((row) => [config.getCode(row), row.rowKey]));
return display.map(({ row, depth }) => ({
rowKey: row.rowKey,
depth,
parentRowKey: config.getParentCode(row)?.trim()
? (cdToRowKey.get(config.getParentCode(row)!.trim()) ?? null)
: null,
}));
}
function hydrateGridFromServer(content: TDto[]): SeGridState<TDto, TBaseEntry, TDraft, TPatch> {
const baseByKey: Record<string, TBaseEntry> = {};
content.forEach((item, index) => {
const rowKey = createLoadedRowKey(index, config.getCode(item));
baseByKey[rowKey] = { ...item, rowKey } as TBaseEntry;
});
return {
baseByKey,
structure: buildStructureFromBaseRows(baseByKey),
drafts: {},
patches: {},
deletedKeys: new Set(),
};
}
function resolveParentCode(
state: SeGridState<TDto, TBaseEntry, TDraft, TPatch>,
parentRowKey: string | null,
): string | undefined {
if (!parentRowKey) return undefined;
const parent = mergeRow(state, parentRowKey);
const code = parent ? config.getCode(parent).trim() : "";
return code || undefined;
}
function mergeRow(
state: SeGridState<TDto, TBaseEntry, TDraft, TPatch>,
rowKey: string,
): TRow | null {
const draft = state.drafts[rowKey];
if (draft) {
const parentCode = resolveParentCode(state, draft.parentRowKey);
return config.assembleDraftRow(draft, parentCode);
}
const base = state.baseByKey[rowKey];
if (!base) return null;
const patch = state.patches[rowKey] ?? ({} as TPatch);
const hasPatch = Object.keys(patch).length > 0;
return {
...base,
...patch,
rowKey,
isNew: false,
isDirty: hasPatch,
isDeleted: state.deletedKeys.has(rowKey),
} as unknown as TRow;
}
function mergeAllRows(state: SeGridState<TDto, TBaseEntry, TDraft, TPatch>): TRow[] {
const keys = new Set([...Object.keys(state.baseByKey), ...Object.keys(state.drafts)]);
return [...keys]
.map((rowKey) => mergeRow(state, rowKey))
.filter((row): row is TRow => row != null);
}
function hasGridPendingChanges(state: SeGridState<TDto, TBaseEntry, TDraft, TPatch>): boolean {
return (
state.deletedKeys.size > 0 ||
Object.keys(state.drafts).length > 0 ||
Object.keys(state.patches).length > 0
);
}
function collectDescendantRowKeys(structure: DisplaySlot[], rootRowKey: string): Set<string> {
const result = new Set<string>([rootRowKey]);
const queue = [rootRowKey];
while (queue.length > 0) {
const parentKey = queue.shift()!;
for (const slot of structure) {
if (slot.parentRowKey === parentKey && !result.has(slot.rowKey)) {
result.add(slot.rowKey);
queue.push(slot.rowKey);
}
}
}
return result;
}
function insertRootSlot(structure: DisplaySlot[], slot: DisplaySlot): DisplaySlot[] {
const firstRootIndex = structure.findIndex((item) => item.parentRowKey === null);
if (firstRootIndex === -1) return [slot, ...structure];
return [...structure.slice(0, firstRootIndex), slot, ...structure.slice(firstRootIndex)];
}
function insertChildSlot(
structure: DisplaySlot[],
parentRowKey: string,
slot: DisplaySlot,
): DisplaySlot[] {
const parentIndex = structure.findIndex((item) => item.rowKey === parentRowKey);
if (parentIndex === -1) return [...structure, slot];
const insertAt = parentIndex + 1;
return [...structure.slice(0, insertAt), slot, ...structure.slice(insertAt)];
}
function removeSlotsFromStructure(
structure: DisplaySlot[],
rowKeysToRemove: Set<string>,
): DisplaySlot[] {
return structure.filter((slot) => !rowKeysToRemove.has(slot.rowKey));
}
function patchGridRow(
state: SeGridState<TDto, TBaseEntry, TDraft, TPatch>,
rowKey: string,
patch: Partial<TRow>,
): SeGridState<TDto, TBaseEntry, TDraft, TPatch> {
const draft = state.drafts[rowKey];
if (draft) {
return {
...state,
drafts: {
...state.drafts,
[rowKey]: config.applyDraftPatch(draft, patch),
},
};
}
const { rowKey: _rk, isNew: _n, isDirty: _d, isDeleted: _del, ...dtoPatch } = patch;
return {
...state,
patches: {
...state.patches,
[rowKey]: { ...state.patches[rowKey], ...dtoPatch } as TPatch,
},
};
}
function markRowsDeleted(
state: SeGridState<TDto, TBaseEntry, TDraft, TPatch>,
rootRowKey: string,
): SeGridState<TDto, TBaseEntry, TDraft, TPatch> {
const keysToDelete = collectDescendantRowKeys(state.structure, rootRowKey);
const deletedKeys = new Set(state.deletedKeys);
for (const key of keysToDelete) {
deletedKeys.add(key);
}
return { ...state, deletedKeys };
}
function validateDraftParentCode(
state: SeGridState<TDto, TBaseEntry, TDraft, TPatch>,
rowKey: string,
): string | null {
const draft = state.drafts[rowKey];
if (!draft?.parentRowKey) return null;
const parent = mergeRow(state, draft.parentRowKey);
if (!parent || !config.getCode(parent).trim()) {
return "하위 항목을 추가하려면 상위 코드를 먼저 입력하세요.";
}
return null;
}
function removeDraftFromGrid(
state: SeGridState<TDto, TBaseEntry, TDraft, TPatch>,
rowKey: string,
): SeGridState<TDto, TBaseEntry, TDraft, TPatch> {
const { [rowKey]: _removed, ...restDrafts } = state.drafts;
const keysToRemove = collectDescendantRowKeys(state.structure, rowKey);
return {
...state,
drafts: restDrafts,
structure: removeSlotsFromStructure(state.structure, keysToRemove),
};
}
return {
createEmptyGridState,
hydrateGridFromServer,
mergeRow,
mergeAllRows,
hasGridPendingChanges,
collectDescendantRowKeys,
insertRootSlot,
insertChildSlot,
removeSlotsFromStructure,
patchGridRow,
markRowsDeleted,
validateDraftParentCode,
removeDraftFromGrid,
resolveParentCode,
};
}