- M02 SK 「수식」 탭 = M01 과 같은 공용 화면(`createDocScreen` · 길 앞머리만 `/m02/layers/{층}/sangeun/{키}/doc`) — 옛 M02 수식 나무 화면(`_Sangeun_Equation.ts` · 796-2b · 2c · 2e · 800 · 801 꼴) 걷음
· 옵션 = SK 문서 값(저장) · [단가 갱신] · [재생성](미리 셈 → 알림 계 전 → 후 · 버리는 것 → 저장) · 「M01 새 판 있음」 배지(`formula_doc_copy.ts`)
· 단가산출 연결 펼침 머리 「연결 / 끊음」 · [연결 끊기] · [다시 잇기] · 끊은 문서 안 줄 그 자리 고침(`formula_doc_sub.ts`) · 저장 몸 {문서, 끊음, 판}
· 층 쓰기 권한 없음 = 읽기만(옵션 잠금 · 단추 없음 · 「읽기만」) · 판 바뀜 409 = 사본 같으면 새 판으로 한 번 더 · 저장 뒤 페이지 판 · 상세 탭 `수식` 칸 새로
· 저장 안 한 SK(일위대가 줄 모달) = 「저장한 단가산출만」 글
- 서버 — 수식 길 답에 `연결`(화면 캐시 기록 · `M02_Doc_Screen.record`) · 연결 후보 · 표 길(층 읽기 · M01 후보 · 표 그대로 · main 등록)
- 꺼진 줄(요구 663-1 마저) — 곱 인수 하나가 선택으로 꺼진 0(만약 0 갈래 · 식 줄 기호면 그 식으로 거침)이면 숨김 · 입력 · 글 0 은 그대로
· 판정을 순수 파일로(`common_util_formula_doc_view.ts` · Node 로 시험) · 1,518 문서 기본 옵션 꺼짐 158 → 203 줄(꺼진 줄 금액 모두 0) · GC000211 폼타이 아님 = 세 규격 줄 모두
- 화면 파일 700 줄 — 고침 함수 · 옵션 가로 · 연결 펼침을 새 파일로(`formula_doc_edit` · `_options` · `_sub`)
- 시험 test_802_4b2(5 · 헬퍼 helper_802_4b2_off.cjs) · 옛 시험 둘 고침 — test_793_3b · test_796_2b 가 걷은 옛 M02 수식 화면 글을 읽음 → 없음 확인으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
6.0 KiB
TypeScript
148 lines
6.0 KiB
TypeScript
/* =============================================================================
|
|
* formula_doc_sub.ts
|
|
* 단가산출 연결 펼침(PLAN 802-3 · 802-4b-2 · 요구 659-3 · 796-2e 뜻) — 정의 영역에서 단가산출 기호를 열면 그 문서의 식 · 값 식
|
|
* M01 = 읽기만 · M02 사본(`copy`) = 머리 「연결」 + [연결 끊기](이 사본 안 자기 문서로 굳힘 · 금액 그대로) ·
|
|
* 「끊음」 + [다시 잇기](고친 것 버림 · 부름 문서로) · 끊은 문서 = 그 자리에서 식 줄 · 금액 줄 · 숫자 고침(`formula_doc_edit.ts`)
|
|
* 셈 = 같은 계산기 · 연결 캐시(끊은 문서는 캐시 문서를 덮어씀 — 부른 쪽이 `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 { editLine, editLit, editMoney, ensureVar, tidyText } from "./formula_doc_edit";
|
|
import { buildLines, type EditAt } from "./formula_doc_lines";
|
|
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; at: EditAt } | 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 auto = next?.key === k ? next.at : null;
|
|
next = null;
|
|
const box = el("div", { className: "fdoc__grid fdoc__sub" });
|
|
box.append(
|
|
...buildLines({
|
|
admin: live,
|
|
doc: sub,
|
|
answer: got,
|
|
vars: new Map((sub.변수 ?? []).map((v) => [varId(v), v])),
|
|
marks: new Map(),
|
|
auto,
|
|
onLine: live ? (i, t) => put((d) => editLine(d, i, t)) : none,
|
|
onExpr: live ? (i, t) => put((d) => (d.식![i].식 = tidyText(t))) : none,
|
|
onMoney: live ? (i, p) => put((d) => editMoney(d, i, p)) : 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 }));
|
|
});
|
|
},
|
|
onAddLine: () => {
|
|
next = { key: k, at: { kind: "line", i: (sub.식 ?? []).length } };
|
|
put((d) => (d.식 ??= []).push({ 기호: "", 식: "" }));
|
|
},
|
|
onAddMoney: (g) => {
|
|
next = { key: k, at: { kind: "name", i: (sub.금액 ?? []).length } };
|
|
put((d) =>
|
|
(d.금액 ??= []).push({ 이름: "", 비목: "노무비", 단가: "", ...(g ? { 묶음: g } : {}) }),
|
|
);
|
|
},
|
|
onGroupName: none,
|
|
onRedraw: () =>
|
|
put((d) => {
|
|
d.식 = (d.식 ?? []).filter((l) => String(l.기호 ?? "") || String(l.식 ?? ""));
|
|
d.금액 = (d.금액 ?? []).filter((l) => l.이름 || l.단가 || l.수량);
|
|
}),
|
|
}),
|
|
);
|
|
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] });
|
|
}
|