Files
Aislo/A00_Common/formula_doc_sub.ts
T
eomsangdonandClaude Opus 5.5 f5545dd62b feat(803-2a): 수식 탭 세 영역 · 하나의 수식 영역 · 합계 상자 · 설명 글 지움
- 세 영역 가로 — 기호 식(줄바꿈으로 이어진 하나의 수식 영역) | 값 식(같은 높이 줄) | 기호 정의
- 맨 아래 합계 상자 = 단가산출 상세 탭 부품(노무비 · 재료비 · 경비 · 계) · 옵션 아래 계 줄 · 설명 글 · 표지 안내 줄 걷음(오류 = 그 기호 빨강)
- 기호 식 줄 누름 = 그 자리 편집기(치는 대로 조판 — / 분수 · _ 아래첨자 · ^ 위첨자 · 괄호 · 화살표 구조 이동 · Enter 새 줄 · Esc 나옴 · 한글 조합) · 편집 중만 영역 위 수식 도구 줄
- 금액 줄 = 영역 안 식 줄 「노무비 형틀목공 = w × Q」(머리 첫 말 = 비목) · 「+ 식 줄」 「+ 금액 줄」 「+ 묶음」 · 비목 고르기 칸 걷음 · 묶음 = 도구 줄 [묶음]
- 저장 전 새 로직 = 세 영역 빈 화면 · 끊은 단가산출 블록도 같은 편집기
- 새 라이브러리 없음 · 서버 길 · 계산기 · 문서 꼴 그대로

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

172 lines
6.6 KiB
TypeScript

/* =============================================================================
* formula_doc_sub.ts
* 단가산출 연결 펼침(PLAN 802-3 · 802-4b-2 · 요구 659-3 · 796-2e 뜻) — 정의 영역에서 단가산출 기호를 열면 그 문서의 식 · 값 식
* M01 = 읽기만 · M02 사본(`copy`) = 머리 「연결」 + [연결 끊기](이 사본 안 자기 문서로 굳힘 · 금액 그대로) ·
* 「끊음」 + [다시 잇기](고친 것 버림 · 부름 문서로) · 끊은 문서 = 그 자리에서 식 줄 · 금액 줄 · 숫자 고침(`formula_doc_edit.ts`)
* · 줄 편집 = 수식 영역과 같은 편집기(`formula_doc_math.ts` · 803-2a) · 맨 아래 계 줄
* 셈 = 같은 계산기 · 연결 캐시(끊은 문서는 캐시 문서를 덮어씀 — 부른 쪽이 `merge`)
* ========================================================================== */
import { createBadge } from "@ui/ui_template_badge";
import { createButton } from "@ui/ui_template_button";
import { eqText } from "@ui/ui_template_equation";
import { editNumber } from "@ui/ui_template_equation_edit";
import { el } from "@ui/ui_template_elements";
import { varId, type FormulaDoc, type Link } from "../common_util/common_util_formula_doc";
import { calcDoc, type DocAnswer } from "../common_util/common_util_formula_doc_calc";
import type { DocCache } from "../common_util/common_util_formula_doc_cache";
import { editLit, editRow, ensureVar, sweep } from "./formula_doc_edit";
import { buildLines, sumRow, type RowRef } from "./formula_doc_lines";
import { openMath } from "./formula_doc_math";
import { evalNode, tryParse } from "./formula_doc_render";
import { ft } from "./formula_doc_text";
export interface SubCopy {
/** 층 쓰기 권한 */
edit: boolean;
/** 이 사본이 부르는 키(부름 · 끊음) */
has: (key: string) => boolean;
/** 끊은 문서(없으면 연결) */
cut: (key: string) => FormulaDoc | undefined;
onCut: (key: string) => void;
onJoin: (key: string) => void;
/** 끊은 문서 고침 — 캐시 · 셈 · 그림까지 부른 쪽이 */
change: (key: string, fn: (d: FormulaDoc) => void) => void;
}
export interface SubHooks {
doc: FormulaDoc | null;
answer: DocAnswer | null;
cache: DocCache;
copy?: SubCopy;
}
/** 끊은 문서에서 편집 중인 줄 — 다음 그림에서 편집기를 엶 */
let next: { key: string; ref: RowRef; at?: { x: number; y: number }; seed?: string } | null = null;
const subDoc = (cache: DocCache, k: string): FormulaDoc | null => {
try {
return cache.links.doc(k) ?? null;
} catch {
cache.takeMissing();
return null;
}
};
function linkHead(k: string, c: SubCopy): HTMLElement | null {
if (!c.has(k)) return null;
const cut = !!c.cut(k);
const tools = c.edit
? [
createButton({
label: cut ? ft("Relink") : ft("Unlink"),
variant: "ghost",
size: 24,
onClick: () => (cut ? c.onJoin(k) : c.onCut(k)),
}),
]
: [];
const head = el("div", {
className: "fdoc__sublink",
attrs: { "data-sub-link": cut ? "cut" : "linked" },
children: [createBadge(cut ? ft("Cut") : ft("Linked"), cut ? "warning" : "info"), ...tools],
});
return head;
}
/** 단가산출 연결 펼침 — 그 문서의 식 · 값 식(끊은 사본 문서면 고침) */
export function subBlock(id: string, h: SubHooks): HTMLElement | null {
const link: Link | undefined = h.doc?.변수?.find((v) => varId(v) === id)?.연결;
if (link?.갈래 !== "단가산출" || !link.키 || link.키.includes("{")) return null;
const k = link.키;
const sub = subDoc(h.cache, k);
if (!sub) return null;
const inputs: Record<string, unknown> = {};
for (const [name, t] of Object.entries(link.입력 ?? {})) {
const n = tryParse(t);
const v = n ? evalNode(n, h.answer?.값 ?? {}) : undefined;
if (v !== undefined) inputs[name] = v;
}
const got = calcDoc(sub, h.cache.links, inputs);
h.cache.takeMissing();
const c = h.copy;
const live = !!(c?.edit && c.cut(k));
const put = (fn: (d: FormulaDoc) => void): void => c!.change(k, fn);
const none = (): void => undefined;
const editing = next?.key === k ? next : null;
next = null;
const box = el("div", { className: "fdoc__grid fdoc__sub" });
const edit = (ref: RowRef | "end", at?: { x: number; y: number }): void => {
if (ref !== "end") {
next = { key: k, ref, at };
return put(() => undefined);
}
const money = (sub.금액 ?? []).length;
next = { key: k, ref: { kind: money ? "money" : "line", i: money || (sub.식 ?? []).length } };
put((d) => {
const last = d.금액?.[d.금액.length - 1];
if (last) {
d.금액!.push({
이름: "",
비목: last.비목 ?? "노무비",
단가: "",
...(last.묶음 ? { 묶음: last.묶음 } : {}),
});
next!.seed = `${last.비목 ?? "노무비"} `;
} else (d.식 ??= []).push({ 기호: "", 식: "" });
});
};
const mount = (host: HTMLElement, ref: RowRef, text: string): void => {
const first = editing?.seed ?? text;
queueMicrotask(() =>
openMath(host, {
text: first,
at: editing?.at ?? "end",
onLeave: (now) =>
put((d) => {
if (now !== first) editRow(d, ref.kind, ref.i, now);
sweep(d);
}),
}),
);
};
box.append(
...buildLines({
admin: live,
doc: sub,
answer: got,
vars: new Map((sub.변수 ?? []).map((v) => [varId(v), v])),
marks: new Map(),
editing: editing?.ref ?? null,
mount,
onEditAt: live ? edit : none,
onLit: live ? (w, i, at, v) => put((d) => editLit(d, w, i, at, v)) : none,
onPickVar: (vid, num) => {
const v = sub.변수?.find((x) => varId(x) === vid);
if (!live || !num || v?.연결?.갈래 !== "숫자") return;
editNumber(num, (t) => {
const n = Number(t.replace(/,/g, ""));
put((d) => (ensureVar(d, vid).연결 = { ...v.연결, 값: Number.isFinite(n) ? n : t }));
});
},
onGroupName: none,
onRedraw: () => put((d) => sweep(d)),
}).cells,
);
if ((sub.금액 ?? []).length)
box.append(...sumRow(ft("Sum"), got.셈됨 ? (got as unknown as Record<string, unknown>) : null));
if (sub.결과) {
const r = got.결과;
box.append(
el("div", { className: "fdoc__cell fdoc__total", text: ft("Result") }),
el("div", {
className: "fdoc__cell fdoc__total",
text: typeof r === "number" ? `${eqText(r)} ${sub.결과단위 ?? ""}` : "—",
}),
);
}
const head = c ? linkHead(k, c) : null;
if (!head) return box;
return el("div", { attrs: { "data-sub": k }, children: [head, box] });
}