fix(725-3·725-4): 바탕 없는 SK 거짓 M01 개정 막음 · 단가산출로도 저장 뒤 나무 다시 읽음 · 더한 줄 단가 찾아 옴

- 725-3 Sangeun_Detail — 개정 판정(배지 · 띠)은 바탕이 있을 때만(바탕 없는 SK 지문은 이름 · 단위 따라 바뀜)
- 725-4 Labor_RowModal — 단가산출로도 저장 성공 뒤 서고 목록 알림(HOPYO_BUILT) · 단가산출 나무 바로 갱신
- 725-4 LogicLab_Modal — prices 에 없는 요소(M02 더한 석공 · 합판)도 키로 찾아 단가 보임(lookup 하나로 합침)
- 시험 test_717_2b 에 725 단언 하나 더함

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 00:00:45 +09:00
co-authored by Claude Sonnet 5.5
parent 0001ef818c
commit b1cba8ed14
4 changed files with 37 additions and 20 deletions
@@ -454,6 +454,22 @@ export function openMaterialModal(target: ModalTarget): void {
};
setSum(known);
const rate = el("div", { className: "m01lab__modal-body" });
/** `prices` 에 있으면 그대로 · 없으면 키로 찾아 옴 — 못 찾으면 「관리자 값 미확정」 */
const lookup = (ref: string, label: string): void => {
const show = (brief: ElementBrief | null): void => {
rate.replaceChildren(
brief ? priceView(ref, brief) : muted(`${label} — ${tl("Pill_NoValue")}`),
);
setSum(brief);
};
rate.append(muted("…"));
const cached = target.prices[ref];
if (cached) show(cached);
else
void searchElements(groupOf(ref), ref)
.then((got) => show(got.items.find((it) => it.ref === ref) ?? null))
.catch(() => show(null));
};
if (known && priceRef) {
rate.append(priceView(priceRef, known, target.usedSlot));
} else if (logicKey) {
@@ -484,25 +500,9 @@ export function openMaterialModal(target: ModalTarget): void {
if (!ref) {
const names = [...element.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]).join(" · ");
rate.append(muted(`${target.unitText || names} — ${tl("Pill_Unresolved")}`));
} else {
const show = (brief: ElementBrief | null): void => {
rate.replaceChildren(
brief
? priceView(ref, brief)
: muted(`${target.unitText ?? ref} — ${tl("Pill_NoValue")}`),
);
setSum(brief);
};
rate.append(muted("…"));
const cached = target.prices[ref];
if (cached) show(cached);
else
void searchElements(groupOf(ref), ref)
.then((got) => show(got.items.find((it) => it.ref === ref) ?? null))
.catch(() => show(null));
}
} else lookup(ref, target.unitText ?? ref);
} else if (element) {
rate.append(muted(`${element} — ${tl("Pill_NoValue")}`));
lookup(element, element); // 더한 줄(M02) 등 `prices` 에 없는 요소 — 키로 찾아 옴(725-4)
}
body.replaceChildren(
...(target.edit ? [target.edit(close)] : []),
@@ -14,6 +14,7 @@ import { createButton, el, showToast } from "@ui/ui_template_elements";
import { askConfirm, openModal } from "@ui/ui_template_modal";
import { createText } from "@ui/ui_template_text";
import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch";
import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check";
import type { LaborCalc, LaborMat, LaborRow, MatMarkKey } from "./M02_MasterTemplete_Labor_Api";
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
import { logicMatRows } from "./M02_MasterTemplete_Labor_LogicMats";
@@ -175,6 +176,7 @@ export function openRowModal(o: RowModalOptions): void {
try {
const name = await saveAsSk(row, got?.patch ?? row.패치 ?? null, o.title, o.unit);
showToast(t("Saved").replace("{k}", name), "success");
document.dispatchEvent(new Event(HOPYO_BUILT)); // 좌측 단가산출 나무 다시 읽음(725-4)
} catch (error) {
return showToast(error instanceof Error ? error.message : String(error), "error");
}
@@ -139,6 +139,9 @@ export function mountSangeunDetail(
);
const opened = (): boolean => (doc.열린입력 ?? []).length > 0;
// 바탕 없는 SK 의 지문 = 이름 · 단위 따라 바뀜 — 바탕이 있을 때만 M01 개정으로 봄(725-3)
const revisedBase = (): boolean =>
!!(doc.바탕 && doc.바탕판 && ans?.바탕판 && doc.바탕판 !== ans.바탕판);
const baseKey = (): string | null => doc.바탕?.로직 ?? null;
/** 바탕 없는 · 참조 바탕 SK 의 기본정보 자리 줄 */
@@ -191,7 +194,7 @@ export function mountSangeunDetail(
};
const baseName = base ? `${base.logic.키} ${base.logic.이름}` : null;
const baseText = baseName ?? doc.바탕?.참조?.키 ?? L("M02_SangeunNoBase");
const revised = !!(doc.바탕판 && ans?.바탕판 && doc.바탕판 !== ans.바탕판);
const revised = revisedBase();
pick.hidden = !edit || !!doc.바탕 || !!doc.뺀줄?.length || !!Object.keys(doc.덮개 ?? {}).length;
head.replaceChildren(
field(L("M02_Name"), "이름"),
@@ -211,7 +214,7 @@ export function mountSangeunDetail(
function drawNotes(): void {
const lost = ans?.짝잃음 ?? [];
const revised = !!(doc.바탕판 && ans?.바탕판 && doc.바탕판 !== ans.바탕판);
const revised = revisedBase();
notes.replaceChildren(
...(fail || ans?.까닭
? [createBanner({ tone: "danger", text: fail || String(ans?.까닭) })]
@@ -233,3 +233,15 @@ def test_화면_문서_꼴_서버_저장_셈(world): # noqa: F811 — 가져온
assert added["수량"] == pytest.approx(0.01)
assert added["단가"] == pytest.approx(lines["labor:LB000002#0"]["단가"])
assert got["기준"]["계"] == pytest.approx(sum(x["금액"] for x in got["줄"]))
def test_725_바탕_없으면_개정_안_뜸_새_SK_나무_다시_읽음_더한줄_단가_찾음():
"""725-3 바탕 없는 SK 는 이름 · 단위 따라 지문이 바뀌어도 M01 개정 아님 · 725-4 새 SK 뒤 나무 알림 ·
`prices` 에 없는 요소 = 키로 찾아 옴."""
detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
assert "!!(doc.바탕 && doc.바탕판 && ans?.바탕판 && doc.바탕판 !== ans.바탕판)" in detail
assert detail.count("const revised = revisedBase();") == 2
modal = _text(M02 / "M02_MasterTemplete_Labor_RowModal.ts")
assert modal.index("saveAsSk(row") < modal.index("dispatchEvent(new Event(HOPYO_BUILT))")
lab = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Modal.ts")
assert "lookup(element, element)" in lab and "Pill_NoValue" in lab