feat(796-2e-2): 더한 기초 블록 [연결 끊기] · [다시 잇기] 화면 — 수식 탭 소계 블록 머리 상태 · 단추 · 끊은 블록 속 수식 고침
- 소계 표 부품에 덧단추 자리(EqSumHooks head · body) · EqPage.sum 으로 페이지가 끼움 - M02 수식 탭: 블록 머리 「연결」 · 「끊음」 + [연결 끊기] · [다시 잇기](확인창) · 끊은 블록 속 = 같은 수식 부품 · [단가 갱신] · [재생성] 보고에 끊은 기초 한 줄 - test_799_1_subtotals 낡은 문자열 비교(createEqSum(eq, open) 통째)를 인자 앞부분 비교로 고침 — 부품 호출에 덧단추 인자가 붙어서 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,13 @@ import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { createEqChain } from "@ui/ui_template_equation_line";
|
||||
import { createEqSum } from "@ui/ui_template_equation_sum";
|
||||
import { eqText, type EqLine, type EqTree, type EqVarRow } from "@ui/ui_template_equation";
|
||||
import {
|
||||
eqText,
|
||||
type EqLine,
|
||||
type EqSumHooks,
|
||||
type EqTree,
|
||||
type EqVarRow,
|
||||
} from "@ui/ui_template_equation";
|
||||
import {
|
||||
el,
|
||||
hideLoadingOverlay,
|
||||
@@ -119,6 +125,8 @@ export interface EqPage {
|
||||
keep: (edits: EqEdit[]) => void;
|
||||
values: () => Record<string, string>;
|
||||
setValue: (name: string, value: string | null) => void;
|
||||
/** 소계 표 덧단추 자리(796-2e M02 더한 기초 [연결 끊기]) */
|
||||
sum?: EqSumHooks;
|
||||
}
|
||||
|
||||
export interface LabEquationOptions {
|
||||
@@ -458,7 +466,7 @@ export function createLabEquation(opt: LabEquationOptions) {
|
||||
return;
|
||||
}
|
||||
const edit = canEdit();
|
||||
const sum = createEqSum(eq, open);
|
||||
const sum = createEqSum(eq, open, page?.sum);
|
||||
body.replaceChildren(
|
||||
createEqChain(eq.줄, {
|
||||
path: "",
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
* 저장 = 상세 [저장](서버가 다시 얹어 검사 · 셈) · 옵션 = 상세 옵션 값(`onInput`)
|
||||
* 위 줄 = 로직 사본 — 「M01 새 판 있음」 배지 · [단가 갱신] · [재생성](`…/sangeun/copy/price|regen` → 문서 `사본할일` · [저장] 때 서버 적용)
|
||||
* 가져온 식 = 「엑셀 원래」 금액 글(셈 답 `원래` = 엑셀 금액)
|
||||
* 더한 기초 블록(796-2e · 요구 659-3) — 머리 「연결」 + [연결 끊기](`…/sangeun/block/unlink` → 그 더한줄 `끊음` = 이 SK 안 자기 사본) ·
|
||||
* 「끊음」 + [다시 잇기](`…/block/relink` → 고친 것 버림 · 원본 따라감) · 끊은 블록 속 = 같은 수식 부품(길 몸 `블록`) · 고침 = 그 더한줄 `수식고침`
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import type { EqSumHooks, EqSumRow } from "@ui/ui_template_equation";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
@@ -28,6 +31,19 @@ const TEXT = {
|
||||
Job: ["{n} · [저장] 때 적용", "{n} · applied on save"],
|
||||
Excel: ["엑셀 원래", "Excel original"],
|
||||
Lost: ["못 얹은 고침", "Not re-applied"],
|
||||
Linked: ["연결", "Linked"],
|
||||
Cut: ["끊음", "Unlinked"],
|
||||
Unlink: ["연결 끊기", "Unlink"],
|
||||
Relink: ["다시 잇기", "Relink"],
|
||||
AskUnlink: [
|
||||
"이 기초를 이 단가산출 안 사본으로 굳힘 — 끊은 뒤 원본(M01) 바뀜은 따라오지 않음 (금액은 그대로)",
|
||||
"Freeze this basis as a copy inside this item — later source changes will not follow (amount unchanged)",
|
||||
],
|
||||
AskRelink: [
|
||||
"이 기초에서 고친 것을 버리고 원본(M01)을 따라감으로 돌아감",
|
||||
"Discard edits to this basis and follow the source again",
|
||||
],
|
||||
KeptCut: ["끊은 기초 {n} 는 줄 그대로 · 자원 단가만", "Unlinked {n}: prices only"],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
const t = (k: keyof typeof TEXT, n = ""): string =>
|
||||
(TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0]).replace("{n}", n);
|
||||
@@ -50,6 +66,7 @@ interface CopyReply {
|
||||
후: Record<string, number> | null;
|
||||
바뀐자원?: unknown[];
|
||||
못얹은?: { cid: string; 까닭: string }[];
|
||||
끊은기초?: string[];
|
||||
}
|
||||
|
||||
export interface SkEquationOptions {
|
||||
@@ -72,6 +89,14 @@ export function createSangeunEquation(opt: SkEquationOptions) {
|
||||
const bar = el("div", { attrs: { "data-sk-copy": "" } });
|
||||
let last: SkEquationDraw | null = null;
|
||||
const doc = (): SkDoc => last?.doc ?? ({} as SkDoc);
|
||||
const added = (k: number): Record<string, unknown> | undefined =>
|
||||
doc().더한줄?.[k] as Record<string, unknown> | undefined;
|
||||
const post = <T>(path: string, body: unknown, signal?: AbortSignal) =>
|
||||
call<T>(`/layers/${layer}/sangeun/${path}${q}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
});
|
||||
/** 고침 = 저장되는 SK 만(일위대가 줄 모달 `줄모드` = 보기만 — 줄로 못 옮김) · 처음 그릴 때 정함 */
|
||||
const make = (d: SkEquationDraw) =>
|
||||
createLabEquation({
|
||||
@@ -79,6 +104,7 @@ export function createSangeunEquation(opt: SkEquationOptions) {
|
||||
blocked: () => false,
|
||||
onSaved: () => undefined,
|
||||
page: {
|
||||
sum: blockHooks,
|
||||
ask: (suffix, body, signal) =>
|
||||
call(`/layers/${layer}/sangeun/equation${suffix}${q}`, {
|
||||
method: "POST",
|
||||
@@ -96,6 +122,7 @@ export function createSangeunEquation(opt: SkEquationOptions) {
|
||||
},
|
||||
});
|
||||
let lab: ReturnType<typeof make> | null = null;
|
||||
const NO_ROW = { 입력: [] } as unknown as LogicRow; // 끊은 기초 옵션 칸 = 안 그림
|
||||
const root = el("div", { attrs: { "data-sk-equation": "" }, children: [bar] });
|
||||
|
||||
async function job(how: "price" | "regen"): Promise<void> {
|
||||
@@ -107,9 +134,11 @@ export function createSangeunEquation(opt: SkEquationOptions) {
|
||||
doc()[JOB] = got.문서[JOB];
|
||||
const sum = (v: Record<string, number> | null): string => formatMoney(v?.계 ?? 0);
|
||||
const lost = got.못얹은 ?? [];
|
||||
const kept = got.끊은기초 ?? [];
|
||||
showToast(
|
||||
`${t(how === "price" ? "Price" : "Regen")} ${sum(got.전)} → ${sum(got.후)}` +
|
||||
(lost.length ? ` · ${t("Lost")} ${lost.map((x) => x.cid).join(" · ")}` : ""),
|
||||
(lost.length ? ` · ${t("Lost")} ${lost.map((x) => x.cid).join(" · ")}` : "") +
|
||||
(kept.length ? ` · ${t("KeptCut", kept.join(" · "))}` : ""),
|
||||
lost.length ? "warning" : "success",
|
||||
);
|
||||
opt.touch();
|
||||
@@ -118,6 +147,90 @@ export function createSangeunEquation(opt: SkEquationOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 더한 기초 연결 끊기 · 다시 잇기(796-2e) — 서버가 고친 그 더한줄만 화면 문서에 끼움 */
|
||||
async function link(k: number, how: "unlink" | "relink"): Promise<void> {
|
||||
const ask = t(how === "unlink" ? "AskUnlink" : "AskRelink");
|
||||
if (!(await showConfirmDialog(ask, t(how === "unlink" ? "Unlink" : "Relink")))) return;
|
||||
try {
|
||||
const got = await post<CopyReply>(`block/${how}`, { 문서: doc(), 차례: k });
|
||||
const one = got.문서.더한줄?.[k];
|
||||
const list = doc().더한줄;
|
||||
if (!one || !list) return;
|
||||
list[k] = one;
|
||||
blocks.delete(k);
|
||||
showToast(
|
||||
`${t(how === "unlink" ? "Unlink" : "Relink")} ${formatMoney(got.전?.계 ?? 0)} → ${formatMoney(got.후?.계 ?? 0)}`,
|
||||
"success",
|
||||
);
|
||||
opt.touch();
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : String(error), "error");
|
||||
}
|
||||
}
|
||||
|
||||
/** 끊은 기초 속 수식 — 기초마다 한 부품(다시 그려도 고르던 줄 · 쓰던 글 그대로) */
|
||||
const blocks = new Map<number, ReturnType<typeof make>>();
|
||||
function blockLab(k: number): ReturnType<typeof make> {
|
||||
let got = blocks.get(k);
|
||||
if (got) return got;
|
||||
got = createLabEquation({
|
||||
admin: true,
|
||||
blocked: () => false,
|
||||
onSaved: () => undefined,
|
||||
page: {
|
||||
ask: (suffix, body, signal) =>
|
||||
post(`equation${suffix}`, { ...body, 문서: doc(), 블록: k }, signal),
|
||||
edits: () => (added(k)?.[KEY] as EqEdit[] | undefined) ?? [],
|
||||
keep: (edits) => {
|
||||
const one = added(k);
|
||||
if (!one) return;
|
||||
if (edits.length) one[KEY] = edits;
|
||||
else delete one[KEY];
|
||||
opt.touch();
|
||||
},
|
||||
values: () => (added(k)?.입력값 as Record<string, string> | undefined) ?? {},
|
||||
setValue: () => undefined,
|
||||
},
|
||||
});
|
||||
blocks.set(k, got);
|
||||
return got;
|
||||
}
|
||||
|
||||
const blockHooks: EqSumHooks = {
|
||||
head: (r: EqSumRow): HTMLElement[] => {
|
||||
const k = r.블록;
|
||||
if (k === undefined || !r.나무) return [];
|
||||
const out: HTMLElement[] = [
|
||||
createBadge(t(r.끊음 ? "Cut" : "Linked"), r.끊음 ? "warning" : "neutral", { size: 18 }),
|
||||
];
|
||||
if (opt.edit && doc().줄모드 !== true)
|
||||
out.push(
|
||||
createButton({
|
||||
label: t(r.끊음 ? "Relink" : "Unlink"),
|
||||
variant: "ghost",
|
||||
size: 22,
|
||||
onClick: () => void link(k, r.끊음 ? "relink" : "unlink"),
|
||||
}),
|
||||
);
|
||||
return out;
|
||||
},
|
||||
body: (r: EqSumRow): HTMLElement | null => {
|
||||
const k = r.블록;
|
||||
const one = k === undefined ? undefined : added(k);
|
||||
if (k === undefined || !r.끊음 || !one || !opt.edit || doc().줄모드 === true) return null;
|
||||
const version = JSON.stringify([
|
||||
one.끊음 && (one.끊음 as { 시점?: string }).시점,
|
||||
one.입력값,
|
||||
one.수량,
|
||||
doc().입력값,
|
||||
last?.values,
|
||||
]);
|
||||
const part = blockLab(k);
|
||||
part.draw(`${String(doc().키 ?? "")}#${k}`, version, NO_ROW);
|
||||
return part.root;
|
||||
},
|
||||
};
|
||||
|
||||
function drawBar(d: SkEquationDraw): void {
|
||||
const ans = d.ans as Answer | null;
|
||||
const mark = ans?.사본;
|
||||
|
||||
@@ -99,4 +99,4 @@ def test_화면_부품이_서버_칸을_읽음():
|
||||
for name in ("노무비", "재료비", "경비", "끝수", "합계", "자원", "덧줄"):
|
||||
assert name in ts
|
||||
line = Path("M01_MasterData/M01_MasterData_UI_LogicLab_Equation.ts").read_text(encoding="utf-8")
|
||||
assert "createEqSum(eq, open)" in line
|
||||
assert "createEqSum(eq, open" in line # 796-2e 덧단추 자리 인자가 뒤에 붙음
|
||||
|
||||
@@ -65,6 +65,16 @@ export interface EqSumRow {
|
||||
밖금액?: unknown;
|
||||
/** 더한 기초의 수식(접힘 블록) */
|
||||
나무?: EqTree | null;
|
||||
/** 더한 기초 차례(M02 SK 더한줄 · 연결 끊기 796-2e) */
|
||||
블록?: number;
|
||||
/** 연결을 끊은 기초(이 SK 안 자기 사본) */
|
||||
끊음?: boolean;
|
||||
}
|
||||
|
||||
/** 소계 표 덧단추 자리(796-2e) — 페이지가 줄 머리 단추 · 접힘 블록 속을 바꿔 끼움 */
|
||||
export interface EqSumHooks {
|
||||
head?: (row: EqSumRow, at: number) => HTMLElement[];
|
||||
body?: (row: EqSumRow, at: number) => HTMLElement | null;
|
||||
}
|
||||
|
||||
/** 로직 하나의 수식(줄 · 변수표 · 합계 · 소계) */
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
* 표 = 줄(하위 로직 블록마다 한 줄 · 자기 자원 · 덧줄) × 노무비 · 재료비 · 경비 · 계 → 합계(엔진 비목 합) → 끝수(합계 − 줄 합)
|
||||
* 값 = 서버 `소계`(M01 `master_equation.subtotals` · M02 `M02_Sangeun_Equation_Full` = SK 셈 답) · 끝수가 0 이면 줄을 안 그림 · 소계가 없으면(결과 로직) 아무것도 안 그림
|
||||
* M02 SK 줄 = 상태 알약(더함 · 덮음 · 뺌) · 덮음 갈래 · 원래 금액 · 수량 · 더한 기초는 접힘 수식 블록 · 뺌 = 흐리게 · 금액 0
|
||||
* 쓰는 법 = `createEqSum(tree, open?)` → 요소 | null — 줄 이음(`createEqChain`) 아래에 붙임 · `open` = 펼친 블록 이름표 모음
|
||||
* 쓰는 법 = `createEqSum(tree, open?, hooks?)` → 요소 | null — 줄 이음(`createEqChain`) 아래에 붙임 · `open` = 펼친 블록 이름표 모음 ·
|
||||
* `hooks` = 페이지가 더한 기초 줄 머리 단추(`head`) · 접힘 속(`body`)을 바꿔 끼움(796-2e 연결 끊기)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge, type BadgeTone } from "./ui_template_badge";
|
||||
import { createButton } from "./ui_template_button";
|
||||
import { eqText, type EqSumRow, type EqTree } from "./ui_template_equation";
|
||||
import { eqText, type EqSumHooks, type EqSumRow, type EqTree } from "./ui_template_equation";
|
||||
import { createEqChain } from "./ui_template_equation_line";
|
||||
|
||||
const COLS = ["노무비", "재료비", "경비", "계"] as const;
|
||||
@@ -40,7 +41,7 @@ function row(kind: string, name: string, get: (k: (typeof COLS)[number]) => unkn
|
||||
}
|
||||
|
||||
/** M02 SK 줄 하나 — 이름 + 알약 + 원래 · 수량 / 더한 기초는 접힘 수식 */
|
||||
function itemRow(r: EqSumRow, open: Set<string>, at: number): HTMLElement {
|
||||
function itemRow(r: EqSumRow, open: Set<string>, at: number, hooks?: EqSumHooks): HTMLElement {
|
||||
const item = node("div", "ui-eq__sum-item");
|
||||
const line = node("div", "ui-eq__sum-row");
|
||||
line.dataset.eqSumKind = r.갈래;
|
||||
@@ -64,12 +65,13 @@ function itemRow(r: EqSumRow, open: Set<string>, at: number): HTMLElement {
|
||||
open.add(id);
|
||||
if (!body.childElementCount)
|
||||
body.append(
|
||||
createEqChain(tree.줄, {
|
||||
path: id,
|
||||
open,
|
||||
resultUnit: tree.결과단위,
|
||||
numbered: false,
|
||||
}),
|
||||
hooks?.body?.(r, at) ??
|
||||
createEqChain(tree.줄, {
|
||||
path: id,
|
||||
open,
|
||||
resultUnit: tree.결과단위,
|
||||
numbered: false,
|
||||
}),
|
||||
);
|
||||
} else open.delete(id);
|
||||
item.dataset.open = on ? "1" : "";
|
||||
@@ -82,6 +84,7 @@ function itemRow(r: EqSumRow, open: Set<string>, at: number): HTMLElement {
|
||||
const detail = r.덮개?.length ? ` · ${r.덮개.join(" · ")}` : "";
|
||||
name.append(createBadge(`${r.상태}${detail}`, TONE[r.상태] ?? "neutral", { size: 18 }));
|
||||
}
|
||||
name.append(...(hooks?.head?.(r, at) ?? []));
|
||||
if (r.합밖) {
|
||||
name.append(createBadge("합 밖", "neutral", { size: 18 }));
|
||||
line.dataset.eqSumOut = "1";
|
||||
@@ -105,6 +108,7 @@ function itemRow(r: EqSumRow, open: Set<string>, at: number): HTMLElement {
|
||||
export function createEqSum(
|
||||
tree: Pick<EqTree, "합계" | "소계" | "답까닭">,
|
||||
open: Set<string> = new Set(),
|
||||
hooks?: EqSumHooks,
|
||||
): HTMLElement | null {
|
||||
const sub = tree.소계;
|
||||
if (!sub?.줄.length) return null;
|
||||
@@ -115,7 +119,7 @@ export function createEqSum(
|
||||
for (const k of COLS) head.append(node("span", "ui-eq__sum-cell", k));
|
||||
box.append(head);
|
||||
sub.줄.forEach((r, at) => {
|
||||
if (r.상태) box.append(itemRow(r, open, at));
|
||||
if (r.상태) box.append(itemRow(r, open, at, hooks));
|
||||
else
|
||||
box.append(row(r.갈래, r.이름 === r.갈래 ? (KIND[r.갈래] ?? r.이름) : r.이름, (k) => r[k]));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user