/* ============================================================================= * line_stack_rows.ts * 줄 쌓기 공식 묶음(PLAN 804-2 · 요구 675) — 「이름 = 값」 · 「이름 = 식」 줄 묶음 · 틀 이름표 · [식으로 풀기] · [틀로 저장] * 접힘 = 값 줄은 값 칸(연결 값을 고쳐 적으면 숫자 = 고정값 · 요구 664) · 식 줄은 「이름 = 식 = 값」 글 · 단가산출 결과 기호 = [식으로 풀기] * 풀림 = 줄마다 이름 · 식 · 단위 칸 + 지움 · [+ 줄] — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결 * 틀 = 단가산출 결과 문서(굴착기 Q 등)를 이 문서 식으로 옮김(`unfold`) · 내 틀 = 이 브라우저 저장(localStorage) — 꼴이 바뀌면 * 「내 틀 (틀 이름 에서 변경)」 · 빈 틀 = [찾아 넣기](돈 아닌 단가산출 결과 — 작업량 등) * 표 값(804-3) = [표 · 칸] 단추 → 표 고르기 창(`line_stack_pick.ts`) · 값 칸 누름 = 그 줄 · 칸(`pickCell`) * ========================================================================== */ import { createButton } from "@ui/ui_template_button"; import { eqText } from "@ui/ui_template_equation"; import { askText, el, showToast } from "@ui/ui_template_elements"; import { createInputField } from "@ui/ui_template_input"; import { DocError, rename, splitId, varId, type FormulaDoc, } from "../common_util/common_util_formula_doc"; import { isNum } from "../common_util/common_util_formula_doc_calc"; import { dropSyms, freshId, newVar, setRow, shapeOf, unfold, type StackDoc, type StackItem, type StackLine, } from "../common_util/common_util_line_stack"; import { boundConds, currentRow, pickCell } from "../common_util/common_util_line_stack_pick"; import { openFindPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Find_Pick"; import type { EditHooks } from "./line_stack_edit"; import { openTablePick } from "./line_stack_pick"; import { lt } from "./line_stack_text"; /** 내 틀 — 기호 · 식 조각(이 브라우저) */ export interface Tpl { id: string; name: string; 문서: FormulaDoc; } const clone = (v: T): T => JSON.parse(JSON.stringify(v)) as T; type Block = Extract; /** 틀 넣기 — 새 공식 묶음 줄 */ export function insertTpl(d: StackDoc, t: Tpl): Block { const moved = unfold(d, t.문서); return { 꼴: "공식", 기호: moved, 틀: `my:${t.id}`, 틀꼴: shapeOf(d, moved) }; } const LINK_TAG: Record = { 표: "표", 요소: "요소", 단가산출: "단가산출", 선택: "선택", 입력: "입력", }; const input = ( value: string, placeholder: string, cls: string, set: (t: string) => void, ): HTMLElement => { const f = createInputField({ value, placeholder, compact: true, size: 24 }); f.root.classList.add(cls); f.input.addEventListener("change", () => set(f.input.value)); return f.root; }; export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTMLElement[] { const d = h.doc; const at = `${i}.${j}`; const open = h.open.has(at); const vals = (h.answer.값 ?? {}) as Record; const vars = new Map((d.변수 ?? []).map((v) => [varId(v), v])); const lines = new Map((d.식 ?? []).map((l) => [String(l.기호 ?? ""), String(l.식 ?? "")])); const blockOf = (items: StackItem[]): Block => items[i].줄[j] as Block; const valText = (id: string): string => { const v = vals[id]; if (isNum(v) || typeof v === "string") return `${eqText(v)} ${vars.get(id)?.단위 ?? ""}`.trim(); if (v && typeof v === "object") return ["노무비", "재료비", "경비"] .map((c) => `${c} ${eqText((v as Record)[c])}`) .join(" · "); return `— ${h.answer.빈값?.[id] ?? ""}`.trim(); }; // ── 이름표 ── const tplName = (k: string): string => k.startsWith("my:") ? (h.templates().find((t) => `my:${t.id}` === k)?.name ?? lt("My_Tpl")) : (h.subDoc(k)?.이름 ?? (h.name(k) || k)); const changed = !!line.틀 && !!line.틀꼴 && shapeOf(d, line.기호) !== line.틀꼴; const label = !line.틀 ? lt("K_공식") : changed ? `${lt("My_Tpl")} (${tplName(line.틀)} ${lt("Changed_From")})` : tplName(line.틀); const head = el("div", { className: "lstk-fhead", children: [ el("span", { className: "lstk-kind", text: label }), createButton({ label: open ? lt("Fold") : lt("Unfold"), variant: "ghost", size: 24, onClick: () => { if (open) h.open.delete(at); else h.open.add(at); h.change(() => undefined); }, }), createButton({ label: lt("Save_Tpl"), variant: "ghost", size: 24, onClick: () => void askText({ message: lt("Tpl_Name"), value: label }).then((name) => { if (!name?.trim()) return; const ids = new Set(line.기호); const id = `t${Date.now()}`; h.saveTpl({ id, name: name.trim(), 문서: { 변수: clone((d.변수 ?? []).filter((v) => ids.has(varId(v)))), 식: clone((d.식 ?? []).filter((l) => ids.has(String(l.기호 ?? "")))), }, }); h.change((doc, items) => Object.assign(blockOf(items), { 틀: `my:${id}`, 틀꼴: shapeOf(doc, line.기호) }), ); }), }), ], }); head.dataset.tpl = changed ? "changed" : line.틀 ? "tpl" : ""; // ── 표 값 — [표 · 칸] → 표 고르기 ── const tableBtn = (id: string, link: NonNullable[number]["연결"] & object) => { const b = createButton({ label: [ lt("Tbl"), ...Object.values(link.조건 ?? {}).flatMap((t) => /^'.*'$/.test(String(t)) ? [String(t).slice(1, -1)] : [], ), link.칸, ] .filter(Boolean) .join(" · ") + " ▾", // 고른 줄(글 조건) · 칸 variant: "ghost", size: 24, onClick: () => void h.table(String(link.표 ?? "")).then((t) => { if (!t) return void showToast(lt("Tbl_Missing"), "warning"); openTablePick({ table: t, now: currentRow(t, link, vals), col: String(link.칸 ?? ""), bound: boundConds(d, link), done: (row, col) => h.change((doc) => pickCell(doc, id, t, row, col)), }); }), }); b.dataset.tablePick = id; b.title = h.name(String(link.표 ?? "")) || String(link.표 ?? ""); return b; }; // ── 줄 ── const renameRow = (old: string, now: string): void => { const t = now.trim(); if (!t || t === old) return; try { const copy = clone(d); const [s, lo, up] = splitId(t); rename(copy, old, s, lo, up); } catch (e) { if (e instanceof DocError) return void showToast(lt("Bad_Name"), "warning"); throw e; } h.change((doc, items) => { const [s, lo, up] = splitId(t); rename(doc, old, s, lo, up); for (const it of items) for (const l of it.줄) if (l.꼴 === "공식") l.기호 = l.기호.map((x) => (x === old ? t : x)); }); }; const unfoldRow = (id: string, sub: FormulaDoc, key: string): void => h.change((doc, items) => { const moved = unfold(doc, sub, id); const blk = blockOf(items); blk.기호 = blk.기호.filter((x) => x !== id); items[i].줄.splice(j + 1, 0, { 꼴: "공식", 기호: moved, 틀: key, 틀꼴: shapeOf(doc, moved) }); if (!blk.기호.length) items[i].줄.splice(j, 1); }); const rows = line.기호.map((id) => { const v = vars.get(id); const link = v?.연결; const expr = lines.get(id); const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null; const tag = link?.갈래 === "표" && !expr ? tableBtn(id, link) : link && LINK_TAG[link.갈래 ?? ""] && !expr ? el("span", { className: "lstk-tag", text: `${LINK_TAG[link.갈래!]}${link.키 && !link.키.includes("{") ? ` · ${sub?.이름 ?? h.name(link.키) ?? ""}` : ""}`, }) : null; const tools: HTMLElement[] = []; if (sub?.결과 && link?.키) tools.push( createButton({ label: lt("Unfold"), variant: "ghost", size: 24, onClick: () => unfoldRow(id, sub, link.키!), }), ); if (link?.갈래 === "단가산출" && !link.키) tools.push( createButton({ label: lt("Find_Title"), variant: "ghost", size: 24, onClick: () => openFindPicker({ layer: "system", owner: null, title: lt("K_틀"), kinds: ["logic"], results: true, logic: (x) => h.change((doc) => { const y = doc.변수?.find((z) => varId(z) === id); if (y) y.연결 = { 갈래: "단가산출", 키: x.키, 입력: {} }; }), }), }), ); const name = el("b", { className: "lstk-sym", text: id }); const valueOf = expr ?? (link?.갈래 === "숫자" ? String(link.값 ?? "") : isNum(vals[id]) ? String(vals[id]) : ""); let body: (HTMLElement | string)[]; if (open) body = [ input(id, lt("Row_Name"), "lstk-f-s", (t) => renameRow(id, t)), "=", input(valueOf, lt("Row_Expr"), "lstk-f-l", (t) => h.change((doc) => setRow(doc, id, t))), input(v?.단위 ?? "", lt("Unit"), "lstk-f-s", (t) => h.change((doc) => { let x = doc.변수?.find((y) => varId(y) === id); if (!x) (doc.변수 ??= []).push((x = newVar(id, { 갈래: "식" }))); x.단위 = t.trim(); }), ), el("span", { className: "lstk-val", text: `= ${valText(id)}` }), createButton({ label: "×", variant: "danger-ghost", size: 24, onClick: () => h.change((doc) => dropSyms(doc, [id])), }), ]; else if (expr !== undefined || sub || link?.갈래 === "단가산출") body = [ name, `= ${expr ?? ""}${expr ? " =" : ""}`, el("span", { className: "lstk-val", text: valText(id) }), ]; else body = [ name, "=", input(valueOf, valText(id), "lstk-f-m", (t) => h.change((doc) => setRow(doc, id, t))), el("span", { className: "lstk-unit", text: v?.단위 ?? "" }), ]; const row = el("div", { className: `lstk-frow ${open ? "is-open" : ""}`, children: [...body, ...(tag ? [tag] : []), ...tools], }); row.dataset.sym = id; return row; }); const more = open ? [ createButton({ label: lt("Add_Row"), variant: "ghost", size: 24, onClick: () => h.change((doc, items) => { const id = freshId(doc, "x"); (doc.변수 ??= []).push(newVar(id, { 갈래: "숫자", 값: 0 })); blockOf(items).기호.push(id); }), }), ] : []; return [head, ...rows, ...more]; }