246 lines
6.9 KiB
TypeScript
246 lines
6.9 KiB
TypeScript
import type { CnstpSeMngResponseDto } from "@/entities/cnstp";
|
|
|
|
import { buildCnstpDisplayRows } from "../model/build-cnstp-display-rows";
|
|
import type {
|
|
CnstpDisplaySlot,
|
|
CnstpSeBaseEntry,
|
|
CnstpSeGridState,
|
|
} from "../model/cnstp-se-grid-types";
|
|
import type { CnstpSeRow } from "../model/cnstp-se-row-types";
|
|
|
|
export function createLoadedRowKey(index: number, cnstpSeCd: string): string {
|
|
return `loaded-${index}-${cnstpSeCd}`;
|
|
}
|
|
|
|
export function createDraftRowKey(): string {
|
|
return `draft-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
}
|
|
|
|
export function createEmptyGridState(): CnstpSeGridState {
|
|
return {
|
|
baseByKey: {},
|
|
structure: [],
|
|
drafts: {},
|
|
patches: {},
|
|
deletedKeys: new Set(),
|
|
};
|
|
}
|
|
|
|
function toInitialRow(entry: CnstpSeBaseEntry): CnstpSeRow {
|
|
return {
|
|
...entry,
|
|
isNew: false,
|
|
isDirty: false,
|
|
isDeleted: false,
|
|
};
|
|
}
|
|
|
|
export function buildStructureFromBaseRows(
|
|
baseByKey: Record<string, CnstpSeBaseEntry>,
|
|
): CnstpDisplaySlot[] {
|
|
const rows = Object.values(baseByKey).map(toInitialRow);
|
|
const display = buildCnstpDisplayRows(rows);
|
|
const cdToRowKey = new Map(rows.map((row) => [row.cnstpSeCd, row.rowKey]));
|
|
|
|
return display.map(({ row, depth }) => ({
|
|
rowKey: row.rowKey,
|
|
depth,
|
|
parentRowKey: row.upCnstpCd?.trim() ? (cdToRowKey.get(row.upCnstpCd.trim()) ?? null) : null,
|
|
}));
|
|
}
|
|
|
|
export function hydrateGridFromServer(content: CnstpSeMngResponseDto[]): CnstpSeGridState {
|
|
const baseByKey: Record<string, CnstpSeBaseEntry> = {};
|
|
|
|
content.forEach((item, index) => {
|
|
const rowKey = createLoadedRowKey(index, item.cnstpSeCd);
|
|
baseByKey[rowKey] = { ...item, rowKey };
|
|
});
|
|
|
|
return {
|
|
baseByKey,
|
|
structure: buildStructureFromBaseRows(baseByKey),
|
|
drafts: {},
|
|
patches: {},
|
|
deletedKeys: new Set(),
|
|
};
|
|
}
|
|
|
|
export function resolveParentCnstpSeCd(
|
|
state: CnstpSeGridState,
|
|
parentRowKey: string | null,
|
|
): string | undefined {
|
|
if (!parentRowKey) return undefined;
|
|
const parent = mergeCnstpRow(state, parentRowKey);
|
|
const cd = parent?.cnstpSeCd?.trim();
|
|
return cd || undefined;
|
|
}
|
|
|
|
export function mergeCnstpRow(state: CnstpSeGridState, rowKey: string): CnstpSeRow | null {
|
|
const draft = state.drafts[rowKey];
|
|
if (draft) {
|
|
return {
|
|
rowKey,
|
|
cnstpSeCd: draft.cnstpSeCd,
|
|
cnstpSeCdNm: draft.cnstpSeCdNm,
|
|
expln: draft.expln,
|
|
useYn: draft.useYn,
|
|
cdLvNo: draft.cdLvNo,
|
|
upCnstpCd: resolveParentCnstpSeCd(state, draft.parentRowKey),
|
|
isNew: true,
|
|
isDirty: true,
|
|
isDeleted: false,
|
|
};
|
|
}
|
|
|
|
const base = state.baseByKey[rowKey];
|
|
if (!base) return null;
|
|
|
|
const patch = state.patches[rowKey] ?? {};
|
|
const hasPatch = Object.keys(patch).length > 0;
|
|
|
|
return {
|
|
...base,
|
|
...patch,
|
|
rowKey,
|
|
isNew: false,
|
|
isDirty: hasPatch,
|
|
isDeleted: state.deletedKeys.has(rowKey),
|
|
};
|
|
}
|
|
|
|
export function mergeAllCnstpRows(state: CnstpSeGridState): CnstpSeRow[] {
|
|
const keys = new Set([...Object.keys(state.baseByKey), ...Object.keys(state.drafts)]);
|
|
|
|
return [...keys]
|
|
.map((rowKey) => mergeCnstpRow(state, rowKey))
|
|
.filter((row): row is CnstpSeRow => row != null);
|
|
}
|
|
|
|
export function hasCnstpGridPendingChanges(state: CnstpSeGridState): boolean {
|
|
return (
|
|
state.deletedKeys.size > 0 ||
|
|
Object.keys(state.drafts).length > 0 ||
|
|
Object.keys(state.patches).length > 0
|
|
);
|
|
}
|
|
|
|
export function collectDescendantRowKeys(
|
|
structure: CnstpDisplaySlot[],
|
|
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;
|
|
}
|
|
|
|
export function insertRootSlot(
|
|
structure: CnstpDisplaySlot[],
|
|
slot: CnstpDisplaySlot,
|
|
): CnstpDisplaySlot[] {
|
|
const firstRootIndex = structure.findIndex((item) => item.parentRowKey === null);
|
|
if (firstRootIndex === -1) return [slot, ...structure];
|
|
return [...structure.slice(0, firstRootIndex), slot, ...structure.slice(firstRootIndex)];
|
|
}
|
|
|
|
export function insertChildSlot(
|
|
structure: CnstpDisplaySlot[],
|
|
parentRowKey: string,
|
|
slot: CnstpDisplaySlot,
|
|
): CnstpDisplaySlot[] {
|
|
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)];
|
|
}
|
|
|
|
export function removeSlotsFromStructure(
|
|
structure: CnstpDisplaySlot[],
|
|
rowKeysToRemove: Set<string>,
|
|
): CnstpDisplaySlot[] {
|
|
return structure.filter((slot) => !rowKeysToRemove.has(slot.rowKey));
|
|
}
|
|
|
|
export function patchCnstpGridRow(
|
|
state: CnstpSeGridState,
|
|
rowKey: string,
|
|
patch: Partial<CnstpSeRow>,
|
|
): CnstpSeGridState {
|
|
const draft = state.drafts[rowKey];
|
|
if (draft) {
|
|
return {
|
|
...state,
|
|
drafts: {
|
|
...state.drafts,
|
|
[rowKey]: {
|
|
...draft,
|
|
...(patch.cnstpSeCd !== undefined ? { cnstpSeCd: patch.cnstpSeCd } : {}),
|
|
...(patch.cnstpSeCdNm !== undefined ? { cnstpSeCdNm: patch.cnstpSeCdNm ?? "" } : {}),
|
|
...(patch.expln !== undefined ? { expln: patch.expln ?? "" } : {}),
|
|
...(patch.useYn !== undefined ? { useYn: patch.useYn } : {}),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
const { rowKey: _rk, isNew: _n, isDirty: _d, isDeleted: _del, ...dtoPatch } = patch;
|
|
|
|
return {
|
|
...state,
|
|
patches: {
|
|
...state.patches,
|
|
[rowKey]: { ...state.patches[rowKey], ...dtoPatch },
|
|
},
|
|
};
|
|
}
|
|
|
|
export function markCnstpRowsDeleted(
|
|
state: CnstpSeGridState,
|
|
rootRowKey: string,
|
|
): CnstpSeGridState {
|
|
const keysToDelete = collectDescendantRowKeys(state.structure, rootRowKey);
|
|
const deletedKeys = new Set(state.deletedKeys);
|
|
for (const key of keysToDelete) {
|
|
deletedKeys.add(key);
|
|
}
|
|
return { ...state, deletedKeys };
|
|
}
|
|
|
|
export function validateDraftParentCnstpSeCd(
|
|
state: CnstpSeGridState,
|
|
rowKey: string,
|
|
): string | null {
|
|
const draft = state.drafts[rowKey];
|
|
if (!draft?.parentRowKey) return null;
|
|
|
|
const parent = mergeCnstpRow(state, draft.parentRowKey);
|
|
if (!parent?.cnstpSeCd?.trim()) {
|
|
return "하위 항목을 추가하려면 상위 코드를 먼저 입력하세요.";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function removeDraftFromGrid(state: CnstpSeGridState, rowKey: string): CnstpSeGridState {
|
|
const { [rowKey]: _removed, ...restDrafts } = state.drafts;
|
|
const keysToRemove = collectDescendantRowKeys(state.structure, rowKey);
|
|
|
|
return {
|
|
...state,
|
|
drafts: restDrafts,
|
|
structure: removeSlotsFromStructure(state.structure, keysToRemove),
|
|
};
|
|
}
|