- 셈 구동 떼기: 새 M01_MasterData_UI_Logic_Drive.ts(driveRun · previewText · autoFill · createDrive) — M01 상세는 숨은 시험 계산 칸 단추 대신 구동 함수 · 시험 계산 칸(Logic_Calc)은 로직 복사 화면이 쓰므로 남기고 같은 구동을 부름 · 재료 고름은 rerun 고리 - M02: 새 _Sangeun_Tabs.ts(수식 · 단가산출 상세 보기 고르기) · Detail 의 시험 계산 · 인력/재료/경비 걷음 - 걷은 파일(git rm): M01 LogicLab_Sheet · Logic_Sheet_Model · LogicLab_Formula(.ts .css) · FormulaTokens · LogicLab_Est / M02 Sangeun_Sheet(.ts .css) · SheetEdit · SheetFormula · logic Sheet_Model · Const_Model · Formula_Model · 부품 시트 줄(createCalcSheetRow) · Cover_Model.sheetGroups · Calc 의 M02 고리(answer · shown · held) · 죽은 CSS · 글 - 서버 길 · 문서 칸(변수줄 · 글줄 · 덮개)은 남김 — 금액 그대로 - 낡은 시험: test_790_1(시트 화면 전부) · test_714_3 · test_748_2(+ helper)는 화면이 없어져 지움 · test_794_1 · test_790_2 는 화면 · TS 거울만 덜고 서버 시험 남김 · test_714_2 · 717_2b · 721 · 722(원래 틀린 한 줄 고침) · 734 · 737 · 750_2 · 793_3b · 796_2 · 796_2b 는 보기 둘 · 새 파일에 맞춤 · 새 test_799_2_old_tabs_gone Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
245 lines
9.0 KiB
TypeScript
245 lines
9.0 KiB
TypeScript
/* =============================================================================
|
|
* ui_template_equation_line.ts
|
|
* 부품 「수식」 둘째 파일(796-2) — 줄 · 변수 풀이 · 하위 로직 블록 · 여러 줄 이음. 식 그림 = `ui_template_equation.ts`.
|
|
* 줄 = 번호 · 결과 변수명 = 이름 식 = 값 식 = 결과 값(단위) — 네 칸(PPT 꼴) · 오른쪽 변수 풀이(기호 : 이름 = 값 단위 · 출처)
|
|
* 값 식이 결과와 같으면 값 식 칸 생략 · 식을 못 읽은 줄 = 원문 + 까닭(붉은 글)
|
|
* 이음 = 여러 줄 위에서 아래로(`createEqChain`) · 중간 계산이 한눈에 · 하위 로직 부르기 = 줄 아래 접힘 블록(노 · 재 · 경 · 계 소계 · 펼치면 그 로직 줄 이음)
|
|
* 크기 = 글 `--text-body` · 풀이 `--text-caption` · 접힘 단추 22
|
|
* 상태 = 고른 줄 `data-selected`(왼쪽 띠 · 식 칸 자리 `slot` 이 줄 아래에 열림) · 펼친 블록 `data-open`
|
|
* 쓰는 법 = `createEqChain(tree.줄, { path: "", open: new Set(), selected, onSelect, onVar, slot })`
|
|
* 대신한 옛 변종 = 시트 줄(이름 | 식 | 값 한 줄 · 분수 · 풀이 없음 · 799-2 에 걷음)
|
|
* ========================================================================== */
|
|
|
|
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>;
|
|
/** 고른 줄 이름표(`lineId`) */
|
|
selected?: string | null;
|
|
/** 줄을 누름(없으면 안 눌림) */
|
|
onSelect?: (line: EqLine, id: string) => void;
|
|
/** 변수 누름(없으면 눌리지 않음) */
|
|
onVar?: (node: EqNode) => void;
|
|
/** 고른 줄 아래에 열 칸(식 칸 · 아이콘 줄) */
|
|
slot?: (line: EqLine, id: string) => HTMLElement | null;
|
|
/** 결과 줄 값 뒤 단위 */
|
|
resultUnit?: string;
|
|
/** 줄 번호 1 부터 보임 */
|
|
numbered?: boolean;
|
|
}
|
|
|
|
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.열쇠}`;
|
|
|
|
/** 식 안 변수(찾기 · 입력 · 중간 · 요소) — 열쇠마다 하나 · 블록 속은 안 들어감 */
|
|
export function varsOf(node: EqNode | undefined, out = new Map<string, EqNode>()): EqNode[] {
|
|
if (!node) return [];
|
|
if (node.꼴 === "변수" && ["입력", "중간", "찾기", "요소"].includes(node.갈래 ?? "")) {
|
|
if (!out.has(node.열쇠 ?? "")) out.set(node.열쇠 ?? "", node);
|
|
}
|
|
for (const k of ["좌", "우", "안", "조건", "참", "거짓"] as const) varsOf(node[k], out);
|
|
for (const k of ["위", "아래"] as const) {
|
|
const child = node[k];
|
|
if (typeof child === "object") varsOf(child, out);
|
|
}
|
|
(node.인자 ?? []).forEach((a) => varsOf(a, out));
|
|
return [...out.values()];
|
|
}
|
|
|
|
const SOURCE: Record<string, string> = { 입력: "입력", 중간: "중간 줄", 요소: "요소" };
|
|
|
|
function solveRow(node: EqNode, o: EqLineOptions): HTMLElement {
|
|
const value = eqText(node.값);
|
|
const where =
|
|
node.갈래 === "찾기"
|
|
? [node.표, ...Object.entries(node.행 ?? {}).map(([k, v]) => `${k}=${String(v)}`)]
|
|
.filter(Boolean)
|
|
.join(" · ")
|
|
: (SOURCE[node.갈래 ?? ""] ?? "");
|
|
const name = span("ui-eq__solve-name", node.이름 || node.열쇠 || "");
|
|
name.title = name.textContent ?? "";
|
|
const val = span("ui-eq__solve-val", value ? `${value}${node.단위 ? ` ${node.단위}` : ""}` : "—");
|
|
val.title = val.textContent ?? "";
|
|
const row = div(
|
|
"ui-eq__solve-row",
|
|
symbolOf(node),
|
|
span("ui-eq__op", ":"),
|
|
name,
|
|
span("ui-eq__op", "="),
|
|
val,
|
|
...(where ? [span("ui-eq__solve-src", where)] : []),
|
|
);
|
|
row.dataset.eqSolve = node.열쇠 ?? "";
|
|
if (o.onVar && node.갈래 !== "찾기" && node.갈래 !== "요소") {
|
|
row.classList.add("is-pick");
|
|
row.addEventListener("click", () => o.onVar?.(node));
|
|
}
|
|
return row;
|
|
}
|
|
|
|
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.나무.결과단위,
|
|
numbered: false,
|
|
}),
|
|
);
|
|
} 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), span("ui-eq__block-key", node.열쇠 ?? "")),
|
|
body,
|
|
);
|
|
if (o.open.has(id)) toggle(true);
|
|
return box;
|
|
}
|
|
|
|
/** 줄 하나 */
|
|
export function createEqLine(line: EqLine, index: number, 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");
|
|
if (o.numbered !== false) body.append(span("ui-eq__step", String(index + 1)));
|
|
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)] : []),
|
|
);
|
|
let vars: EqNode[] = [];
|
|
if (line.이름식) {
|
|
vars = varsOf(line.이름식);
|
|
const name = span("ui-eq__box ui-eq__name", renderEq(line.이름식, hand));
|
|
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 solve = div("ui-eq__solve", ...vars.map((v) => solveRow(v, o)));
|
|
const main = div("ui-eq__main", body, solve);
|
|
if (o.onSelect) {
|
|
root.classList.add("is-pick");
|
|
main.addEventListener("click", () => o.onSelect?.(line, id));
|
|
}
|
|
if (o.selected === id) root.dataset.selected = "1";
|
|
root.append(main);
|
|
blocks.forEach((b, i) => root.append(blockPanel(b, i + 1, o)));
|
|
const slot = o.selected === id ? o.slot?.(line, id) : null;
|
|
if (slot) root.append(slot);
|
|
return root;
|
|
}
|
|
|
|
/** 여러 줄 위에서 아래로 이음 */
|
|
export function createEqChain(lines: EqLine[], o: EqLineOptions): HTMLElement {
|
|
return div("ui-eq", ...lines.map((l, i) => createEqLine(l, i, o)));
|
|
}
|