/* ============================================================================= * ui_template_equation_line.ts * 부품 「수식」 둘째 파일(796-2 · 800-2) — 수식 영역 줄 · 하위 로직 블록 · 여러 줄 이음. 식 그림 = `ui_template_equation.ts`. * 줄 = 결과 변수명 = 이름 식 = 값 식 = 결과 값(단위) — 네 칸(PPT 꼴) · 줄 번호 · 구분선 · 줄마다 풀이 없음(변수 정의 = `ui_template_equation_defs.ts`) * 값 식이 결과와 같으면 값 식 칸 생략 · 식을 못 읽은 줄 = 원문 + 까닭(붉은 글) * 이음 = 여러 줄 위에서 아래로(`createEqChain`) · 하위 로직 부르기 = 줄 아래 접힘 블록(노 · 재 · 경 · 계 소계 · 펼치면 그 로직 줄 이음) * 크기 = 글 `--text-body` · 접힘 단추 22 * 상태 = 바로 고침 — 이름 식 칸을 누르면 그 자리가 식 칸으로 바뀜(`onEdit` → 부르는 쪽이 `editing` 을 정해 다시 그림 · `editor` 가 칸을 줌) · 펼친 블록 `data-open` * 쓰는 법 = `createEqChain(tree.줄, { path: "", open: new Set(), onVar, editing, onEdit, editor })` * 대신한 옛 변종 = 시트 줄(이름 | 식 | 값 한 줄 · 분수 · 풀이 없음 · 799-2 에 걷음) · 줄 눌러 여는 아래 편집 칸(800-3 에 걷음) * ========================================================================== */ import { createBadge } from "./ui_template_badge"; import { createButton } from "./ui_template_button"; import { eqText, plainEq, renderEq, symbolOf, type EqHandlers, type EqLine, type EqNode, } from "./ui_template_equation"; export interface EqLineOptions { /** 블록 접힘 이름표 앞머리(블록 안 이음이 겹치지 않게) */ path: string; /** 펼친 블록 이름표 모음 — 다시 그려도 유지 */ open: Set; /** 변수 누름(없으면 눌리지 않음) */ onVar?: (node: EqNode) => void; /** 바로 고치는 줄 이름표(`lineId`) — 이 줄의 이름 식 칸 자리에 `editor` 칸이 들어감 */ editing?: string | null; /** 이름 식 칸을 누름(없으면 고칠 수 없음) */ onEdit?: (line: EqLine, id: string) => void; /** 고치는 줄의 식 칸 */ editor?: (line: EqLine, id: string) => HTMLElement; /** 줄 바로 아래에 끼울 것(하위 로직 블록 소계 묶음 · 800-4) */ after?: (line: EqLine) => HTMLElement | null; /** 결과 줄 값 뒤 단위 */ resultUnit?: string; } const BUNDLE: [string, string][] = [ ["노무비", "노"], ["재료비", "재"], ["경비", "경"], ["계", "계"], ]; const span = (className: string, ...kids: (Node | string)[]): HTMLSpanElement => { const node = document.createElement("span"); node.className = className; node.append(...kids); return node; }; const div = (className: string, ...kids: Node[]): HTMLDivElement => { const node = document.createElement("div"); node.className = className; node.append(...kids); return node; }; /** 줄 이름표 — 호표 · 덧줄은 차례 · 그 밖은 열쇠 */ export const lineId = (line: EqLine): string => line.갈래 === "호표" || line.갈래 === "덧줄" ? `${line.갈래}:${line.차례 ?? 0}` : `${line.갈래}:${line.열쇠}`; function bundle(node: EqNode): HTMLElement { const box = span("ui-eq__bundle"); const got = node.묶음; if (got && typeof got === "object") for (const [key, label] of BUNDLE) if (got[key] !== undefined && got[key] !== null) box.append(span("ui-eq__bundle-item", `${label} ${eqText(got[key])}`)); if (!box.childElementCount && node.값 !== undefined && node.값 !== null) box.append(span("ui-eq__bundle-item", eqText(node.값))); return box; } function blockPanel(node: EqNode, at: number, o: EqLineOptions): HTMLElement { const id = `${o.path}/${node.열쇠 ?? at}`; const body = div("ui-eq__block-body"); const box = div("ui-eq__blockpanel"); box.dataset.eqBlockPanel = node.열쇠 ?? ""; box.dataset.blockId = id; const head = createButton({ label: `${String.fromCodePoint(0x245f + at)} ${node.이름 || node.열쇠 || ""}`, variant: "ghost", size: 22, iconStart: "chevronRight", onClick: () => toggle(), }); head.classList.add("ui-eq__block-head"); const toggle = (force?: boolean): void => { const on = force ?? !o.open.has(id); if (on) { o.open.add(id); if (!body.childElementCount && node.나무) body.append( createEqChain(node.나무.줄, { path: id, open: o.open, resultUnit: node.결과단위 ?? node.나무.결과단위, }), ); } else o.open.delete(id); box.dataset.open = on ? "1" : ""; body.hidden = !on; }; body.hidden = true; box.append(div("ui-eq__block-row", head, bundle(node)), body); if (o.open.has(id)) toggle(true); return box; } /** M02 SK 고친 칸(796-2c) — 갈래 알약 + 원래 → 지금 · 뺀줄 알약 */ function markOf(line: EqLine): HTMLElement | null { const kinds = line.표지 ?? []; if (!kinds.length && !line.뺌) return null; const box = span("ui-eq__mark"); if (kinds.length) box.append(createBadge(`고침 · ${kinds.join(" · ")}`, "warning", { size: 18 })); if (line.뺌) box.append(createBadge("뺌", "neutral", { size: 18 })); if (line.원래 !== undefined && line.원래 !== null) box.append( span( "ui-eq__mark-was", `원래 ${eqText(line.원래)}${line.뺌 ? "" : ` → 지금 ${eqText(line.지금) || "—"}`}`, ), ); return box; } /** 줄 하나 */ export function createEqLine(line: EqLine, o: EqLineOptions): HTMLElement { const id = lineId(line); const root = div("ui-eq__line"); root.dataset.eqLine = id; root.dataset.kind = line.갈래; const blocks: EqNode[] = []; const hand: EqHandlers = { onVar: o.onVar, blocks, onBlock: (node) => { root .querySelector( `[data-block-id="${CSS.escape(`${o.path}/${node.열쇠 ?? ""}`)}"] .ui-eq__block-head`, ) ?.click(); }, }; const body = div("ui-eq__body"); const head = line.결과; if (head) body.append( span( "ui-eq__box ui-eq__head", head.꼴 !== "변수" ? plainEq(head) : line.갈래 === "중간" ? renderEq(head, hand) // 누르면 변수 창 : symbolOf(head), ), ); const resUnit = line.갈래 === "호표" || line.갈래 === "덧줄" ? "원" : line.갈래 === "결과" ? (o.resultUnit ?? "") : (head?.단위 ?? ""); const result = span( "ui-eq__box ui-eq__res", line.글 || "—", ...(resUnit ? [span("ui-eq__unit", resUnit)] : []), ); if (line.이름식) { const name = span("ui-eq__box ui-eq__name"); if (o.editing === id && o.editor) { name.classList.add("is-open"); name.append(o.editor(line, id)); } else { name.append(renderEq(line.이름식, hand)); if (o.onEdit) { name.classList.add("is-edit"); name.title = "눌러서 바로 고침"; name.addEventListener("click", () => o.onEdit?.(line, id)); } } body.append(span("ui-eq__op", "="), name); const same = plainEq(line.값식) === (line.글 ?? "") || plainEq(line.값식) === plainEq(line.이름식); if (line.값식 && !same) body.append(span("ui-eq__op", "="), span("ui-eq__box ui-eq__val", renderEq(line.값식))); body.append(span("ui-eq__op", "="), result); } else { body.append( span("ui-eq__op", "="), span("ui-eq__box ui-eq__raw", line.식), span("ui-eq__bad", line.까닭 ?? ""), ); } const mark = markOf(line); if (mark) body.append(mark); if (line.뺌) root.dataset.removed = "1"; root.append(div("ui-eq__main", body)); blocks.forEach((b, i) => root.append(blockPanel(b, i + 1, o))); const extra = o.after?.(line); if (extra) root.append(extra); return root; } /** 여러 줄 위에서 아래로 이음 */ export function createEqChain(lines: EqLine[], o: EqLineOptions): HTMLElement { return div("ui-eq", ...lines.map((l) => createEqLine(l, o))); }