/* ============================================================================= * formula_doc_sub.ts * 단가산출 연결 펼침(PLAN 802-3 · 802-4b-2 · 요구 659-3 · 796-2e 뜻) — 정의 영역에서 단가산출 기호를 열면 그 문서의 식 · 값 식 * M01 = 읽기만 · M02 사본(`copy`) = 머리 「연결」 + [연결 끊기](이 사본 안 자기 문서로 굳힘 · 금액 그대로) · * 「끊음」 + [다시 잇기](고친 것 버림 · 부름 문서로) · 끊은 문서 = 그 자리에서 식 줄 · 금액 줄 · 숫자 고침(`formula_doc_edit.ts`) * 셈 = 같은 계산기 · 연결 캐시(끊은 문서는 캐시 문서를 덮어씀 — 부른 쪽이 `merge`) * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { createButton } from "@ui/ui_template_button"; import { eqText } from "@ui/ui_template_equation"; import { editNumber } from "@ui/ui_template_equation_edit"; import { el } from "@ui/ui_template_elements"; import { varId, type FormulaDoc, type Link } from "../common_util/common_util_formula_doc"; import { calcDoc, type DocAnswer } from "../common_util/common_util_formula_doc_calc"; import type { DocCache } from "../common_util/common_util_formula_doc_cache"; import { editLine, editLit, editMoney, ensureVar, tidyText } from "./formula_doc_edit"; import { buildLines, type EditAt } from "./formula_doc_lines"; import { evalNode, tryParse } from "./formula_doc_render"; import { ft } from "./formula_doc_text"; export interface SubCopy { /** 층 쓰기 권한 */ edit: boolean; /** 이 사본이 부르는 키(부름 · 끊음) */ has: (key: string) => boolean; /** 끊은 문서(없으면 연결) */ cut: (key: string) => FormulaDoc | undefined; onCut: (key: string) => void; onJoin: (key: string) => void; /** 끊은 문서 고침 — 캐시 · 셈 · 그림까지 부른 쪽이 */ change: (key: string, fn: (d: FormulaDoc) => void) => void; } export interface SubHooks { doc: FormulaDoc | null; answer: DocAnswer | null; cache: DocCache; copy?: SubCopy; } let next: { key: string; at: EditAt } | null = null; // 끊은 문서에 새 줄 — 다음 그림에서 바로 고침 const subDoc = (cache: DocCache, k: string): FormulaDoc | null => { try { return cache.links.doc(k) ?? null; } catch { cache.takeMissing(); return null; } }; function linkHead(k: string, c: SubCopy): HTMLElement | null { if (!c.has(k)) return null; const cut = !!c.cut(k); const tools = c.edit ? [ createButton({ label: cut ? ft("Relink") : ft("Unlink"), variant: "ghost", size: 24, onClick: () => (cut ? c.onJoin(k) : c.onCut(k)), }), ] : []; const head = el("div", { className: "fdoc__sublink", attrs: { "data-sub-link": cut ? "cut" : "linked" }, children: [createBadge(cut ? ft("Cut") : ft("Linked"), cut ? "warning" : "info"), ...tools], }); return head; } /** 단가산출 연결 펼침 — 그 문서의 식 · 값 식(끊은 사본 문서면 고침) */ export function subBlock(id: string, h: SubHooks): HTMLElement | null { const link: Link | undefined = h.doc?.변수?.find((v) => varId(v) === id)?.연결; if (link?.갈래 !== "단가산출" || !link.키 || link.키.includes("{")) return null; const k = link.키; const sub = subDoc(h.cache, k); if (!sub) return null; const inputs: Record = {}; for (const [name, t] of Object.entries(link.입력 ?? {})) { const n = tryParse(t); const v = n ? evalNode(n, h.answer?.값 ?? {}) : undefined; if (v !== undefined) inputs[name] = v; } const got = calcDoc(sub, h.cache.links, inputs); h.cache.takeMissing(); const c = h.copy; const live = !!(c?.edit && c.cut(k)); const put = (fn: (d: FormulaDoc) => void): void => c!.change(k, fn); const none = (): void => undefined; const auto = next?.key === k ? next.at : null; next = null; const box = el("div", { className: "fdoc__grid fdoc__sub" }); box.append( ...buildLines({ admin: live, doc: sub, answer: got, vars: new Map((sub.변수 ?? []).map((v) => [varId(v), v])), marks: new Map(), auto, onLine: live ? (i, t) => put((d) => editLine(d, i, t)) : none, onExpr: live ? (i, t) => put((d) => (d.식![i].식 = tidyText(t))) : none, onMoney: live ? (i, p) => put((d) => editMoney(d, i, p)) : none, onLit: live ? (w, i, at, v) => put((d) => editLit(d, w, i, at, v)) : none, onPickVar: (vid, num) => { const v = sub.변수?.find((x) => varId(x) === vid); if (!live || !num || v?.연결?.갈래 !== "숫자") return; editNumber(num, (t) => { const n = Number(t.replace(/,/g, "")); put((d) => (ensureVar(d, vid).연결 = { ...v.연결, 값: Number.isFinite(n) ? n : t })); }); }, onAddLine: () => { next = { key: k, at: { kind: "line", i: (sub.식 ?? []).length } }; put((d) => (d.식 ??= []).push({ 기호: "", 식: "" })); }, onAddMoney: (g) => { next = { key: k, at: { kind: "name", i: (sub.금액 ?? []).length } }; put((d) => (d.금액 ??= []).push({ 이름: "", 비목: "노무비", 단가: "", ...(g ? { 묶음: g } : {}) }), ); }, onGroupName: none, onRedraw: () => put((d) => { d.식 = (d.식 ?? []).filter((l) => String(l.기호 ?? "") || String(l.식 ?? "")); d.금액 = (d.금액 ?? []).filter((l) => l.이름 || l.단가 || l.수량); }), }), ); if (sub.결과) { const r = got.결과; box.append( el("div", { className: "fdoc__cell fdoc__total", text: ft("Result") }), el("div", { className: "fdoc__cell fdoc__total", text: typeof r === "number" ? `${eqText(r)} ${sub.결과단위 ?? ""}` : "—", }), ); } const head = c ? linkHead(k, c) : null; if (!head) return box; return el("div", { attrs: { "data-sub": k }, children: [head, box] }); }