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:
@@ -38,11 +38,25 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 탭 폭이 좁으면(1366 화면 등) 편집 위 · 산출근거 아래 */
|
||||
@container (max-width: 1240px) {
|
||||
/* 산출근거(요구 683) — 왼쪽을 내려도 화면에 고정 · 길면 자기 스크롤(높이 = 스크롤 영역 · fit()) */
|
||||
.lstk[data-edit="1"] .lstk__right {
|
||||
position: sticky;
|
||||
inset-block-start: 0;
|
||||
max-height: var(--lstk-view, 100vh);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 탭 폭이 좁으면(1366 화면 등) 편집 위 · 산출근거 아래(고정 없음) */
|
||||
@container (max-width: 1040px) {
|
||||
.lstk__areas {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.lstk[data-edit="1"] .lstk__right {
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 왼쪽 편집 ── */
|
||||
@@ -269,13 +283,9 @@
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
/* 짝 강조(679) — 왼쪽 카드 · 오른쪽 줄 같은 자리 */
|
||||
/* 짝 강조(679 · 684) — 왼쪽 카드 · 그룹 머리 · 오른쪽 줄 = 같은 바탕 채우기 */
|
||||
.lstk-line.is-pair,
|
||||
.lstk-item.is-pair {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.lstk-item.is-pair > .lstk-ihead,
|
||||
.lstk-out tr.is-pair > td {
|
||||
background: var(--color-accent-soft);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,15 @@ import {
|
||||
type StackItem,
|
||||
} from "../common_util/common_util_line_stack";
|
||||
import { placeNames } from "../common_util/common_util_line_stack_copy";
|
||||
import { addPicked, repick } from "../common_util/common_util_line_stack_pick";
|
||||
import {
|
||||
addPicked,
|
||||
aliasText,
|
||||
repick,
|
||||
tableAlias,
|
||||
} from "../common_util/common_util_line_stack_pick";
|
||||
import { attachDrag } from "./line_stack_drag";
|
||||
import { openStackPick, type StackTake } from "./line_stack_pick";
|
||||
import { buildBlock, insertTpl, type Tpl } from "./line_stack_rows";
|
||||
import { buildBlock } from "./line_stack_rows";
|
||||
import { lt, type LsKey } from "./line_stack_text";
|
||||
|
||||
export interface EditHooks {
|
||||
@@ -49,10 +54,8 @@ export interface EditHooks {
|
||||
name: (key: string) => string;
|
||||
/** 공식 묶음 펼친 자리(`항목.줄`) — 화면 상태만 */
|
||||
open: Set<string>;
|
||||
templates: () => Tpl[];
|
||||
/** 줄 · 항목 누름 — 오른쪽 짝 강조(679) */
|
||||
pair: (loc: string) => void;
|
||||
saveTpl: (tpl: Tpl) => void;
|
||||
/** M02 단가산출(804-4) — 다른 단가산출 · 일위대가 = 복사해 펼침 · M01 = 없음(M01 전체만) */
|
||||
take?: StackTake;
|
||||
}
|
||||
@@ -112,10 +115,14 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
]),
|
||||
],
|
||||
});
|
||||
// 표 값 기호 = 화면 이름(요구 683) — 칸 글은 화면 이름 ↔ 기호로 오감
|
||||
const alias = tableAlias(d);
|
||||
const shown = (t: unknown): string => aliasText(String(t ?? ""), alias);
|
||||
const real = (t: string): string => aliasText(t.trim(), alias, true);
|
||||
const qty = field(
|
||||
lt("Qty"),
|
||||
line.수량 ?? "",
|
||||
(t) => put((l) => (t.trim() ? (l.수량 = t.trim()) : delete l.수량)),
|
||||
shown(line.수량),
|
||||
(t) => put((l) => (t.trim() ? (l.수량 = real(t)) : delete l.수량)),
|
||||
"l",
|
||||
);
|
||||
const price: HTMLElement[] = [];
|
||||
@@ -145,12 +152,13 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
pick.title = lt("Repick");
|
||||
pick.dataset.repick = String(i);
|
||||
price.push(pick);
|
||||
} else price.push(field(lt("Price"), line.단가, (t) => put((l) => (l.단가 = t.trim())), "l"));
|
||||
} else
|
||||
price.push(field(lt("Price"), shown(line.단가), (t) => put((l) => (l.단가 = real(t))), "l"));
|
||||
const why = (h.answer.줄?.[i] as { 까닭?: string } | undefined)?.까닭;
|
||||
// 율 밑 번지(복사해 온 조각) — 칸 아래 읽는 이름(804-5)
|
||||
const places = /번지\./.test(`${line.단가} ${line.수량 ?? ""}`) ? placeNames(d, h.items) : null;
|
||||
const read = (t: unknown): string =>
|
||||
String(t ?? "").replace(/번지\.([^.\s()]+)\./g, (all, p: string) =>
|
||||
shown(t).replace(/번지\.([^.\s()]+)\./g, (all, p: string) =>
|
||||
places?.[p] ? `${places[p]} ` : all,
|
||||
);
|
||||
return [
|
||||
@@ -180,11 +188,6 @@ function addLine(h: EditHooks, at: number, kind: string): void {
|
||||
(p) => h.change((d, items) => void addPicked(d, items[at], kind as MoneyKind, p)),
|
||||
h.take,
|
||||
);
|
||||
if (kind.startsWith("tpl:")) {
|
||||
const t = h.templates().find((x) => x.id === kind.slice(4));
|
||||
if (t) h.change((d, items) => items[at].줄.push(insertTpl(d, t)));
|
||||
return;
|
||||
}
|
||||
h.change((d, items) => {
|
||||
const it = items[at];
|
||||
const n = (d.금액 ??= []).length;
|
||||
@@ -228,9 +231,6 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
})),
|
||||
{ value: "공식", text: lt("K_새공식") },
|
||||
{ value: "틀", text: lt("K_틀") },
|
||||
...h
|
||||
.templates()
|
||||
.map((t) => ({ value: `tpl:${t.id}`, text: `${lt("K_공식")} — ${t.name}` })),
|
||||
{ value: "글", text: lt("K_글") },
|
||||
],
|
||||
value: "",
|
||||
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
type StackDoc,
|
||||
} from "../common_util/common_util_line_stack";
|
||||
import { placeNames } from "../common_util/common_util_line_stack_copy";
|
||||
import { stackLevels } from "../common_util/common_util_line_stack_pick";
|
||||
import { katexNode, symTex, tex, valTex } from "./line_stack_tex";
|
||||
import { shownUnit, stackLevels, tableAlias } from "../common_util/common_util_line_stack_pick";
|
||||
import { katexNode, symTex, tex, valTex, type TexCtx } from "./line_stack_tex";
|
||||
import { lt } from "./line_stack_text";
|
||||
|
||||
type Cols = Partial<Record<"sum" | (typeof COSTS)[number], number>>;
|
||||
@@ -71,11 +71,11 @@ function exprNode(
|
||||
vals: Values,
|
||||
inline = false,
|
||||
cost?: string,
|
||||
place?: Record<string, string>,
|
||||
more: Partial<TexCtx> = {},
|
||||
) {
|
||||
const n = nodeOf(text);
|
||||
return n
|
||||
? katexNode(tex(n, { side, vals, inline, cost, place }))
|
||||
? katexNode(tex(n, { side, vals, inline, cost, ...more }))
|
||||
: el("span", { text: String(text ?? "") });
|
||||
}
|
||||
const hasVar = (text: unknown): boolean => {
|
||||
@@ -103,7 +103,8 @@ function rows(
|
||||
const shown = new Set(reach(doc, vals)); // 지금 탄 갈래로 닿는 기호만
|
||||
const steps = lineMap(doc);
|
||||
const place = placeNames(doc, stackOf(doc)); // 번지 읽는 이름(804-5)
|
||||
const unit = (id: string): string => vars.get(id)?.단위 ?? "";
|
||||
const unit = (id: string): string => shownUnit(vars.get(id)?.단위);
|
||||
const alias = tableAlias(doc); // 표 값 기호 화면 이름(요구 683)
|
||||
const subOf = (id: string): FormulaDoc | null => {
|
||||
const k = vars.get(id)?.연결;
|
||||
if (k?.갈래 !== "단가산출" || !k.키 || k.키.includes("{")) return null;
|
||||
@@ -153,7 +154,7 @@ function rows(
|
||||
for (const id of ids.filter((x) => shown.has(x))) {
|
||||
const v = vals[id];
|
||||
const t = lines.get(id);
|
||||
const head = katexNode(symTex(id));
|
||||
const head = katexNode(symTex(alias[id] ?? id));
|
||||
if (t !== undefined && hasVar(t)) {
|
||||
flush();
|
||||
const res = isNum(v)
|
||||
@@ -161,8 +162,8 @@ function rows(
|
||||
: missing(a, id);
|
||||
// 기호식 = 값식 = 결과 — 같은 꼴이 이어지면 한 번만(만약이 수 하나를 탄 줄)
|
||||
const n = nodeOf(t)!;
|
||||
const sym = tex(n, { side: "sym", vals });
|
||||
const val = tex(n, { side: "val", vals });
|
||||
const sym = tex(n, { side: "sym", vals, alias });
|
||||
const val = tex(n, { side: "val", vals, alias });
|
||||
const same = isNum(v) && val === valTex(v);
|
||||
push({
|
||||
h: [
|
||||
@@ -190,7 +191,10 @@ function rows(
|
||||
expand(id);
|
||||
continue;
|
||||
}
|
||||
const expr = t !== undefined && !isNum(Number(t)) ? [" = ", exprNode(t, "sym", vals)] : [];
|
||||
const expr =
|
||||
t !== undefined && !isNum(Number(t))
|
||||
? [" = ", exprNode(t, "sym", vals, false, undefined, { alias })]
|
||||
: [];
|
||||
const cell = el("span", {
|
||||
className: "lstk-pack",
|
||||
children: [
|
||||
@@ -234,7 +238,11 @@ function rows(
|
||||
const qtyNode = (cost?: string): (string | HTMLElement)[] =>
|
||||
qty === null
|
||||
? []
|
||||
: [" × ", exprNode(qty, "val", scope, true, cost), line.단위 ? ` ${line.단위}` : ""];
|
||||
: [
|
||||
" × ",
|
||||
exprNode(qty, "val", scope, true, cost, { alias }),
|
||||
line.단위 ? ` ${line.단위}` : "",
|
||||
];
|
||||
if (r.까닭 && r.금액 === null) {
|
||||
push({ h: [`${name} : — `, el("span", { className: "lstk-miss", text: r.까닭 })] });
|
||||
return { sub: false, cols: {} };
|
||||
@@ -264,11 +272,11 @@ function rows(
|
||||
const price =
|
||||
hasVar(line.단가) && kindOf(doc, i) === "율"
|
||||
? [
|
||||
exprNode(line.단가, "sym", scope, true, undefined, place),
|
||||
exprNode(line.단가, "sym", scope, true, undefined, { place, alias }),
|
||||
" = ",
|
||||
exprNode(line.단가, "val", scope, true),
|
||||
exprNode(line.단가, "val", scope, true, undefined, { alias }),
|
||||
]
|
||||
: [exprNode(line.단가, "val", scope, true)];
|
||||
: [exprNode(line.단가, "val", scope, true, undefined, { alias })];
|
||||
const c = String(line.비목);
|
||||
push({
|
||||
h: [`${name} : `, ...price, ...qtyNode(), ` = ${one(amt)}`],
|
||||
|
||||
@@ -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" : ""}`,
|
||||
|
||||
@@ -39,7 +39,6 @@ import { addTaken, type Taken } from "../common_util/common_util_line_stack_copy
|
||||
import type { Layer } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { buildEdit } from "./line_stack_edit";
|
||||
import { buildOut } from "./line_stack_out";
|
||||
import type { Tpl } from "./line_stack_rows";
|
||||
import { lt } from "./line_stack_text";
|
||||
import "./formula_doc.css";
|
||||
import "./line_stack.css";
|
||||
@@ -66,7 +65,6 @@ interface Loaded {
|
||||
판?: string;
|
||||
}
|
||||
|
||||
const TPL_KEY = "lineStack.myTemplates";
|
||||
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
||||
|
||||
export function createStackScreen(opt: StackScreenOptions) {
|
||||
@@ -80,6 +78,13 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
attrs: { "data-line-stack": "" },
|
||||
children: [head, notes, el("div", { className: "lstk__areas", children: [left, right] }), foot],
|
||||
});
|
||||
// 산출근거 고정 높이(요구 683) — 스크롤 조상(없으면 창) 높이 · 넓을 때만 고정(line_stack.css)
|
||||
const fit = (): void => {
|
||||
let p = root.parentElement;
|
||||
while (p && !/(auto|scroll)/.test(getComputedStyle(p).overflowY)) p = p.parentElement;
|
||||
root.style.setProperty("--lstk-view", `${p?.clientHeight ?? innerHeight}px`);
|
||||
};
|
||||
window.addEventListener("resize", fit);
|
||||
const cp = opt.copy;
|
||||
let key = "";
|
||||
let shown: string | null = null; // 연 키(같은 키 다시 그림 = 안 읽음 · 탭 바꿈 = force)
|
||||
@@ -136,13 +141,6 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
/* 캐시가 막혀도 화면 안 고침은 남음 */
|
||||
}
|
||||
};
|
||||
const templates = (): Tpl[] => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(TPL_KEY) ?? "[]") as Tpl[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// ── 셈 ──
|
||||
const usable = (): Record<string, unknown> => {
|
||||
@@ -323,9 +321,7 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
subDoc,
|
||||
name: (k) => cache.names.get(k) ?? "",
|
||||
open: blocks,
|
||||
templates,
|
||||
pair: (loc) => pair(loc, "left"),
|
||||
saveTpl: (t) => localStorage.setItem(TPL_KEY, JSON.stringify([...templates(), t])),
|
||||
take: takeCtx(),
|
||||
}),
|
||||
]
|
||||
@@ -348,6 +344,7 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
);
|
||||
foot.replaceChildren(...(doc.결과 ? [] : [docFoot(answer)]));
|
||||
pair(paired);
|
||||
fit();
|
||||
};
|
||||
|
||||
const take = (got: Loaded): void => {
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface TexCtx {
|
||||
cost?: string;
|
||||
/** 번지 읽는 이름(804-5) — 「번지.복사N.노무비」 → 「항목 이름 노무비」 */
|
||||
place?: Record<string, string>;
|
||||
/** 표 값 기호 화면 이름(요구 683 — 「Q_수량2^표」 → 「Q_작업반장」) */
|
||||
alias?: Record<string, string>;
|
||||
}
|
||||
|
||||
const HIDE = new Set(["반올림", "올림", "버림"]);
|
||||
@@ -90,12 +92,11 @@ export function tex(n: Node, c: TexCtx): string {
|
||||
case "str":
|
||||
return txt(`'${n[1]}'`);
|
||||
case "var": {
|
||||
if (c.side === "sym") return symTex(n[1]);
|
||||
const id = c.alias?.[n[1]] ?? n[1];
|
||||
if (c.side === "sym") return symTex(id);
|
||||
let v = c.vals[n[1]];
|
||||
if (v && typeof v === "object" && c.cost) v = (v as Values)[c.cost];
|
||||
return v === null || v === undefined || typeof v === "object"
|
||||
? miss(symTex(n[1]))
|
||||
: valTex(v);
|
||||
return v === null || v === undefined || typeof v === "object" ? miss(symTex(id)) : valTex(v);
|
||||
}
|
||||
case "field": {
|
||||
const [, of, key] = n;
|
||||
|
||||
@@ -11,18 +11,18 @@ const TEXT = {
|
||||
Total: ["합 계", "Total"],
|
||||
Sum: ["계", "Sum"],
|
||||
Sub_Sum: ["소 계", "Subtotal"],
|
||||
Add_Item: ["+ 항목", "+ Item"],
|
||||
Add_Item: ["+ 그룹", "+ Group"],
|
||||
Add_Line: ["+ 줄", "+ Line"],
|
||||
Del_Item: ["항목 지움", "Delete item"],
|
||||
Del_Item: ["그룹 지움", "Delete group"],
|
||||
Del: ["지움", "Delete"],
|
||||
Item: ["항목", "Item"],
|
||||
Item: ["그룹", "Group"],
|
||||
K_줄: ["줄", "Line"],
|
||||
Drag: ["끌어 옮기기", "Drag to move"],
|
||||
Fold_Item: ["항목 접기", "Collapse item"],
|
||||
Unfold_Item: ["항목 펼치기", "Expand item"],
|
||||
Fold_Item: ["그룹 접기", "Collapse group"],
|
||||
Unfold_Item: ["그룹 펼치기", "Expand group"],
|
||||
Dropped: ["지움 — 눌러 되돌리기", "deleted — click to undo"],
|
||||
Undo_Late: ["그 뒤 고친 것이 있어 못 되돌림", "Changed since — cannot undo"],
|
||||
Title: ["항목 이름(비우면 번호 없음)", "Item title"],
|
||||
Title: ["그룹 이름(비우면 번호 없음)", "Group title"],
|
||||
Gana: ["가나다", "Sub"],
|
||||
K_인력: ["인력", "Labor"],
|
||||
K_재료: ["재료", "Material"],
|
||||
@@ -33,6 +33,7 @@ const TEXT = {
|
||||
Take_Failed: ["복사 못 함", "Copy failed"],
|
||||
K_율: ["율 · 식", "Rate"],
|
||||
K_공식: ["공식", "Formula"],
|
||||
K_표값: ["표 값", "Table values"],
|
||||
K_새공식: ["공식 — 새 공식(빈 묶음)", "Formula — new"],
|
||||
K_틀: ["공식 — 틀 찾기(단가산출 결과)", "Formula — template"],
|
||||
K_글: ["글 · 근거", "Text"],
|
||||
@@ -45,10 +46,8 @@ const TEXT = {
|
||||
Find: ["찾기", "Find"],
|
||||
Pick: ["고름 ▾", "Pick ▾"],
|
||||
None_Found: ["찾은 것 없음", "Nothing found"],
|
||||
Unfold: ["식으로 풀기", "Unfold"],
|
||||
Unfold: ["식 고치기", "Edit formula"],
|
||||
Fold: ["접기", "Fold"],
|
||||
Save_Tpl: ["틀로 저장", "Save as template"],
|
||||
Tpl_Name: ["틀 이름", "Template name"],
|
||||
New_Formula: ["새 공식", "New formula"],
|
||||
My_Formula: ["내 공식", "My formula"],
|
||||
My_Tpl: ["내 틀", "My template"],
|
||||
@@ -58,7 +57,7 @@ const TEXT = {
|
||||
Row_Expr: ["값 또는 식", "Value or expr"],
|
||||
Bad_Name: ["못 쓰는 이름 · 같은 이름 있음", "Bad or duplicate name"],
|
||||
Empty_Item: ["줄 없음 — [+ 줄]", "No lines — [+ Line]"],
|
||||
Empty: ["빈 단가산출 — [+ 항목] 으로 시작", "Empty — start with [+ Item]"],
|
||||
Empty: ["빈 단가산출 — [+ 그룹] 으로 시작", "Empty — start with [+ Group]"],
|
||||
Text_Ph: ["< 품셈 근거 > · ※ 메모 · 별산", "Note"],
|
||||
Notes: [
|
||||
"줄 쌓기에서 못 고치는 칸(보존됨 · 수식 탭에서 고침)",
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
* 산출근거 수준 = 항목(1. 2.) 1 · 가나다 2 · 줄 = 항목 + 1 · 제목 없는 항목 줄 = 1
|
||||
* ========================================================================== */
|
||||
|
||||
import { names, varId, type FormulaDoc, type Link } from "./common_util_formula_doc";
|
||||
import {
|
||||
names,
|
||||
renameText,
|
||||
splitId,
|
||||
symId,
|
||||
varId,
|
||||
type FormulaDoc,
|
||||
type Link,
|
||||
} from "./common_util_formula_doc";
|
||||
import { findRow, type DocTable } from "./common_util_formula_doc_table";
|
||||
import { evalNode } from "./common_util_formula_doc_view";
|
||||
import {
|
||||
@@ -14,6 +22,7 @@ import {
|
||||
isOption,
|
||||
newVar,
|
||||
nodeOf,
|
||||
symbols,
|
||||
type MoneyKind,
|
||||
type StackItem,
|
||||
} from "./common_util_line_stack";
|
||||
@@ -144,3 +153,48 @@ export function stackLevels(items: StackItem[]): { item: number; line: number }[
|
||||
return { item, line: item + 1 };
|
||||
});
|
||||
}
|
||||
|
||||
/** 표 값 기호 화면 이름(804-5 · 요구 683) — 「Q_수량2^표」 → 「Q_작업반장」(고른 줄 글 조건 첫 낱말) · 글 조건 없으면 「^표」만 뗌
|
||||
* · 「^표」 기호만 · 겹치면 2 · 3 · 문서 기호는 그대로(화면만 · 칸 글은 `aliasText` 로 오감) */
|
||||
export function tableAlias(doc: FormulaDoc): Record<string, string> {
|
||||
const taken = new Set([
|
||||
...symbols(doc),
|
||||
...(doc.금액 ?? []).map((l) => String(l.기호 ?? "")),
|
||||
"노무비",
|
||||
"재료비",
|
||||
"경비",
|
||||
]);
|
||||
const out: Record<string, string> = {};
|
||||
for (const v of doc.변수 ?? []) {
|
||||
const link = v.연결;
|
||||
if (link?.갈래 !== "표") continue;
|
||||
const id = varId(v);
|
||||
const [s, lo, up] = splitId(id);
|
||||
const said = Object.values(link.조건 ?? {})
|
||||
.map(String)
|
||||
.find((t) => /^'.+'$/.test(t));
|
||||
const label = said?.slice(1, -1).split(/[\s(,·/]/)[0] ?? "";
|
||||
if (up !== "표") continue; // 「^표」 꼬리 기호만(r_인력 등 손 이름은 그대로)
|
||||
const base = label || lo.replace(/\d+$/, "") || lo; // 번호 꼬리 뗌(겹치면 다시 2 · 3)
|
||||
for (let n = 1; n < 100; n++) {
|
||||
const now = symId(s, n > 1 ? `${base}${n}` : base);
|
||||
const node = nodeOf(now);
|
||||
if (taken.has(now) || node?.[0] !== "var" || node[1] !== now) continue;
|
||||
taken.add(now);
|
||||
out[id] = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 식 글의 기호를 화면 이름으로(또는 `back` = 화면 이름을 기호로) */
|
||||
export function aliasText(text: string, alias: Record<string, string>, back = false): string {
|
||||
let t = text;
|
||||
for (const [id, now] of Object.entries(alias))
|
||||
t = String(back ? renameText(t, now, id) : renameText(t, id, now));
|
||||
return t;
|
||||
}
|
||||
|
||||
/** 표 칸 단위 「줄 따름」(줄마다 다른 단위) = 화면에 안 보임(요구 683) */
|
||||
export const shownUnit = (u: unknown): string => (u === "줄 따름" ? "" : String(u ?? ""));
|
||||
|
||||
@@ -213,6 +213,32 @@ function moveAll({ 문서, 옮김 }) {
|
||||
}
|
||||
const placeOf = ({ 문서 }) => cp.placeNames(문서, ls.stackOf(문서));
|
||||
|
||||
/* 683 표 값 기호 화면 이름 — 별칭 · 글 왕복(화면 이름 → 기호 = 처음 식과 같은 나무) */
|
||||
const pk2 = util("common_util_line_stack_pick");
|
||||
const fd = util("common_util_formula_doc");
|
||||
function aliasOf({ 문서 }) {
|
||||
const alias = pk2.tableAlias(문서);
|
||||
const texts = [
|
||||
...(문서.식 || []).map((l) => l.식),
|
||||
...(문서.금액 || []).flatMap((l) => [l.수량, l.단가]),
|
||||
].filter((t) => typeof t === "string" && t);
|
||||
const bad = [];
|
||||
for (const t of texts) {
|
||||
let want;
|
||||
try {
|
||||
want = fd.textOf(fd.parse(t));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const shown = pk2.aliasText(t, alias);
|
||||
const back = pk2.aliasText(shown, alias, true);
|
||||
if (fd.textOf(fd.parse(back)) !== want) bad.push(t);
|
||||
if (Object.keys(alias).some((id) => fd.names(fd.parse(shown)).includes(id)))
|
||||
bad.push(`남음:${t}`);
|
||||
}
|
||||
return { 별칭: alias, 틀림: bad };
|
||||
}
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const job = {
|
||||
왕복: roundTrip,
|
||||
@@ -224,5 +250,6 @@ const job = {
|
||||
복사: takeIn,
|
||||
옮김: moveAll,
|
||||
번지: placeOf,
|
||||
별칭: aliasOf,
|
||||
}[input.할일];
|
||||
process.stdout.write(JSON.stringify(input.문서들.map(job)));
|
||||
|
||||
@@ -230,3 +230,28 @@ def test_끌어옮김_번지이름(world):
|
||||
places = {m["번지"] for m in g["문서"]["금액"] if m.get("번지")}
|
||||
assert places and set(names) == places
|
||||
assert all(v and not v.startswith("복사") for v in names.values()), names
|
||||
|
||||
|
||||
def test_표값_화면이름_전수():
|
||||
"""요구 683 — 표 값 기호(「Q_수량2^표」) = 화면 이름(「Q_작업반장」) · 겹침 없음 · 식 글 왕복 = 같은 식(1,518 전수)."""
|
||||
docs = dc.folder()
|
||||
keys = sorted(docs)
|
||||
got = _ts("별칭", [{"문서": docs[k]} for k in keys])
|
||||
named = 0
|
||||
for k, g in zip(keys, got):
|
||||
assert not g["틀림"], (k, g["틀림"][:3])
|
||||
alias = g["별칭"]
|
||||
assert len(set(alias.values())) == len(alias), k # 겹치면 2 · 3
|
||||
assert all("^표" not in v and k.endswith("^표") for k, v in alias.items()), k
|
||||
named += len(alias)
|
||||
assert named > 500
|
||||
gc = dict(zip(keys, got))[FORM]["별칭"]
|
||||
assert gc and "Q_보통인부22" not in gc.values() # 번호 꼬리 뗀 뒤 2 · 3
|
||||
# 문서 기호는 그대로(화면만) — 원본 문서 변수에 별칭 이름 없음
|
||||
names = {
|
||||
v["기호"]
|
||||
+ (f"_{v['아래']}" if v.get("아래") else "")
|
||||
+ (f"^{v['위']}" if v.get("위") else "")
|
||||
for v in docs[FORM]["변수"]
|
||||
}
|
||||
assert not set(gc.values()) & names
|
||||
|
||||
Reference in New Issue
Block a user