/* ============================================================================= * formula_doc_sub.ts * 단가산출 연결 펼침(PLAN 802-3 · 802-4b-2 · 요구 659-3 · 796-2e 뜻) — 정의 영역에서 단가산출 기호를 열면 그 문서의 식 · 값 식 * M01 = 읽기만 · M02 사본(`copy`) = 머리 「연결」 + [연결 끊기](이 사본 안 자기 문서로 굳힘 · 금액 그대로) · * 「끊음」 + [다시 잇기](고친 것 버림 · 부름 문서로) · 끊은 문서 = 그 자리에서 식 줄 · 금액 줄 · 숫자 고침(`formula_doc_edit.ts`) * · 줄 편집 = 수식 영역과 같은 편집기(`formula_doc_math.ts` · 803-2a) · 맨 아래 계 줄 * 셈 = 같은 계산기 · 연결 캐시(끊은 문서는 캐시 문서를 덮어씀 — 부른 쪽이 `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 { editLit, editRow, ensureVar, sweep } from "./formula_doc_edit"; import { buildLines, sumRow, type RowRef } from "./formula_doc_lines"; import { openMath } from "./formula_doc_math"; 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; ref: RowRef; at?: { x: number; y: number }; seed?: string } | 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 editing = next?.key === k ? next : null; next = null; const box = el("div", { className: "fdoc__grid fdoc__sub" }); const edit = (ref: RowRef | "end", at?: { x: number; y: number }): void => { if (ref !== "end") { next = { key: k, ref, at }; return put(() => undefined); } const money = (sub.금액 ?? []).length; next = { key: k, ref: { kind: money ? "money" : "line", i: money || (sub.식 ?? []).length } }; put((d) => { const last = d.금액?.[d.금액.length - 1]; if (last) { d.금액!.push({ 이름: "", 비목: last.비목 ?? "노무비", 단가: "", ...(last.묶음 ? { 묶음: last.묶음 } : {}), }); next!.seed = `${last.비목 ?? "노무비"} `; } else (d.식 ??= []).push({ 기호: "", 식: "" }); }); }; const mount = (host: HTMLElement, ref: RowRef, text: string): void => { const first = editing?.seed ?? text; queueMicrotask(() => openMath(host, { text: first, at: editing?.at ?? "end", onLeave: (now) => put((d) => { if (now !== first) editRow(d, ref.kind, ref.i, now); sweep(d); }), }), ); }; box.append( ...buildLines({ admin: live, doc: sub, answer: got, vars: new Map((sub.변수 ?? []).map((v) => [varId(v), v])), marks: new Map(), editing: editing?.ref ?? null, mount, onEditAt: live ? edit : 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 })); }); }, onGroupName: none, onRedraw: () => put((d) => sweep(d)), }).cells, ); if ((sub.금액 ?? []).length) box.append(...sumRow(ft("Sum"), got.셈됨 ? (got as unknown as Record) : null)); 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] }); }