feat(804-5): 줄 쌓기 표 값 이름 · 산출근거 고정 · 그룹 글 · 짝 강조 채우기 · 틀로 저장 걷음(요구 683~685)

- 표 값 기호 화면 이름 = 고른 줄 이름(Q_수량2^표 → Q_특별인부 · 겹치면 2 · 3) — 왼쪽 · 산출근거 같음 · 칸 글은 화면 이름 ↔ 기호로 오감(문서 기호 그대로)
- 단위 「줄 따름」 안 보임 · 표 값만 든 묶음 머리 = 「표 값」 · [식으로 풀기] → 「식 고치기」
- 산출근거 = 넓을 때 화면 고정 + 자기 스크롤(높이 = 스크롤 영역) · 나란히 놓는 탭 폭 1240 → 1040
- 글 「항목」 → 「그룹」(그룹 이름 · + 그룹 · 그룹 지움)
- 짝 강조 = 왼쪽 카드 · 그룹 머리 · 오른쪽 줄 모두 같은 바탕 채우기(테두리 걷음)
- [틀로 저장] · 내 틀 목록(브라우저 저장) 걷음 · 공식 틀 고르기 = M01 단가산출 결과만
- 시험 test_804_4 표 값 화면 이름 1,518 전수(겹침 없음 · 식 글 왕복 같음) 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 08:42:41 +09:00
co-authored by Claude Opus 5.5
parent af576e5a7a
commit 6f678fb91b
10 changed files with 219 additions and 114 deletions
+35 -51
View File
@@ -3,14 +3,14 @@
* 줄 쌓기 공식 묶음(PLAN 804-2 · 요구 675) — 「이름 = 값」 · 「이름 = 식」 줄 묶음 · 틀 이름표 · [식으로 풀기] · [틀로 저장]
* 접힘 = 값 줄은 값 칸(연결 값을 고쳐 적으면 숫자 = 고정값 · 요구 664) · 식 줄은 「이름 = 식 = 값」 글 · 단가산출 결과 기호 = [식으로 풀기]
* 풀림 = 줄마다 이름 · 식 · 단위 칸 + 지움 · [+ 줄] — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결
* 틀 = 단가산출 결과 문서(굴착기 Q 등)를 이 문서 식으로 옮김(`unfold`) · 내 틀 = 이 브라우저 저장(localStorage) — 꼴이 바뀌면
* 「내 틀 (틀 이름 에서 변경)」 · 빈 틀 = [찾아 넣기](돈 아닌 단가산출 결과 — 작업량 등)
* 틀 = 단가산출 결과 문서(굴착기 Q 등)를 이 문서 식으로 옮김(`unfold`) — 꼴이 바뀌면 「내 틀 (틀 이름 에서 변경)」 ·
* 빈 틀 = [찾아 넣기](M01 단가산출 결과 — 작업량 등) · [틀로 저장] · 내 틀 목록 걷음(요구 685) · 표 값만 = 머리 「표 값」
* 표 값(804-3) = [표 · 칸] 단추 → 표 고르기 창(`line_stack_pick.ts`) · 값 칸 누름 = 그 줄 · 칸(`pickCell`)
* ========================================================================== */
import { createButton } from "@ui/ui_template_button";
import { eqText } from "@ui/ui_template_equation";
import { askText, el, showToast } from "@ui/ui_template_elements";
import { el, showToast } from "@ui/ui_template_elements";
import { createInputField } from "@ui/ui_template_input";
import {
DocError,
@@ -27,32 +27,25 @@ import {
setRow,
shapeOf,
unfold,
type StackDoc,
type StackItem,
type StackLine,
} from "../common_util/common_util_line_stack";
import { boundConds, currentRow, pickCell } from "../common_util/common_util_line_stack_pick";
import {
aliasText,
boundConds,
currentRow,
pickCell,
shownUnit,
tableAlias,
} from "../common_util/common_util_line_stack_pick";
import { openFindPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Find_Pick";
import type { EditHooks } from "./line_stack_edit";
import { openTablePick } from "./line_stack_pick";
import { lt } from "./line_stack_text";
/** 내 틀 — 기호 · 식 조각(이 브라우저) */
export interface Tpl {
id: string;
name: string;
문서: FormulaDoc;
}
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
type Block = Extract<StackLine, { 꼴: "공식" }>;
/** 틀 넣기 — 새 공식 묶음 줄 */
export function insertTpl(d: StackDoc, t: Tpl): Block {
const moved = unfold(d, t.문서);
return { 꼴: "공식", 기호: moved, 틀: `my:${t.id}`, 틀꼴: shapeOf(d, moved) };
}
const LINK_TAG: Record<string, string> = {
표: "표",
요소: "요소",
@@ -81,9 +74,15 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM
const vars = new Map((d.변수 ?? []).map((v) => [varId(v), v]));
const lines = new Map((d.식 ?? []).map((l) => [String(l.기호 ?? ""), String(l.식 ?? "")]));
const blockOf = (items: StackItem[]): Block => items[i].줄[j] as Block;
// 표 값 기호 = 화면 이름(요구 683 · 문서 기호 그대로) — 칸 글은 화면 이름 ↔ 기호로 오감
const alias = tableAlias(d);
const show = (id: string): string => alias[id] ?? id;
const shownText = (t: string): string => aliasText(t, alias);
const realText = (t: string): string => aliasText(t, alias, true);
const valText = (id: string): string => {
const v = vals[id];
if (isNum(v) || typeof v === "string") return `${eqText(v)} ${vars.get(id)?.단위 ?? ""}`.trim();
if (isNum(v) || typeof v === "string")
return `${eqText(v)} ${shownUnit(vars.get(id)?.단위)}`.trim();
if (v && typeof v === "object")
return ["노무비", "재료비", "경비"]
.map((c) => `${c} ${eqText((v as Record<string, unknown>)[c])}`)
@@ -93,12 +92,11 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM
// ── 이름표 ──
const tplName = (k: string): string =>
k.startsWith("my:")
? (h.templates().find((t) => `my:${t.id}` === k)?.name ?? lt("My_Tpl"))
: (h.subDoc(k)?.이름 ?? (h.name(k) || k));
k.startsWith("my:") ? lt("My_Tpl") : (h.subDoc(k)?.이름 ?? (h.name(k) || k)); // my: = 옛 내 틀(걷음 · 요구 685)
const changed = !!line.틀 && !!line.틀꼴 && shapeOf(d, line.기호) !== line.틀꼴;
const tableOnly = line.기호.every((id) => vars.get(id)?.연결?.갈래 === "표" && !lines.has(id));
const label = !line.틀
? lt("K_공식")
? lt(tableOnly && line.기호.length ? "K_표값" : "K_공식")
: changed
? `${lt("My_Tpl")} (${tplName(line.틀)} ${lt("Changed_From")})`
: tplName(line.틀);
@@ -116,28 +114,6 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM
h.change(() => undefined);
},
}),
createButton({
label: lt("Save_Tpl"),
variant: "ghost",
size: 24,
onClick: () =>
void askText({ message: lt("Tpl_Name"), value: label }).then((name) => {
if (!name?.trim()) return;
const ids = new Set(line.기호);
const id = `t${Date.now()}`;
h.saveTpl({
id,
name: name.trim(),
문서: {
변수: clone((d.변수 ?? []).filter((v) => ids.has(varId(v)))),
식: clone((d.식 ?? []).filter((l) => ids.has(String(l.기호 ?? "")))),
},
});
h.change((doc, items) =>
Object.assign(blockOf(items), { 틀: `my:${id}`, 틀꼴: shapeOf(doc, line.기호) }),
);
}),
}),
],
});
head.dataset.tpl = changed ? "changed" : line.틀 ? "tpl" : "";
@@ -246,16 +222,20 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM
}),
}),
);
const name = el("b", { className: "lstk-sym", text: id });
const name = el("b", { className: "lstk-sym", text: show(id) });
const valueOf =
expr ??
(link?.갈래 === "숫자" ? String(link.값 ?? "") : isNum(vals[id]) ? String(vals[id]) : "");
let body: (HTMLElement | string)[];
if (open)
body = [
input(id, lt("Row_Name"), "lstk-f-s", (t) => renameRow(id, t)),
input(show(id), lt("Row_Name"), "lstk-f-s", (t) =>
renameRow(id, t.trim() === show(id) ? id : t),
),
"=",
input(valueOf, lt("Row_Expr"), "lstk-f-l", (t) => h.change((doc) => setRow(doc, id, t))),
input(shownText(valueOf), lt("Row_Expr"), "lstk-f-l", (t) =>
h.change((doc) => setRow(doc, id, realText(t))),
),
input(v?.단위 ?? "", lt("Unit"), "lstk-f-s", (t) =>
h.change((doc) => {
let x = doc.변수?.find((y) => varId(y) === id);
@@ -274,15 +254,19 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM
else if (expr !== undefined || sub || link?.갈래 === "단가산출")
body = [
name,
`= ${expr ?? ""}${expr ? " =" : ""}`,
`= ${shownText(expr ?? "")}${expr ? " =" : ""}`,
el("span", { className: "lstk-val", text: valText(id) }),
];
else
body = [
name,
"=",
input(valueOf, valText(id), "lstk-f-m", (t) => h.change((doc) => setRow(doc, id, t))),
el("span", { className: "lstk-unit", text: v?.단위 ?? "" }),
input(shownText(valueOf), valText(id), "lstk-f-m", (t) =>
h.change((doc) => setRow(doc, id, realText(t))),
),
...(shownUnit(v?.단위)
? [el("span", { className: "lstk-unit", text: shownUnit(v?.단위) })]
: []),
];
const row = el("div", {
className: `lstk-frow ${open ? "is-open" : ""}`,