Files
Aislo/M01_MasterData/M01_MasterData_Draft.ts
T

162 lines
4.7 KiB
TypeScript

/* =============================================================================
* M01_MasterData_Draft.ts
* 고친 것 초안 — sessionStorage 에만 쌓임(자동저장 없음) · [저장] 때만 서버로.
*
* 요소 파일 = 줄 단위(edits · adds · deletes) · 표형 파일 = 표 하나 통째(tables).
* ========================================================================== */
import type { FileChanges, Row } from "./M01_MasterData_Api_Fetch";
/** o = 원래 줄 번호(새 줄은 null) — 노랑 칠은 이 번호의 원래 줄과 견줌. */
export interface TableItem {
o: number | null;
r: Row;
}
interface TableDraft {
base: Row;
items: TableItem[];
}
interface FileDraft {
version: string;
edits: Record<string, Row>;
adds: Row[];
deletes: string[];
tables: Record<string, TableDraft>;
}
const STORAGE_KEY = "m01_master_draft";
const listeners = new Set<() => void>();
function readStore(): Record<string, FileDraft> {
try {
return JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? "{}") as Record<string, FileDraft>;
} catch {
return {};
}
}
let store = readStore();
export const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
/** 구독 — 돌려받은 함수를 부르면 해제. */
export function onDraftChange(fn: () => void): () => void {
listeners.add(fn);
return () => void listeners.delete(fn);
}
function touch(file: string): void {
const d = store[file];
if (
d &&
!Object.keys(d.edits).length &&
!d.adds.length &&
!d.deletes.length &&
!Object.keys(d.tables).length
) {
delete store[file];
}
try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(store));
} catch {
/* 저장소 막힘 — 이 탭 메모리에서만 유지 */
}
listeners.forEach((fn) => fn());
}
function slot(file: string, version: string): FileDraft {
return (store[file] ??= { version, edits: {}, adds: [], deletes: [], tables: {} });
}
export function peek(file: string): FileDraft | undefined {
return store[file];
}
const countOf = (d: FileDraft): number =>
Object.keys(d.edits).length + d.adds.length + d.deletes.length + Object.keys(d.tables).length;
export const fileCount = (file: string): number => (store[file] ? countOf(store[file]) : 0);
export const totalCount = (): number => Object.values(store).reduce((n, d) => n + countOf(d), 0);
/** 그 파일을 초안이 받은 판본과 다른 판본으로 읽었나 — 저장하면 409 로 막힐 초안. */
export const isStale = (file: string, version: string): boolean =>
store[file] !== undefined && store[file].version !== version;
/* --- 요소 파일 --- */
export function editRow(file: string, version: string, key: string, orig: Row, next: Row): void {
const d = slot(file, version);
if (same(orig, next)) delete d.edits[key];
else d.edits[key] = next;
touch(file);
}
export function addRow(file: string, version: string, row: Row): void {
slot(file, version).adds.push(row);
touch(file);
}
export function setAdd(file: string, index: number, row: Row): void {
store[file].adds[index] = row;
touch(file);
}
export function removeAdd(file: string, index: number): void {
store[file].adds.splice(index, 1);
touch(file);
}
export function toggleDelete(file: string, version: string, key: string): void {
const d = slot(file, version);
d.deletes = d.deletes.includes(key) ? d.deletes.filter((k) => k !== key) : [...d.deletes, key];
touch(file);
}
/* --- 표형 파일 --- */
export function setTable(
file: string,
version: string,
key: string,
base: Row,
items: TableItem[],
orig: Row[],
): void {
const d = slot(file, version);
if (items.length === orig.length && items.every((it, i) => it.o === i && same(it.r, orig[i]))) {
delete d.tables[key];
} else {
d.tables[key] = { base, items };
}
touch(file);
}
export const tableItems = (file: string, key: string): TableItem[] | undefined =>
store[file]?.tables[key]?.items;
/* --- 저장·버리기 --- */
export function payload(): FileChanges[] {
return Object.entries(store).map(([file, d]) => ({
file,
version: d.version,
changes: [
...Object.entries(d.edits)
.filter(([key]) => !d.deletes.includes(key))
.map(([key, row]) => ({ op: "edit" as const, key, row })),
...Object.entries(d.tables).map(([key, t]) => ({
op: "edit" as const,
key,
row: { ...t.base, : t.items.map((it) => it.r) },
})),
...d.adds.map((row) => ({ op: "add" as const, row })),
...d.deletes.map((key) => ({ op: "delete" as const, key })),
],
}));
}
/** files 를 안 주면 전부 버림. */
export function discard(files?: string[]): void {
for (const file of files ?? Object.keys(store)) delete store[file];
touch("");
}