feat(717-2b): SK 더한 재료 줄 = 기본 계 안(재료 소계 · 계에 더함) · 줄 「별도계상」 고름만 계 밖

- 서버 M02_Sangeun_Doc — 더한줄 재료 칸 별도계상(true 만 남김) · patched 가 더한 재료 줄 자재표시를 직접계산(기본) | 별도계상(칸 켬)으로 둠 · 원문 로직 줄 별도계상은 그대로
- 화면 — SK 상세 고침 표 더한 재료 줄 로직 칸에 「별도계상」 칩(켬 · 끔) · 순수 함수 setApart · 행 apartPick
- 지킴 시험 — test_715_sangeun_engine 더한 재료 계 안 · 별도계상 계 밖 · test_717_2b 칩 꼴

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 17:34:36 +09:00
co-authored by Claude Opus 5.5
parent 04f3489481
commit a1342124c6
6 changed files with 58 additions and 2 deletions
@@ -12,6 +12,7 @@
import { createBadge } from "@ui/ui_template_badge";
import { createButton } from "@ui/ui_template_button";
import { createCheckbox } from "@ui/ui_template_checkbox";
import { el } from "@ui/ui_template_elements";
import { createInputField } from "@ui/ui_template_input";
import { t as L } from "@ui/ui_template_locale";
@@ -181,6 +182,8 @@ export interface SkCoverOptions {
onPrice: (row: CoverRow, value: number | null) => void;
onDrop: (row: CoverRow) => void;
onRestore: (cid: string) => void;
/** 더한 재료 줄 별도계상 켬 · 끔 */
onApart: (row: CoverRow, on: boolean) => void;
}
const ORDER = ["인력", "재료", "경비", "하위"];
@@ -243,7 +246,18 @@ function rowCells(r: CoverRow, o: SkCoverOptions): (string | Node)[] {
...(r.covered.qty || r.covered.price ? [createBadge(o.tag, "warning")] : []),
...(r.added !== null ? [createBadge(L("M02_SangeunAddedTag"), "accent")] : []),
...(r.pct ? [createBadge(L("M02_SangeunPct"), "neutral")] : []),
...(r.apart ? [createBadge(L("M02_SangeunApart"), "neutral")] : []),
...(can && r.apartPick
? [
createCheckbox(L("M02_SangeunApart"), {
chip: true,
size: 22,
checked: r.apart,
onChange: (on) => o.onApart(r, on),
}).root,
]
: r.apart
? [createBadge(L("M02_SangeunApart"), "neutral")]
: []),
...(r.revised ? [createBadge(L("M02_SangeunRevised"), "danger")] : []),
];
const drop = createButton({
@@ -33,6 +33,7 @@ import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick";
import {
dropLine,
restoreLine,
setApart,
rowsOf,
setPrice,
setQty,
@@ -231,6 +232,7 @@ export function mountSangeunDetail(
onPrice: (r, v) => (setPrice(doc, r, v), touch()),
onDrop: (r) => (dropLine(doc, r), touch()),
onRestore: (cid) => (restoreLine(doc, cid), touch()),
onApart: (r, on) => (setApart(doc, r, on), touch()),
});
const row = base?.logic ?? standIn();
const est = el("div", {
+8 -1
View File
@@ -6,7 +6,8 @@
입력값 {이름: 값}, 열린입력 [이름](설계 때 정함 · 그 SK 품 덮개 = 배율만), 켜쌓기율?,
고름 {cid: MT키}, 자재표시 {cid: {세 칸}},
덮개 {cid: {qty | 배율, labor, material, expense, 원값}},
더한줄 [{종류: 인력|재료|기계, 요소, 이름, 단위, 수량, 비목?, 단가?}
더한줄 [{종류: 인력|재료|기계, 요소, 이름, 단위, 수량, 비목?, 단가?,
별도계상?(재료 · 없음 = 계 안)}
| {종류: 로직, 로직, 입력값, 수량} | {참조 | 고정, 이름?, 단위?, 수량}],
뺀줄 [cid], 바탕판}
줄 정체 cid = `kind:키#n`(B09 702 덮개 꼴) — kind = labor · material · machine · logic · pct(덧줄) ·
@@ -179,6 +180,8 @@ def _added(raw: Any, where: str, whole: Any) -> dict:
head = {"종류": raw["종류"], "요소": key, "비목": part}
if raw.get("단가") is not None:
head["단가"] = plain(_num(raw["단가"], f"{where} 단가"))
if raw["종류"] == "재료" and raw.get("별도계상") is True:
head["별도계상"] = True # 더한 재료 기본 = 계 안 · 이 칸만 별도계상(`patched` 자재표시)
name = name or str((got or {}).get("이름") or key)
unit = unit or str((got or {}).get("단위") or "")
else:
@@ -360,6 +363,10 @@ def patched(
new_items.append(item)
cids.append(cid)
for k, one in enumerate(doc.get("더한줄") or []):
if one.get("종류") == "재료": # 더한 재료 = 기본 계 안(직접계산) · `별도계상` 칸만 밖
out_marks[str(len(new_items))] = {
"별도계상": "별도계상" if one.get("별도계상") else "직접계산"
}
new_items.append(_add_item(one))
cids.append(f"add:#{k}")
cids += [c for at, c in enumerate(ids) if at >= len(items) and c not in drop]
@@ -107,6 +107,8 @@ export interface CoverRow {
/** 저장 때 적힌 원값이 지금 바탕 값과 다름(M01 개정) */
revised: boolean;
priceEdit: boolean;
/** 더한 재료 줄 — 별도계상 고름(기본 계 안) */
apartPick: boolean;
}
/* ── 순수 셈 ── */
@@ -183,6 +185,7 @@ export function rowsOf(ans: SkAnswer, bare: SkAnswer | null, doc: SkDoc): CoverR
((old.qty != null && !near(old.qty, base.qty)) ||
(old.단가 != null && !near(old.단가, base.단가))),
priceEdit: !pct && (added === null || ["인력", "재료", "기계"].includes(String(one?.종류))),
apartPick: one?.종류 === "재료",
};
};
for (const l of ans.줄) rows.push(row(l.cid, l, partsOf(l), l));
@@ -263,6 +266,14 @@ export function dropLine(doc: SkDoc, r: CoverRow): void {
}
}
/** 더한 재료 줄 별도계상 — 켬 = 계 밖(재료 목록만 · 서버 자재표시) · 끔 = 칸 지움(기본 계 안) */
export function setApart(doc: SkDoc, r: CoverRow, on: boolean): void {
const one = r.added === null ? undefined : doc.더한줄?.[r.added];
if (!one || one.종류 !== "재료") return;
if (on) one.별도계상 = true;
else delete one.별도계상;
}
/** 뺀 줄 되살림 */
export function restoreLine(doc: SkDoc, cid: string): void {
doc.뺀줄 = (doc.뺀줄 ?? []).filter((c) => c !== cid);
@@ -84,3 +84,17 @@ def test_added_ref_and_fixed_lines(world):
assert got["기준"]["계"] == pytest.approx(unit * 2 + 5000, abs=1)
expense = labor.run_line(_whole(), PCT, {})["기준"]["경비"]
assert got["기준"]["경비"] == pytest.approx(5000 + 2 * float(expense), abs=1)
def test_added_material_counts_unless_apart():
"""사용자가 더한 재료 줄 = 기본 계 안(재료비 · 계에 더함) · 줄 「별도계상」 칸만 계 밖(재료 목록만)."""
one = {"종류": "재료", "요소": "MT000010", "수량": 3, "단가": 1200}
bare = _run(바탕={"로직": PCT})
inside = _run(바탕={"로직": PCT}, 더한줄=[one])
assert inside["기준"]["재료비"] == pytest.approx(bare["기준"]["재료비"] + 3600)
assert inside["기준"]["계"] == pytest.approx(bare["기준"]["계"] + 3600, abs=1)
assert any(x["cid"] == "add:#0" for x in inside["줄"])
apart = _run(바탕={"로직": PCT}, 더한줄=[{**one, "별도계상": True}])
assert apart["기준"]["계"] == pytest.approx(bare["기준"]["계"])
assert not any(x["cid"] == "add:#0" for x in apart["줄"])
assert any(m["금액"] == pytest.approx(3600) for m in apart["재료"])
@@ -74,6 +74,11 @@ out.split = doc.덮개["logic:G#0"];
setQty(doc, by("add:#0"), 3, false);
setPrice(doc, by("add:#0"), 350);
out.added = doc.더한줄[0];
out.pick = [by("add:#0").apartPick, by("labor:A#0").apartPick];
setApart(doc, by("add:#0"), true);
out.apartOn = doc.더한줄[0].별도계상 ?? null;
setApart(doc, by("add:#0"), false);
out.apartOff = doc.더한줄[0].별도계상 ?? null;
const opened = { 덮개: {} };
setQty(opened, by("labor:A#0"), 1.5, true);
out.scale = opened.덮개["labor:A#0"];
@@ -105,6 +110,9 @@ console.log(JSON.stringify(out));
assert got["split"]["labor"] == 60 and got["split"]["material"] == 30
assert got["split"]["expense"] == 10
assert got["added"]["수량"] == 3 and got["added"]["단가"] == 350
# 더한 재료 줄만 별도계상 고름 · 켬 = 칸 true · 끔 = 칸 지움(기본 계 안)
assert got["pick"] == [True, False]
assert got["apartOn"] is True and got["apartOff"] is None
assert got["scale"]["배율"] == 1.5 and "qty" not in got["scale"]
assert got["scaleBack"] is None
assert got["drop"] == [["labor:A#0"], None]