fix(802-3): 선택으로 꺼진 금액 줄 숨김 · 값 식 한 번 · 옮김 군더더기 걷음(브레인 사진 지적)

- 663-1: 수량 · 단가가 만약으로 0 을 탄 금액 줄 = 안 보임(GC000211 폼타이 9.5 · 7.9 ㎜) · 규격 바꾸면 그 줄만 · [선택으로 꺼진 줄 보기] = 고침 모드에서만 · 정의 영역도 꺼진 줄 기호 뺌
  1,518 문서 금액 줄 5,912 중 기본 옵션에서 꺼짐 145 줄(107 문서) · 옵션에 따라 꺼질 수 있는 줄 305 줄(141 문서)
- 식 안 만약이 0 · 1 을 타면 그 항(+ 0 · × 1)도 안 보임 — 고침 글은 그대로
- 수 하나 · 기호 하나 줄 값 식 = 한 번만(「= 1 = 1」 없음) · 수량 없음 「× 1」 자리는 줄에 마우스 올릴 때만
- 옮김 slim(master_doc_convert) — + 0 · × 1 · ÷ 1 지움 · A × k ÷ 100 → A × k% · 빌더 다시(목록 xlsx 는 안 씀) · 대조 그대로(기본 1,519 같음 · 옵션 다름 1 · 저장 1,518) · 31 파일 글만
- 시험 test_802_3 + 군더더기 없음 1(12) · 802_1 · 802_2 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 19:43:00 +09:00
co-authored by Claude Opus 5.5
parent 54fb55487c
commit a003587aca
38 changed files with 693 additions and 568 deletions
+5
View File
@@ -304,6 +304,11 @@
gap: 0 var(--spacing-4);
}
/* 수량 없음 = 1 — 고치는 자리만(줄에 마우스를 올릴 때 흐리게) */
.fdoc__money .is-ghost {
opacity: 0;
}
.fdoc__money:hover .is-ghost {
opacity: 0.45;
}
+23 -10
View File
@@ -17,8 +17,10 @@ import type { DocVar, FormulaDoc, MoneyLine, Node } from "../common_util/common_
import type { DocAnswer } from "../common_util/common_util_formula_doc_calc";
import {
litsOf,
offLine,
renderNode,
symNode,
takenOf,
tryParse,
valueNode,
type Values,
@@ -33,6 +35,9 @@ export type EditAt = { kind: "line" | "expr" | "price" | "qty" | "name"; i: numb
export interface LinesHooks {
admin: boolean;
/** 선택으로 꺼진 금액 줄도 보임(고침 모드) */
showOff?: boolean;
onShowOff?: () => void;
doc: FormulaDoc;
answer: DocAnswer;
vars: Map<string, DocVar>;
@@ -161,7 +166,14 @@ function lineRow(i: number, h: LinesHooks): HTMLElement[] {
vexpr.append(expr(line.식, h, "val").node);
const got = vals(h)[head];
right.append(span("ui-eq__op", "="), vexpr);
if (got !== null && got !== undefined && typeof got !== "object") {
// 수 하나 · 기호 하나(탄 갈래) 줄 = 값 한 번만(「= 1 = 1」 없음)
let eff = tryParse(line.식);
for (let pick = eff ? takenOf(eff, vals(h)) : 0; eff && pick; pick = takenOf(eff, vals(h)))
eff = (eff as ["call", string, Node[]])[2][pick];
const single = !!eff && (eff[0] === "num" || (eff[0] === "var" && vals(h)[eff[1]] === got));
if (single) {
/* 값 식이 곧 결과 */
} else if (got !== null && got !== undefined && typeof got !== "object") {
right.append(span("ui-eq__op", "="), valueNode(got, unitOf(head, h)));
} else right.append(span("ui-eq__op", "="), span("fdoc__dash", "—"));
if (h.admin) {
@@ -333,10 +345,13 @@ export function buildLines(h: LinesHooks): HTMLElement[] {
const lines = h.doc.금액 ?? [];
const groups: string[] = [];
lines.forEach((l) => groups.includes(l.묶음 ?? "") || groups.push(l.묶음 ?? ""));
const off = new Set(lines.flatMap((l, i) => (offLine(l, vals(h)) ? [i] : [])));
const seen = (i: number): boolean => !!h.showOff || !off.has(i);
const many = groups.length > 1;
const bundles = (h.answer.묶음 ?? []) as Record<string, unknown>[];
const done = h.answer.셈됨;
groups.forEach((g, gi) => {
if (!lines.some((l, i) => (l.묶음 ?? "") === g && seen(i))) return; // 다 꺼진 묶음
if (many) {
const head = span("fdoc__group", `${ORDER[gi] ?? gi + 1}. `);
const label = span("fdoc__name", g || ft("Unnamed"));
@@ -345,7 +360,7 @@ export function buildLines(h: LinesHooks): HTMLElement[] {
label.addEventListener("click", () => editName(label, g, (t) => h.onGroupName(g, t)));
add([el("div", { className: "fdoc__cell fdoc__grouphead", children: [head] })]); // 두 칸 걸침
}
lines.forEach((l, i) => (l.묶음 ?? "") === g && add(moneyRow(i, h)));
lines.forEach((l, i) => (l.묶음 ?? "") === g && seen(i) && add(moneyRow(i, h)));
if (h.admin && lines.length)
pair(
el("div", {
@@ -364,14 +379,12 @@ export function buildLines(h: LinesHooks): HTMLElement[] {
}),
null,
);
if (h.admin && lines.length)
pair(
el("div", {
className: "fdoc__cell",
children: [button(ft("Add_Group"), () => h.onAddMoney(ft("New_Group")))],
}),
null,
);
if (h.admin && lines.length) {
const kids = [button(ft("Add_Group"), () => h.onAddMoney(ft("New_Group")))];
if (off.size && h.onShowOff)
kids.push(button(`${h.showOff ? ft("Hide_Off") : ft("Show_Off")} ${off.size}`, h.onShowOff));
pair(el("div", { className: "fdoc__cell", children: kids }), null);
}
if (lines.length) {
const sum = done ? (h.answer as unknown as Record<string, unknown>) : null;
add(totalRow(many ? ft("Total") : ft("Sum"), sum, "is-end"));
+26 -1
View File
@@ -112,6 +112,23 @@ export function evalNode(n: Node, vals: Values): unknown {
}
}
/** 만약 사슬이 탄 끝 갈래가 수 하나면 그 수(만약이 아니거나 모르면 undefined) */
export function leafNum(n: Node, vals: Values): number | undefined {
let at = n;
let hop = 0;
for (let pick = takenOf(at, vals); pick; pick = takenOf(at, vals), hop++)
at = (at as ["call", string, Node[]])[2][pick];
return hop && at[0] === "num" ? at[1] : undefined;
}
/** 선택으로 꺼진 금액 줄 — 수량 · 단가가 만약으로 0 을 탐(663-1 · 고침 모드에서만 보임) */
export function offLine(line: { 수량?: unknown; 단가?: unknown }, vals: Values): boolean {
return [line.수량, line.단가].some((t) => {
const n = tryParse(t);
return !!n && leafNum(n, vals) === 0;
});
}
/** 만약 — 탄 갈래 차례(1 참 · 2 거짓 · 0 모름) */
export function takenOf(n: Node, vals: Values): 0 | 1 | 2 {
if (n[0] !== "call" || n[1] !== "만약" || n[2].length !== 3) return 0;
@@ -262,6 +279,13 @@ export function renderNode(n: Node, c: Ctx): HTMLElement {
}
case "bin": {
const [, o, a, b] = n;
// 선택으로 꺼진 항(만약이 0 · 1 을 탐) = 안 보임(663-1) · 고침 글은 그대로
const na = leafNum(a, c.vals);
const nb = leafNum(b, c.vals);
if ((o === "+" || o === "-") && nb === 0) return hidden("(", `) ${o} ${textOf(b)}`, go(a));
if ((o === "*" || o === "/") && nb === 1) return hidden("(", `) ${o} ${textOf(b)}`, go(a));
if ((o === "+" && na === 0) || (o === "*" && na === 1))
return hidden(`${textOf(a)} ${o} (`, ")", go(b));
if (o === "/" && !c.inline) {
const f = span("ui-eq__frac");
const top = span("ui-eq__top");
@@ -341,7 +365,7 @@ export function namesTaken(n: Node, vals: Values, out: string[] = []): string[]
const RESERVED_MONEY = new Set(["노무비", "재료비", "경비", "번지", "단계"]);
/** 정의 영역에 보일 기호 — 식 줄 · 금액 줄 · 결과에서 지금 탄 갈래로 닿는 것(보이는 차례) */
export function reach(doc: FormulaDoc, vals: Values): string[] {
export function reach(doc: FormulaDoc, vals: Values, showOff = false): string[] {
const vars = new Map((doc.변수 ?? []).map((v) => [varId(v), v]));
const lines = new Map((doc.식 ?? []).map((l) => [String(l.기호 ?? ""), l.식]));
const money = new Set((doc.금액 ?? []).map((l) => String(l.기호 ?? "")).filter(Boolean));
@@ -365,6 +389,7 @@ export function reach(doc: FormulaDoc, vals: Values): string[] {
from(l.식);
}
for (const l of doc.금액 ?? []) {
if (!showOff && offLine(l, vals)) continue;
from(l.수량);
from(l.단가);
}
+8 -1
View File
@@ -86,6 +86,7 @@ export function createDocScreen(opt: DocScreenOptions) {
let given: Record<string, unknown> = {};
let openDef: string | null = null;
let auto: EditAt = null;
let showOff = false; // 선택으로 꺼진 금액 줄(고침 모드)
let seq = 0;
let asked = "";
let ctl: AbortController | null = null;
@@ -466,7 +467,7 @@ export function createDocScreen(opt: DocScreenOptions) {
const vals = answer.값 ?? {};
const vars = new Map((doc.변수 ?? []).map((v) => [varId(v), v]));
const lineSet = new Set((doc.식 ?? []).map((l) => String(l.기호 ?? "")).filter(Boolean));
const shown = reach(doc, vals);
const shown = reach(doc, vals, showOff);
const markMap = new Map<string, string>();
for (const m of answer.표지 ?? []) {
const at = m.기호 || (m as { 자리?: string }).자리;
@@ -483,6 +484,11 @@ export function createDocScreen(opt: DocScreenOptions) {
markBox();
const lines = buildLines({
admin: opt.admin,
showOff,
onShowOff: () => {
showOff = !showOff;
draw();
},
doc,
answer,
vars,
@@ -629,6 +635,7 @@ export function createDocScreen(opt: DocScreenOptions) {
key = next;
doc = answer = null;
given = {};
showOff = false;
openDef = null;
asked = "";
cache = createDocCache();
+2
View File
@@ -21,6 +21,8 @@ const TEXT = {
Total: ["합계", "Total"],
Need: ["정의 필요", "Needs definition"],
Unused: ["쓰지 않는 기호", "Unused symbols"],
Show_Off: ["선택으로 꺼진 줄 보기", "Show switched-off lines"],
Hide_Off: ["선택으로 꺼진 줄 숨김", "Hide switched-off lines"],
Save: ["수식 저장", "Save equation"],
Discard: ["고침 버림", "Discard"],
Saved: ["수식 저장됨", "Equation saved"],