/* ============================================================================= * ui_template_equation_sum.ts * 부품 「수식」 셋째 파일(799-1 · 796-2c · 800-4 · 801-3 · 요구 655-1 · 661 · 662-3 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로). * 묶음 = 부른 · 더한 단가산출 하나씩 + 직접 줄(인력 · 재료 · 기계 · 덧줄) 한 묶음 — 값 = 서버 `소계`(M01 `master_equation.subtotals` · M02 `M02_Sangeun_Equation_Full` · 꼴 = `master_equation_group`) · 소계가 없으면(결과 로직) 아무것도 안 그림 * 묶음 하나뿐 = 머리 없음 · 노무비 · 재료비 · 경비 줄 → 「계」 한 줄(끝수가 있으면 끝수 줄 먼저) * 묶음 둘 이상 = 묶음마다 「가. 이름」 머리 → 노무비 · 재료비 · 경비 줄 → 소 계 · 맨 끝 묶음 소계를 더한 식 → 끝수 → 합계(단가산출이 둘 이상) 아니면 계 * M02 SK = 상태 알약(더함 · 덮음) · 원래 금액 · 더한 기초는 접힘 수식 블록(더한 기초 하나뿐이어도 머리 줄은 둠 — 연결 끊기 단추 자리) · 합계 밖 줄은 글 한 줄 * 자리 = 하위 로직 블록 묶음은 그 블록 식 줄 바로 아래(`inline` · 줄 열쇠 = 블록 이름 · `createEqChain` 의 `after`) · 나머지(직접 줄 · 더한 기초) · 맨 끝 합계 · 끝수는 `tail` * 쓰는 법 = `createEqSum(tree, open?, hooks?, lineKeys?)` → { inline, tail } | null — `lineKeys` = 수식 영역 호표 줄 열쇠들 · `open` = 펼친 블록 이름표 모음 · * `hooks` = 페이지가 더한 기초 줄 머리 단추(`head`) · 접힘 속(`body`)을 바꿔 끼움(796-2e 연결 끊기) * ========================================================================== */ import { createBadge, type BadgeTone } from "./ui_template_badge"; import { createButton } from "./ui_template_button"; import { eqText, type EqSumHooks, type EqSumRow, type EqTree } from "./ui_template_equation"; import { createEqChain } from "./ui_template_equation_line"; const COLS = ["노무비", "재료비", "경비", "계"] as const; const PARTS = ["노무비", "재료비", "경비"] as const; type Col = (typeof COLS)[number]; const TONE: Record = { 더함: "success", 덮음: "warning", 뺌: "neutral" }; const ORDER = "가나다라마바사아자차카타파하"; /** 묶음 번호 — 가 · 나 · 다 … (열네 개를 넘으면 숫자) */ const mark = (at: number): string => (at < ORDER.length ? (ORDER[at] ?? "") : String(at + 1)); const node = (tag: "div" | "span", className: string, text = ""): HTMLElement => { const out = document.createElement(tag); out.className = className; if (text) out.textContent = text; return out; }; const num = (v: unknown): number => { const n = Number(v); return Number.isFinite(n) ? n : 0; }; function cells(line: HTMLElement, get: (k: Col) => unknown): void { for (const k of COLS) { const cell = node("span", "ui-eq__sum-cell", eqText(get(k)) || "0"); cell.dataset.eqSumCol = k; line.append(cell); } } function row(kind: string, name: string, get: (k: Col) => unknown): HTMLElement { const line = node("div", "ui-eq__sum-row"); line.dataset.eqSumKind = kind; line.append(node("span", "ui-eq__sum-name", name)); cells(line, get); return line; } /** 묶음 안 한 줄 「노무비 : 값」 */ const part = (label: string, text: string): HTMLElement => { const line = node("div", "ui-eq__grp-line"); line.append( node("span", "ui-eq__grp-key", label), node("span", "ui-eq__op", ":"), node("span", "ui-eq__grp-val", text), ); line.dataset.eqGrpPart = label; return line; }; /** 기준 단가산출 묶음 하나 — 「가. 이름」 머리(+ 상태 · 접힘 수식) → 노 · 재 · 경 줄 → 소 계 */ function group( r: EqSumRow, open: Set, at: number, heads: boolean, hooks?: EqSumHooks, ): HTMLElement { const box = node("div", "ui-eq__grp"); box.dataset.eqSumItem = r.갈래; box.dataset.eqSumState = r.상태 ?? "바탕"; const name = node("div", "ui-eq__grp-head"); const title = r.이름; const tree = r.나무; const id = `sum:${at}:${r.이름}`; const body = node("div", "ui-eq__sum-tree"); body.hidden = true; if (heads) name.append(node("span", "ui-eq__grp-mark", `${mark(at)}.`)); if (tree) { const head = createButton({ label: title, variant: "ghost", size: 22, iconStart: "chevronRight", onClick: () => toggle(), }); head.classList.add("ui-eq__block-head"); const toggle = (force?: boolean): void => { const on = force ?? !open.has(id); if (on) { open.add(id); if (!body.childElementCount) body.append( hooks?.body?.(r, at) ?? createEqChain(tree.줄, { path: id, open, resultUnit: tree.결과단위 }), ); } else open.delete(id); box.dataset.open = on ? "1" : ""; body.hidden = !on; }; name.append(head); if (open.has(id)) toggle(true); } else name.append(node("span", "ui-eq__sum-label", title)); if (r.상태 && r.상태 !== "바탕") { const detail = r.덮개?.length ? ` · ${r.덮개.join(" · ")}` : ""; name.append(createBadge(`${r.상태}${detail}`, TONE[r.상태] ?? "neutral", { size: 18 })); } name.append(...(hooks?.head?.(r, at) ?? [])); const notes: string[] = []; if (r.상태 === "더함" && r.수량 !== undefined && r.수량 !== null) notes.push(`× ${eqText(r.수량)}`); if (r.원래 !== undefined && r.원래 !== null && r.상태 !== "더함") notes.push(`원래 ${eqText(r.원래)}`); if (r.까닭) notes.push(r.까닭); if (notes.length) name.append(node("span", "ui-eq__sum-was", notes.join(" · "))); const shown = heads || !!tree || notes.length > 0 || (!!r.상태 && r.상태 !== "바탕"); if (shown) box.append(name, body); for (const k of PARTS) if (num(r[k]) !== 0) box.append(part(k, eqText(r[k]))); for (const x of r.낱줄 ?? []) { const was = x.원래 !== undefined && x.원래 !== null ? ` · 원래 ${eqText(x.원래)}` : ""; box.append(part(`${x.이름} · ${x.상태}`, `${eqText(x.계) || "0"}${was}`)); } if (heads) { const total = row("소계", "소 계", (k) => r[k]); total.classList.add("ui-eq__grp-total"); box.append(total); } return box; } /** 맨 끝 — 묶음 소계를 더한 식(노무비 : 2,299.4 + 5,162 = 7,461.4) */ function final(rows: EqSumRow[]): HTMLElement { const box = node("div", "ui-eq__grp"); box.dataset.eqSumItem = "합"; for (const k of PARTS) { const adds = rows.map((r) => num(r[k])).filter((v) => v !== 0); if (!adds.length) continue; const text = adds.map((v) => eqText(v)).join(" + "); const sum = eqText(adds.reduce((a, b) => a + b, 0)); box.append(part(k, adds.length > 1 ? `${text} = ${sum}` : text)); } return box; } export interface EqSumParts { /** 블록 이름 → 그 블록 식 줄 아래에 끼울 묶음 */ inline: Map; /** 수식 영역 맨 끝 — 남은 묶음 · 소 계 · 끝수 · 합계 */ tail: HTMLElement; } export function createEqSum( tree: Pick, open: Set = new Set(), hooks?: EqSumHooks, lineKeys: string[] = [], ): EqSumParts | null { const sub = tree.소계; if (!sub) return null; const inline = new Map(); const box = node("div", "ui-eq__sum"); box.dataset.eqSum = ""; const heads = sub.줄.length >= 2; // 묶음 하나뿐 = 머리 · 소계 없음 const word = sub.줄.filter((r) => r.갈래 === "블록").length >= 2 ? "합계" : "계"; // 합계 = 단가산출 둘 이상 sub.줄.forEach((r, at) => { const one = group(r, open, at, heads, hooks); if (r.갈래 === "블록" && lineKeys.includes(r.이름) && !inline.has(r.이름)) inline.set(r.이름, one); else box.append(one); }); if (heads) box.append(final(sub.줄)); const rest = sub.끝수; const odd = COLS.some((k) => Number(rest[k] ?? 0) !== 0); if (odd) box.append(row("끝수", "끝수", (k) => rest[k])); box.append(row(word, word, (k) => tree.합계[k])); for (const x of sub.밖 ?? []) box.append( node("div", "ui-eq__sum-note", `합계에 안 듦 · ${x.이름} ${eqText(x.밖금액) || "0"}`), ); const note = [odd ? sub.끝수까닭 : "", tree.답까닭].filter(Boolean).join(" · "); if (note) box.append(node("div", "ui-eq__sum-note", note)); return { inline, tail: box }; }