- 표 값(요구 678-1): 「표」 꼬리표가 글뿐이라 눌러도 안 열림 → [표 · 고른 줄 · 칸 ▾] 단추 = 품셈 표 창(값 칸 누름 = 그 줄 · 칸) 옵션에 매인 조건(폼타이규격 등) = 그 줄만 고름 · 다른 줄 막힘 + 「위 옵션에서 고름」 안내 · M02 길 표 값 글 수도 고름 - 산출근거 들여쓰기(678-2): 머리 0 · 항목 1 · 가나다 2 · 줄 = 항목 + 1 · 펼친 블록 = 그 줄 + 1(수준 = stackLevels) - 찾아 넣기(678-3): [+ 줄] 인력 · 재료 · 기계 · 다른 단가산출 = M02 [항목 추가] 창 그대로(그 종류만) · 고르면 줄 생김(닫으면 없음) 있는 줄 = [찾아 바꾸기](같은 로직 기계 = 지역 · 보정작업 넘김 그대로) · 옛 찾기 칸 + 고름 목록 · 기종 코드 고르개 걷음 빈 틀 = [찾아 넣기](돈 아닌 단가산출 결과만 — Find_Pick results) - 짝 강조(679-1): 오른쪽 줄 누름 = 왼쪽 카드 강조 + 보이는 자리로 · 왼쪽 누름 = 오른쪽 같은 줄 강조 - 기계 이름(679-2): 기계 줄 = 기종 이름 · 규격 · machine-hourly 답에 로직 키 · 원문번호 더함(기존 칸 그대로) - 모델 common_util_line_stack_pick.ts(boundConds · currentRow · pickCell · addPicked · repick · stackLevels) - 시험 test_804_2 넓힘 3(표 고름 = 값 + 출처 · 서버 셈 같음 · 찾아 넣기 넷 = 요소 키 · 서버 금액 · 다시 고름 · 수준) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
/* =============================================================================
|
||
* line_stack_rows.ts
|
||
* 줄 쌓기 공식 묶음(PLAN 804-2 · 요구 675) — 「이름 = 값」 · 「이름 = 식」 줄 묶음 · 틀 이름표 · [식으로 풀기] · [틀로 저장]
|
||
* 접힘 = 값 줄은 값 칸(연결 값을 고쳐 적으면 숫자 = 고정값 · 요구 664) · 식 줄은 「이름 = 식 = 값」 글 · 단가산출 결과 기호 = [식으로 풀기]
|
||
* 풀림 = 줄마다 이름 · 식 · 단위 칸 + 지움 · [+ 줄] — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결
|
||
* 틀 = 단가산출 결과 문서(굴착기 Q 등)를 이 문서 식으로 옮김(`unfold`) · 내 틀 = 이 브라우저 저장(localStorage) — 꼴이 바뀌면
|
||
* 「내 틀 (틀 이름 에서 변경)」 · 빈 틀 = [찾아 넣기](돈 아닌 단가산출 결과 — 작업량 등)
|
||
* 표 값(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 { 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 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 { 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> = {
|
||
표: "표",
|
||
요소: "요소",
|
||
단가산출: "단가산출",
|
||
선택: "선택",
|
||
입력: "입력",
|
||
};
|
||
|
||
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;
|
||
const valText = (id: string): string => {
|
||
const v = vals[id];
|
||
if (isNum(v) || typeof v === "string") return `${eqText(v)} ${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:")
|
||
? (h.templates().find((t) => `my:${t.id}` === k)?.name ?? lt("My_Tpl"))
|
||
: (h.subDoc(k)?.이름 ?? (h.name(k) || k));
|
||
const changed = !!line.틀 && !!line.틀꼴 && shapeOf(d, line.기호) !== line.틀꼴;
|
||
const label = !line.틀
|
||
? lt("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);
|
||
},
|
||
}),
|
||
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" : "";
|
||
|
||
// ── 표 값 — [표 · 칸] → 표 고르기 ──
|
||
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: 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(valueOf, lt("Row_Expr"), "lstk-f-l", (t) => h.change((doc) => setRow(doc, id, 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,
|
||
`= ${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?.단위 ?? "" }),
|
||
];
|
||
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];
|
||
}
|