Files
Aislo/ui_template/ui_template_equation_sum.ts
T
eomsangdonandClaude Sonnet 5.5 59f0dd90a8 feat(800-4): 소계 · 합계를 기준 단가산출별 묶음으로 — 가. 이름 → 노 · 재 · 경 → 소 계 · 맨 끝 소 계(묶음 소계를 더한 식) · 블록 식 줄 아래 · 블록 줄 곁 열쇠 글 걷음
- 수식 영역: 블록 묶음은 그 블록 식 줄 바로 아래(createEqChain after) · 남은 묶음 · 소 계 · 끝수 · 합계는 맨 끝
- 금액 셈 그대로(보이기만) · 묶음 소계 합 + 끝수 = 합계 시험 추가(test_800_2)
- 정의 영역: 로직 부르기 정의의 이름 되풀이 걷음 · 상수 연결 설명

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 17:03:00 +09:00

182 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* ui_template_equation_sum.ts
* 부품 「수식」 셋째 파일(799-1 · 796-2c · 800-4 · 요구 655-1 · 661 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로).
* 묶음 = 기준 단가산출마다(하위 로직 블록 · 자기 자원 · 덧줄) 「가. 이름」 머리 → 노무비 · 재료비 · 경비 줄 → 소 계(노 · 재 · 경 · 계 칸)
* 맨 끝 「소 계」 묶음 = 묶음 소계를 더한 식(노무비 : 2,299.4 + 5,162 = 7,461.4) → 끝수(합계 − 묶음 합 · 0 이면 줄 없음) → 합계(엔진 비목 합)
* 값 = 서버 `소계`(M01 `master_equation.subtotals` · M02 `M02_Sangeun_Equation_Full` = SK 셈 답) · 소계가 없으면(결과 로직) 아무것도 안 그림
* M02 SK 묶음 = 상태 알약(더함 · 덮음 · 뺌) · 덮음 갈래 · 원래 금액 · 수량 · 더한 기초는 접힘 수식 블록 · 뺌 = 흐리게 · 금액 0
* 자리 = 하위 로직 블록 묶음은 그 블록 식 줄 바로 아래(`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 KIND: Record<string, string> = { 자원: "자기 자원", 덧줄: "덧줄" };
const TONE: Record<string, BadgeTone> = { 더함: "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<string>, at: number, 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.갈래 === r.이름 ? (KIND[r.갈래] ?? r.이름) : r.이름;
const tree = r.나무;
const id = `sum:${at}:${r.이름}`;
const body = node("div", "ui-eq__sum-tree");
body.hidden = true;
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 out = !!r.합밖;
if (out) name.append(createBadge("합 밖", "neutral", { size: 18 }));
const notes: string[] = [];
if (out) notes.push(`합계에 안 듦 ${eqText(r.밖금액) || "0"}`);
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(" · ")));
box.append(name, body);
for (const k of PARTS) if (num(r[k]) !== 0) box.append(part(k, eqText(r[k])));
const total = row("소계", "소 계", (k) => r[k]);
total.classList.add("ui-eq__grp-total");
if (out) total.dataset.eqSumOut = "1";
box.append(total);
return box;
}
/** 맨 끝 「소 계」 묶음 — 묶음 소계를 더한 식 */
function final(rows: EqSumRow[], at: number): HTMLElement {
const box = node("div", "ui-eq__grp");
box.dataset.eqSumItem = "소계";
const head = node("div", "ui-eq__grp-head");
head.append(
node("span", "ui-eq__grp-mark", `${mark(at)}.`),
node("span", "ui-eq__sum-label", "소 계"),
);
box.append(head);
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<string, HTMLElement>;
/** 수식 영역 맨 끝 — 남은 묶음 · 소 계 · 끝수 · 합계 */
tail: HTMLElement;
}
export function createEqSum(
tree: Pick<EqTree, "합계" | "소계" | "답까닭">,
open: Set<string> = new Set(),
hooks?: EqSumHooks,
lineKeys: string[] = [],
): EqSumParts | null {
const sub = tree.소계;
if (!sub?.줄.length) return null;
const inline = new Map<string, HTMLElement>();
const box = node("div", "ui-eq__sum");
box.dataset.eqSum = "";
sub.줄.forEach((r, at) => {
const one = group(r, open, at, hooks);
if (r.갈래 === "블록" && lineKeys.includes(r.이름) && !inline.has(r.이름))
inline.set(r.이름, one);
else box.append(one);
});
box.append(final(sub.줄, sub.줄.length));
const rest = sub.끝수;
const odd = COLS.some((k) => Number(rest[k] ?? 0) !== 0);
if (odd) box.append(row("끝수", "끝수", (k) => rest[k]));
box.append(row("합계", "합계", (k) => tree.합계[k]));
const note = [odd ? sub.끝수까닭 : "", tree.답까닭].filter(Boolean).join(" · ");
if (note) box.append(node("div", "ui-eq__sum-note", note));
return { inline, tail: box };
}