- 새 M02_Sangeun_Const.py — 식 n 번째 숫자 · 표 찾기 덮개(엔진 파서 차례 · 식 구조 그대로라 옵션 따라감) · 글줄 · 까닭 꼴 검사 · 저장 때 M01판(자리마다 지문) · 셈 답 덧칸 원래 {노 · 재 · 경 · 계} · 시트 {자리 · 글줄 · 짝잃음}
- Doc.patched 첫머리 · clean 끝 · Calc.clean_doc · calc_doc 한 줄씩 끼움(줄 수 그대로) — 화면 · 저장 원값 · B09 · 펼침 겹침 한 벌
- Library_Flat — 상수 · 표값 덮개 SK 는 안 펼침(참조 그대로 · 값 그대로) · [초기화]가 새 덮개 · 까닭도 걷음
- 새 logic/M02_Sangeun_Const_Model.ts — 캐시 고치기(setConst · setTable · 글줄 · setReason · restoreM01 · dropAddKeys · coverKinds) · 파이썬 짝 거울 시험
- 새 test_790_2_sangeun_const.py — 견본 금액 = _분석_650-H2(61,088 · 51,802 · 38,696 · 35,764) · 옛 SK 꼴 · 금액 그대로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
7.2 KiB
TypeScript
193 lines
7.2 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete/logic/M02_Sangeun_Const_Model.ts
|
|
* 단가산출(SK) 숫자 넓힘 캐시 고치기(PLAN 790-2 · 요구서_652 · _분석_650-H3 E2) —
|
|
* 짝 = 서버 `M02_MasterTemplete/M02_Sangeun_Const.py`(`restore` · `drop_add_keys` · `kinds` 같은 꼴)
|
|
* · 거울 시험 `resources/tester/test_790_2_sangeun_const.py`.
|
|
* 상수덮개 {자리: {n: 값}} 식 n 번째 숫자(0 부터 · 서버 답 `시트.자리[자리].상수` 의 n)
|
|
* 표값덮개 {자리: {n: 값}} 식 n 번째 `찾기(…).칸`(서버 답 `표값` 의 n)
|
|
* 글줄 [{자리, 글}] 셈 없는 글 줄 · 자리 뒤("" = 맨 위)
|
|
* 까닭 {열쇠: 글} 덮개 · 더한줄 · 뺀줄 · 새 덮개마다
|
|
* M01판 서버 몫 — 받은 그대로 둠(셈 때 「M01바뀜」 판정)
|
|
* 자리 = 바탕 cid · 중간 `mid:이름` · 열쇠 = 자리 · `add:#k` · `add:#k/세부 cid`.
|
|
* 금액은 늘 서버가 셈 — 여기는 문서 칸 고치기만(새 문서를 돌려줌 · 받은 문서 안 바꿈).
|
|
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
|
* ========================================================================== */
|
|
|
|
export type NumCovers = Record<string, Record<string, number>>;
|
|
|
|
export interface TextLine {
|
|
자리: string;
|
|
글: string;
|
|
}
|
|
|
|
export interface ConstDoc {
|
|
덮개?: Record<string, Record<string, unknown>>;
|
|
뺀줄?: string[];
|
|
더한줄?: Record<string, unknown>[];
|
|
상수덮개?: NumCovers;
|
|
표값덮개?: NumCovers;
|
|
글줄?: TextLine[];
|
|
까닭?: Record<string, string>;
|
|
[k: string]: unknown;
|
|
}
|
|
|
|
export type CoverKind = "품" | "단가" | "상수" | "표값" | "뺌";
|
|
|
|
const ADD = /^add:#(\d+)(\/.*)?$/;
|
|
const PARTS = ["labor", "material", "expense"];
|
|
|
|
/** 빈 묶음 · 목록 칸은 지움(서버 `clean_more` 처럼 없으면 안 둠). */
|
|
function put<T extends ConstDoc>(doc: T, name: string, value: unknown): T {
|
|
const out: ConstDoc = { ...doc };
|
|
const empty =
|
|
value == null ||
|
|
(Array.isArray(value)
|
|
? value.length === 0
|
|
: typeof value === "object" && Object.keys(value).length === 0);
|
|
if (empty) delete out[name];
|
|
else out[name] = value;
|
|
return out as T;
|
|
}
|
|
|
|
function setNum<T extends ConstDoc>(
|
|
doc: T,
|
|
name: "상수덮개" | "표값덮개",
|
|
spot: string,
|
|
n: number,
|
|
v: number | null,
|
|
): T {
|
|
const all: NumCovers = { ...(doc[name] ?? {}) };
|
|
const one = { ...(all[spot] ?? {}) };
|
|
if (v == null) delete one[String(n)];
|
|
else one[String(n)] = v;
|
|
if (Object.keys(one).length) all[spot] = one;
|
|
else delete all[spot];
|
|
return put(doc, name, all);
|
|
}
|
|
|
|
/** 식 속 n 번째 숫자 상수 → 값(null = 원래로). */
|
|
export const setConst = <T extends ConstDoc>(
|
|
doc: T,
|
|
spot: string,
|
|
n: number,
|
|
v: number | null,
|
|
): T => setNum(doc, "상수덮개", spot, n, v);
|
|
|
|
/** 식 속 n 번째 표 찾기 값 → 값(null = 표 연결 되살림). */
|
|
export const setTable = <T extends ConstDoc>(
|
|
doc: T,
|
|
spot: string,
|
|
n: number,
|
|
v: number | null,
|
|
): T => setNum(doc, "표값덮개", spot, n, v);
|
|
|
|
/** 글 줄 넣기 — `at` 차례(없으면 끝). */
|
|
export function addText<T extends ConstDoc>(doc: T, spot: string, text: string, at?: number): T {
|
|
const lines = [...(doc.글줄 ?? [])];
|
|
lines.splice(at ?? lines.length, 0, { 자리: spot, 글: text });
|
|
return put(doc, "글줄", lines);
|
|
}
|
|
|
|
export function editText<T extends ConstDoc>(doc: T, at: number, text: string): T {
|
|
const lines = (doc.글줄 ?? []).map((t, i) => (i === at ? { ...t, 글: text } : t));
|
|
return put(doc, "글줄", lines);
|
|
}
|
|
|
|
export function dropText<T extends ConstDoc>(doc: T, at: number): T {
|
|
return put(
|
|
doc,
|
|
"글줄",
|
|
(doc.글줄 ?? []).filter((_, i) => i !== at),
|
|
);
|
|
}
|
|
|
|
/** 까닭 — 빈 글 = 지움. */
|
|
export function setReason<T extends ConstDoc>(doc: T, key: string, text: string): T {
|
|
const why = { ...(doc.까닭 ?? {}) };
|
|
if (text.trim()) why[key] = text.trim();
|
|
else delete why[key];
|
|
return put(doc, "까닭", why);
|
|
}
|
|
|
|
const topAdd = (key: string): boolean => {
|
|
const hit = ADD.exec(key);
|
|
return !!hit && !hit[2];
|
|
};
|
|
|
|
/** [M01 원래로] — 열쇠 없음 = 전체(덮개 · 뺀줄 · 상수 · 표값 덮개 · 그 까닭 · 더한줄 · 글줄 · 옵션 남김) ·
|
|
* 열쇠 = 그 자리 덮개 · 뺀줄 · 새 덮개 · 까닭(`add:#k/cid` = 그 더한줄 세부) · 짝 `restore`. */
|
|
export function restoreM01<T extends ConstDoc>(doc: T, key?: string): T {
|
|
let out: ConstDoc = { ...doc };
|
|
delete out.상수덮개;
|
|
delete out.표값덮개;
|
|
delete out.까닭;
|
|
const why = { ...(doc.까닭 ?? {}) };
|
|
if (key == null) {
|
|
out = { ...out, 덮개: {}, 뺀줄: [] };
|
|
const kept = Object.fromEntries(Object.entries(why).filter(([k]) => topAdd(k)));
|
|
return put(out, "까닭", kept) as T;
|
|
}
|
|
const hit = ADD.exec(key);
|
|
if (hit && hit[2]) {
|
|
const k = Number(hit[1]);
|
|
const sub = hit[2].slice(1);
|
|
const adds = (doc.더한줄 ?? []).map((x) => ({ ...x }));
|
|
if (k < adds.length) {
|
|
const was = adds[k];
|
|
const one: Record<string, unknown> = { ...was };
|
|
delete one.덮개;
|
|
delete one.뺀줄;
|
|
const covers = Object.fromEntries(
|
|
Object.entries((was.덮개 as Record<string, unknown>) ?? {}).filter(([c]) => c !== sub),
|
|
);
|
|
const drop = ((was.뺀줄 as string[]) ?? []).filter((c) => c !== sub);
|
|
if (Object.keys(covers).length) one.덮개 = covers;
|
|
if (drop.length) one.뺀줄 = drop;
|
|
adds[k] = one;
|
|
}
|
|
out.더한줄 = adds;
|
|
} else {
|
|
out.덮개 = Object.fromEntries(Object.entries(doc.덮개 ?? {}).filter(([c]) => c !== key));
|
|
out.뺀줄 = (doc.뺀줄 ?? []).filter((c) => c !== key);
|
|
for (const name of ["상수덮개", "표값덮개"] as const) {
|
|
const got = Object.fromEntries(Object.entries(doc[name] ?? {}).filter(([s]) => s !== key));
|
|
out = put(out, name, got);
|
|
}
|
|
}
|
|
delete why[key];
|
|
return put(out, "까닭", why) as T;
|
|
}
|
|
|
|
/** 더한줄 k 를 뺀 뒤 — 까닭 · 글줄 열쇠 `add:#j`(j > k) 한 칸 당김 · k 는 지움 · 짝 `drop_add_keys`. */
|
|
export function dropAddKeys<T extends ConstDoc>(doc: T, k: number): T {
|
|
const move = (key: string): string | null => {
|
|
const hit = ADD.exec(key);
|
|
if (!hit) return key;
|
|
const j = Number(hit[1]);
|
|
return j === k ? null : `add:#${j > k ? j - 1 : j}${hit[2] ?? ""}`;
|
|
};
|
|
let out: ConstDoc = { ...doc };
|
|
if (doc.까닭) {
|
|
const why: Record<string, string> = {};
|
|
for (const [x, v] of Object.entries(doc.까닭)) {
|
|
const to = move(x);
|
|
if (to) why[to] = v;
|
|
}
|
|
out = put(out, "까닭", why);
|
|
}
|
|
if (doc.글줄) out.글줄 = doc.글줄.map((t) => ({ ...t, 자리: move(t.자리) ?? "" }));
|
|
return out as T;
|
|
}
|
|
|
|
/** 자리의 덮개 갈래(차례 = 품 · 단가 · 상수 · 표값 · 뺌) · 짝 `kinds`. */
|
|
export function coverKinds(doc: ConstDoc, spot: string): CoverKind[] {
|
|
const cover = doc.덮개?.[spot] ?? {};
|
|
const out: CoverKind[] = [];
|
|
if ("qty" in cover || "배율" in cover) out.push("품");
|
|
if (PARTS.some((k) => k in cover)) out.push("단가");
|
|
if (doc.상수덮개?.[spot]) out.push("상수");
|
|
if (doc.표값덮개?.[spot]) out.push("표값");
|
|
if ((doc.뺀줄 ?? []).includes(spot)) out.push("뺌");
|
|
return out;
|
|
}
|