Files
Aislo/common_util/common_util_formula_doc_view.ts
T
eomsangdonandClaude Opus 5.5 c80b5ae9d0 feat(802-4b-2): M02 단가산출 수식 탭 = 공용 수식 문서 화면 · 꺼진 줄 = 계산 변수 거침
- M02 SK 「수식」 탭 = M01 과 같은 공용 화면(`createDocScreen` · 길 앞머리만 `/m02/layers/{층}/sangeun/{키}/doc`) — 옛 M02 수식 나무 화면(`_Sangeun_Equation.ts` · 796-2b · 2c · 2e · 800 · 801 꼴) 걷음
  · 옵션 = SK 문서 값(저장) · [단가 갱신] · [재생성](미리 셈 → 알림 계 전 → 후 · 버리는 것 → 저장) · 「M01 새 판 있음」 배지(`formula_doc_copy.ts`)
  · 단가산출 연결 펼침 머리 「연결 / 끊음」 · [연결 끊기] · [다시 잇기] · 끊은 문서 안 줄 그 자리 고침(`formula_doc_sub.ts`) · 저장 몸 {문서, 끊음, 판}
  · 층 쓰기 권한 없음 = 읽기만(옵션 잠금 · 단추 없음 · 「읽기만」) · 판 바뀜 409 = 사본 같으면 새 판으로 한 번 더 · 저장 뒤 페이지 판 · 상세 탭 `수식` 칸 새로
  · 저장 안 한 SK(일위대가 줄 모달) = 「저장한 단가산출만」 글
- 서버 — 수식 길 답에 `연결`(화면 캐시 기록 · `M02_Doc_Screen.record`) · 연결 후보 · 표 길(층 읽기 · M01 후보 · 표 그대로 · main 등록)
- 꺼진 줄(요구 663-1 마저) — 곱 인수 하나가 선택으로 꺼진 0(만약 0 갈래 · 식 줄 기호면 그 식으로 거침)이면 숨김 · 입력 · 글 0 은 그대로
  · 판정을 순수 파일로(`common_util_formula_doc_view.ts` · Node 로 시험) · 1,518 문서 기본 옵션 꺼짐 158 → 203 줄(꺼진 줄 금액 모두 0) · GC000211 폼타이 아님 = 세 규격 줄 모두
- 화면 파일 700 줄 — 고침 함수 · 옵션 가로 · 연결 펼침을 새 파일로(`formula_doc_edit` · `_options` · `_sub`)
- 시험 test_802_4b2(5 · 헬퍼 helper_802_4b2_off.cjs) · 옛 시험 둘 고침 — test_793_3b · test_796_2b 가 걷은 옛 M02 수식 화면 글을 읽음 → 없음 확인으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:20:52 +09:00

140 lines
5.2 KiB
TypeScript

/* =============================================================================
* common_util_formula_doc_view.ts
* 수식 문서 그림용 값(PLAN 802-3 · 802-4b-2 · 요구 663-1) — 화면 · DOM · fetch 없음(거울 시험이 Node 로 돌림)
* tryParse · evalNode(탄 갈래 값) · takenOf · leafNum · offZero / offLine(선택으로 꺼진 줄)
* 셈 정본은 계산기(`common_util_formula_doc_calc.ts`) — 여기는 그림 · 숨김 판정만
* ========================================================================== */
import { DocError, parse, type Node } from "./common_util_formula_doc";
import { binary, cut, isNum } from "./common_util_formula_doc_calc";
export type Values = Record<string, unknown>;
/** 식 글 → 나무(못 풀면 null) */
export function tryParse(text: unknown): Node | null {
if (text === null || text === undefined || String(text).trim() === "") return null;
try {
return parse(text);
} catch (e) {
if (e instanceof DocError) return null;
throw e;
}
}
/** 그림용 값 — 모르면 undefined */
export function evalNode(n: Node, vals: Values): unknown {
const ev = (x: Node): unknown => evalNode(x, vals);
try {
switch (n[0]) {
case "num":
case "str":
return n[1];
case "var": {
const v = vals[n[1]];
return v === null ? undefined : v;
}
case "field": {
const b = ev(n[1]);
return b && typeof b === "object" ? (b as Values)[n[2]] : undefined;
}
case "neg": {
const a = ev(n[1]);
return isNum(a) ? -a : undefined;
}
case "pct": {
const a = ev(n[1]);
return isNum(a) ? a / 100 : undefined;
}
case "bin": {
const a = ev(n[2]);
if (a === undefined) return undefined;
if (n[1] === "그리고" && !a) return a;
if (n[1] === "또는" && a) return a;
const b = ev(n[3]);
if (b === undefined) return undefined;
if (n[1] === "그리고" || n[1] === "또는") return b;
const got = binary(n[1], a, b);
return typeof got === "number" && !Number.isFinite(got) ? undefined : got;
}
case "call": {
const [, name, items] = n;
if (name === "만약") {
const t = items.length === 3 ? ev(items[0]) : undefined;
return t === undefined ? undefined : ev(t ? items[1] : items[2]);
}
const xs = items.map(ev);
if (!xs.every(isNum)) return undefined;
if (name === "최소") return Math.min(...(xs as number[]));
if (name === "최대") return Math.max(...(xs as number[]));
return cut(xs[0] as number, { 방법: name, 자리: Math.trunc((xs[1] as number) ?? 0) });
}
}
} catch {
return undefined;
}
}
/** 만약 사슬이 탄 끝 갈래가 수 하나면 그 수(만약이 아니거나 모르면 undefined) */
export function leafNum(n: Node, vals: Values): number | undefined {
let at = n;
let hop = 0;
for (let pick = takenOf(at, vals); pick; pick = takenOf(at, vals), hop++)
at = (at as ["call", string, Node[]])[2][pick];
return hop && at[0] === "num" ? at[1] : undefined;
}
/** 선택으로 꺼진 0 — 만약이 탄 끝 갈래가 0 · 곱 인수(× · ÷ 위) 하나가 꺼진 0 · 식 줄 기호면 그 줄 식으로 거침
* (663-1 · 802-4b-2) — 입력 · 숫자 0(구멍땜 0 개소) · 글로 적은 0 은 꺼진 것 아님 */
export function offZero(
n: Node | null,
vals: Values,
lines: ReadonlyMap<string, unknown> = new Map(),
path: ReadonlySet<string> = new Set(),
): boolean {
if (!n) return false;
let at = n;
let hop = 0;
for (let pick = takenOf(at, vals); pick; pick = takenOf(at, vals), hop++)
at = (at as ["call", string, Node[]])[2][pick];
if (hop && at[0] === "num") return at[1] === 0;
const off = (x: Node): boolean => offZero(x, vals, lines, path);
switch (at[0]) {
case "var":
return (
lines.has(at[1]) &&
!path.has(at[1]) &&
offZero(tryParse(lines.get(at[1])), vals, lines, new Set([...path, at[1]]))
);
case "neg":
case "pct":
return off(at[1]);
case "bin":
if (at[1] === "*") return off(at[2]) || off(at[3]);
if (at[1] === "/") return off(at[2]);
if (at[1] === "+" || at[1] === "-") return off(at[2]) && off(at[3]);
return false;
default:
return false;
}
}
/** 식 줄 표 {기호: 식} — `offLine` 이 계산 변수를 거칠 때 */
export const lineMap = (doc: { 식?: { 기호?: unknown; 식?: unknown }[] }): Map<string, unknown> =>
new Map((doc.식 ?? []).map((l) => [String(l.기호 ?? ""), l.식]));
/** 선택으로 꺼진 금액 줄 — 수량 · 단가가 선택으로 꺼진 0(`offZero` · 고침 모드에서만 보임) */
export function offLine(
line: { 수량?: unknown; 단가?: unknown },
vals: Values,
lines: ReadonlyMap<string, unknown> = new Map(),
): boolean {
return [line.수량, line.단가].some((t) => offZero(tryParse(t), vals, lines));
}
/** 만약 — 탄 갈래 차례(1 참 · 2 거짓 · 0 모름) */
export function takenOf(n: Node, vals: Values): 0 | 1 | 2 {
if (n[0] !== "call" || n[1] !== "만약" || n[2].length !== 3) return 0;
const t = evalNode(n[2][0], vals);
return t === undefined ? 0 : t ? 1 : 2;
}