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, TSaveDto, >(config: SeGridConfig) { function createEmptyGridState(): SeGridState { 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): 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 { const baseByKey: Record = {}; 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, 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, 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): 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): 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 { const result = new Set([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, ): DisplaySlot[] { return structure.filter((slot) => !rowKeysToRemove.has(slot.rowKey)); } function patchGridRow( state: SeGridState, rowKey: string, patch: Partial, ): SeGridState { 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, rootRowKey: string, ): SeGridState { 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, 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, rowKey: string, ): SeGridState { 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, }; }