Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
162 lines
4.7 KiB
TypeScript
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("");
|
|
}
|