Files
Aislo/A00_Common/line_stack_rows.ts
T
eomsangdonandClaude Opus 5.5 6f678fb91b 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>
2026-11-08 08:42:41 +09:00

295 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* line_stack_rows.ts
* 줄 쌓기 공식 묶음(PLAN 804-2 · 요구 675) — 「이름 = 값」 · 「이름 = 식」 줄 묶음 · 틀 이름표 · [식으로 풀기] · [틀로 저장]
* 접힘 = 값 줄은 값 칸(연결 값을 고쳐 적으면 숫자 = 고정값 · 요구 664) · 식 줄은 「이름 = 식 = 값」 글 · 단가산출 결과 기호 = [식으로 풀기]
* 풀림 = 줄마다 이름 · 식 · 단위 칸 + 지움 · [+ 줄] — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결
* 틀 = 단가산출 결과 문서(굴착기 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 { el, showToast } from "@ui/ui_template_elements";
import { createInputField } from "@ui/ui_template_input";
import {
DocError,
rename,
splitId,
varId,
type FormulaDoc,
} from "../common_util/common_util_formula_doc";
import { isNum } from "../common_util/common_util_formula_doc_calc";
import {
dropSyms,
freshId,
newVar,
setRow,
shapeOf,
unfold,
type StackItem,
type StackLine,
} from "../common_util/common_util_line_stack";
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";
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
type Block = Extract<StackLine, { 꼴: "공식" }>;
const LINK_TAG: Record<string, string> = {
표: "표",
요소: "요소",
단가산출: "단가산출",
선택: "선택",
입력: "입력",
};
const input = (
value: string,
placeholder: string,
cls: string,
set: (t: string) => void,
): HTMLElement => {
const f = createInputField({ value, placeholder, compact: true, size: 24 });
f.root.classList.add(cls);
f.input.addEventListener("change", () => set(f.input.value));
return f.root;
};
export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTMLElement[] {
const d = h.doc;
const at = `${i}.${j}`;
const open = h.open.has(at);
const vals = (h.answer.값 ?? {}) as Record<string, unknown>;
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)} ${shownUnit(vars.get(id)?.단위)}`.trim();
if (v && typeof v === "object")
return ["노무비", "재료비", "경비"]
.map((c) => `${c} ${eqText((v as Record<string, unknown>)[c])}`)
.join(" · ");
return `— ${h.answer.빈값?.[id] ?? ""}`.trim();
};
// ── 이름표 ──
const tplName = (k: string): string =>
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(tableOnly && line.기호.length ? "K_표값" : "K_공식")
: changed
? `${lt("My_Tpl")} (${tplName(line.틀)} ${lt("Changed_From")})`
: tplName(line.틀);
const head = el("div", {
className: "lstk-fhead",
children: [
el("span", { className: "lstk-kind", text: label }),
createButton({
label: open ? lt("Fold") : lt("Unfold"),
variant: "ghost",
size: 24,
onClick: () => {
if (open) h.open.delete(at);
else h.open.add(at);
h.change(() => undefined);
},
}),
],
});
head.dataset.tpl = changed ? "changed" : line.틀 ? "tpl" : "";
// ── 표 값 — [표 · 칸] → 표 고르기 ──
const tableBtn = (id: string, link: NonNullable<FormulaDoc["변수"]>[number]["연결"] & object) => {
const b = createButton({
label:
[
lt("Tbl"),
...Object.values(link.조건 ?? {}).flatMap((t) =>
/^'.*'$/.test(String(t)) ? [String(t).slice(1, -1)] : [],
),
link.칸,
]
.filter(Boolean)
.join(" · ") + " ▾", // 고른 줄(글 조건) · 칸
variant: "ghost",
size: 24,
onClick: () =>
void h.table(String(link.표 ?? "")).then((t) => {
if (!t) return void showToast(lt("Tbl_Missing"), "warning");
openTablePick({
table: t,
now: currentRow(t, link, vals),
col: String(link.칸 ?? ""),
bound: boundConds(d, link),
done: (row, col) => h.change((doc) => pickCell(doc, id, t, row, col)),
});
}),
});
b.dataset.tablePick = id;
b.title = h.name(String(link.표 ?? "")) || String(link.표 ?? "");
return b;
};
// ── 줄 ──
const renameRow = (old: string, now: string): void => {
const t = now.trim();
if (!t || t === old) return;
try {
const copy = clone(d);
const [s, lo, up] = splitId(t);
rename(copy, old, s, lo, up);
} catch (e) {
if (e instanceof DocError) return void showToast(lt("Bad_Name"), "warning");
throw e;
}
h.change((doc, items) => {
const [s, lo, up] = splitId(t);
rename(doc, old, s, lo, up);
for (const it of items)
for (const l of it.줄) if (l.꼴 === "공식") l.기호 = l.기호.map((x) => (x === old ? t : x));
});
};
const unfoldRow = (id: string, sub: FormulaDoc, key: string): void =>
h.change((doc, items) => {
const moved = unfold(doc, sub, id);
const blk = blockOf(items);
blk.기호 = blk.기호.filter((x) => x !== id);
items[i].줄.splice(j + 1, 0, { 꼴: "공식", 기호: moved, 틀: key, 틀꼴: shapeOf(doc, moved) });
if (!blk.기호.length) items[i].줄.splice(j, 1);
});
const rows = line.기호.map((id) => {
const v = vars.get(id);
const link = v?.연결;
const expr = lines.get(id);
const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null;
const tag =
link?.갈래 === "표" && !expr
? tableBtn(id, link)
: link && LINK_TAG[link.갈래 ?? ""] && !expr
? el("span", {
className: "lstk-tag",
text: `${LINK_TAG[link.갈래!]}${link.키 && !link.키.includes("{") ? ` · ${sub?.이름 ?? h.name(link.키) ?? ""}` : ""}`,
})
: null;
const tools: HTMLElement[] = [];
if (sub?.결과 && link?.키)
tools.push(
createButton({
label: lt("Unfold"),
variant: "ghost",
size: 24,
onClick: () => unfoldRow(id, sub, link.키!),
}),
);
if (link?.갈래 === "단가산출" && !link.키)
tools.push(
createButton({
label: lt("Find_Title"),
variant: "ghost",
size: 24,
onClick: () =>
openFindPicker({
layer: "system",
owner: null,
title: lt("K_틀"),
kinds: ["logic"],
results: true,
logic: (x) =>
h.change((doc) => {
const y = doc.변수?.find((z) => varId(z) === id);
if (y) y.연결 = { 갈래: "단가산출", 키: x.키, 입력: {} };
}),
}),
}),
);
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(show(id), lt("Row_Name"), "lstk-f-s", (t) =>
renameRow(id, t.trim() === show(id) ? 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);
if (!x) (doc.변수 ??= []).push((x = newVar(id, { 갈래: "식" })));
x.단위 = t.trim();
}),
),
el("span", { className: "lstk-val", text: `= ${valText(id)}` }),
createButton({
label: "×",
variant: "danger-ghost",
size: 24,
onClick: () => h.change((doc) => dropSyms(doc, [id])),
}),
];
else if (expr !== undefined || sub || link?.갈래 === "단가산출")
body = [
name,
`= ${shownText(expr ?? "")}${expr ? " =" : ""}`,
el("span", { className: "lstk-val", text: valText(id) }),
];
else
body = [
name,
"=",
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" : ""}`,
children: [...body, ...(tag ? [tag] : []), ...tools],
});
row.dataset.sym = id;
return row;
});
const more = open
? [
createButton({
label: lt("Add_Row"),
variant: "ghost",
size: 24,
onClick: () =>
h.change((doc, items) => {
const id = freshId(doc, "x");
(doc.변수 ??= []).push(newVar(id, { 갈래: "숫자", 값: 0 }));
blockOf(items).기호.push(id);
}),
}),
]
: [];
return [head, ...rows, ...more];
}