Files
Aislo/ui_template/ui_template_equation_line.ts
T
eomsangdonandClaude Opus 5.5 2376e70fbb revert(802-1): 801-1 wip 옛 나무 위 고침 걷음 — 요구 664 수식 문서로 다시 짬
- 걷음: 43784a4c0 의 옛 수식 나무 위 고침(서버 · 화면 · css) = 그 앞 판으로 되돌림
- 남김: master_equation_sign.py · ui_template_equation_text.ts · ui_template_equation_edit.ts(다시 쓸 조각 · 지금 안 부름) · text 는 `입력글` 을 제 타입에
- 지움: test_801_1_equation_inline.py — 걷은 옛 나무 고침을 재는 시험

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

217 lines
8.0 KiB
TypeScript

/* =============================================================================
* 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<string>;
/** 변수 누름(없으면 눌리지 않음) */
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<HTMLElement>(
`[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)));
}