Files
Aislo/ui_template/ui_template_equation_sum.ts
T
eomsangdonandClaude Sonnet 5.5 47f260e012 feat(801-3): 수식 탭 소계 · 합계 묶음 다시 — 돈 묶음 하나 = 머리 없음 · 계 / 둘 이상 = 소계 / 단가산출 둘 이상 = 합계 · 안쪽 말 0
- 묶음 한 곳 master_equation_group.bundles: 부른 · 더한 단가산출 하나씩 + 직접 줄(인력 · 재료 · 기계 · 덧줄) 한 묶음 · 돈 없는 줄 · 뺀 줄 = 묶음 아님 · 합계 밖 줄 = 밖 글
- M01 subtotals · M02 M02_Sangeun_Equation_Full 같은 꼴 · 끝수 셈 그대로 · 금액 전후 같음
- 부품 ui_template_equation_sum: 자기 자원 · 덧줄 글 0 · 묶음 하나 = 머리 없음 · 계 · 둘 이상 = 가. 머리 · 소 계 · 단가산출 둘 이상 = 합계
- 옛 시험 기대 고침: test_799_1(갈래 자원 · 덧줄 → 직접 한 묶음) · test_796_2c(줄마다 한 행 → 묶음 · 낱줄 · 밖) — 묶음 꼴이 바뀜
- 새 test_801_3_groups 5

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

190 lines
8.4 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 · 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<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,
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<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) return null;
const inline = new Map<string, HTMLElement>();
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 };
}