255 lines
7.4 KiB
TypeScript
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,
|
|
};
|
|
}
|