From 20f17827d35791002e0ebc21437d9013e302bdc5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 11:57:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(b09):=20=EC=9E=AC=EB=A3=8C=EB=B9=84?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=ED=91=9C=20=3D=20=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=EC=9E=90=EC=9E=AC=EB=A7=8C=20=C2=B7=20=EC=88=98=EB=9F=89=20=3D?= =?UTF-8?q?=20=EC=97=AD=EC=A0=84=EA=B0=9C=20=EC=B4=9D=EC=88=98=EB=9F=89=20?= =?UTF-8?q?=C2=B7=20=EB=B9=84=EA=B3=A0=20=3D=20=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=EA=B3=B3=20(183-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수요: 사본 일위대가 자재 · 굳힌 사전조합 자재 코드도(Linked.used_keys) → 대가 재료 줄 단가가 MT 줄에서 남 - 두 번 셈(Router.compute_twice): 1차 셈 → 대가 · 산근 · 중기 재료 줄 · B08 줄 · 관급 · 별도 줄만 남김(Summary.used_materials) · 1차 역전개 총수량 = material_qty → 2차 셈 - 수량 원천: Calc_Base 짝 material_sheet 가 material_qty(없는 키 빈칸) · 없으면 입력 줄 수량(거울) - 화면: 비고 = 쓰는 곳(대가 · 산근 · 중기 n호표 · 내역 자재대) · 안내 글 - 실측 엄상준_검증: 141 → 24줄 · 목록 수량 = 재료비수량금액집계표 수량 · 목록표에 없음 0 - 시험: test_183_b09_material_list Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY --- B09_Estimation/B09_Estimation_Calc_Base.py | 23 +++-- B09_Estimation/B09_Estimation_Calc_Base.ts | 12 +-- B09_Estimation/B09_Estimation_Linked.py | 8 +- B09_Estimation/B09_Estimation_Router.py | 19 +++- B09_Estimation/B09_Estimation_Summary.py | 19 ++++ B09_Estimation/B09_Estimation_UI_Tabs_Base.ts | 30 ++++++- .../tester/test_183_b09_material_list.py | 88 +++++++++++++++++++ 7 files changed, 179 insertions(+), 20 deletions(-) create mode 100644 resources/tester/test_183_b09_material_list.py diff --git a/B09_Estimation/B09_Estimation_Calc_Base.py b/B09_Estimation/B09_Estimation_Calc_Base.py index 775553d86..127960c1b 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.py +++ b/B09_Estimation/B09_Estimation_Calc_Base.py @@ -13,8 +13,11 @@ · 견적 = 사용자 손입력(quote · quote_page · 견적서 파일은 `B09_Estimation_Router_Quotes`) · 관급 · 별도계상 = 사용자 표시(없으면 입력 줄 기본 · B08 파형강관 = 관급) → 재료비목록표 단가 = 적용 단가 - 재료비목록표 = 수량(B08 총괄자재수량집계표 합계 열) × 적용 단가 = 총액 ROUND(·, 0) - (엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸) + 재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0) + (엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸) · 수량 = + 입력 `material_qty`(183-1 · 서버 1차 셈 역전개 총수량 = 재료비수량금액집계표 수량 · + 없는 키 빈칸) · + `material_qty` 가 없으면 입력 줄 수량(B08 총괄 합계 열) 중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임) + 연료(재료 · ℓ/hr × 적용 단가) + 잡품(재료 · 연료 금액 × % ÷ 100) 줄 = ROUNDDOWN(단가 × 수량, 1) · 비목 합 = ROUNDDOWN(Σ, 0) → 중기목록표 @@ -178,14 +181,18 @@ def compare_sheet( return rows -def material_sheet(compare: list[dict[str, Any]]) -> list[dict[str, Any]]: - """재료비목록표 — 단가 = 대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0).""" - keep = ("no", "key", "name", "spec", "unit", "supply", "origin", "qty") +def material_sheet( + compare: list[dict[str, Any]], qty_of: dict[str, Any] | None = None +) -> list[dict[str, Any]]: + """재료비목록표 — 단가 = 대비표 적용 단가 · 수량 = `qty_of`(183-1) ?? 줄 수량 · + 총액 = ROUND(수량 × 단가, 0).""" + keep = ("no", "key", "name", "spec", "unit", "supply", "origin") rows = [] for row in compare: - price, q = row["applied"], row["qty"] + price = row["applied"] + q = row["qty"] if qty_of is None else num(qty_of.get(row["key"])) amount = None if price is None or q is None else round_half(q * price, 0) - rows.append({**{k: row[k] for k in keep}, "price": price, "amount": amount}) + rows.append({**{k: row[k] for k in keep}, "qty": q, "price": price, "amount": amount}) return rows @@ -328,7 +335,7 @@ def compute_base(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any] "expense": expense, "basis": basis, "compare": compare, - "material": material_sheet(compare), + "material": material_sheet(compare, inputs.get("material_qty")), "machine_blocks": blocks, "machine": machine, "gaps": gaps, diff --git a/B09_Estimation/B09_Estimation_Calc_Base.ts b/B09_Estimation/B09_Estimation_Calc_Base.ts index c5223530c..7c133ea91 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.ts +++ b/B09_Estimation/B09_Estimation_Calc_Base.ts @@ -9,7 +9,8 @@ * 노무비 · 경비목록표 = 굳힌 단가 · 환율및기초자료 = 운전사 시간 노임 ROUNDDOWN(일당 × 분자 ÷ 분모, 0) * 자재단가대비표 = 출처 중 최저 자동(견적 포함) · 적용 = 고른 출처 › 사용자 단가 › 최저 · * 관급 · 별도 표시 → 재료비목록표 - * 재료비목록표 = 수량(B08 총괄 합계 열) × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸 + * 재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸 · 수량 = 입력 + * material_qty(183-1 · 서버 역전개 총수량 · 없는 키 빈칸) ?? 입력 줄 수량(B08 총괄 합계 열) * 중기사용료 = 손료 + 운전사 + 연료 + 잡품 · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 → 중기목록표 * 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)에 남김 * ========================================================================== */ @@ -157,13 +158,14 @@ function compareSheet(materials: Row[], user: Row, gaps: Gap[]): Row[] { }); } -function materialSheet(compare: Row[]): Row[] { - const keep = ["no", "key", "name", "spec", "unit", "supply", "origin", "qty"]; +function materialSheet(compare: Row[], qtyOf?: Row | null): Row[] { + const keep = ["no", "key", "name", "spec", "unit", "supply", "origin"]; return compare.map((row) => { const price: Num = row.applied; - const q: Num = row.qty; + const q: Num = qtyOf == null ? row.qty : num(qtyOf[row.key]); return { ...Object.fromEntries(keep.map((k) => [k, row[k]])), + qty: q, price, amount: price === null || q === null ? null : roundHalf(q * price, 0), }; @@ -301,7 +303,7 @@ export function computeBase(inputs: Row, user: Row): Row { expense, basis, compare, - material: materialSheet(compare), + material: materialSheet(compare, inputs.material_qty), machine_blocks: blocks, machine, gaps, diff --git a/B09_Estimation/B09_Estimation_Linked.py b/B09_Estimation/B09_Estimation_Linked.py index ded22e253..ff2427dd3 100644 --- a/B09_Estimation/B09_Estimation_Linked.py +++ b/B09_Estimation/B09_Estimation_Linked.py @@ -3,7 +3,9 @@ 연결된 사본 = `M02_Item_Snapshot.linked_snapshots`(지정 ∩ 같은 항목 사본 · B08 · B09 공용). 사본마다 `일위대가.json` 담은 로직을 그 사본 단가 묶음(`단가.json`) · 입력값 · 고른자재로 돌리며 실제로 부른 자원 줄(인력 LB · 기계 EQ · 자재 MT · 유가 MO)만 모음 — 단가 묶음 통째 -(고르기 후보 전부)는 안 씀 · 값은 쓰지 않음(단가는 원가 한 벌 `price_book.json` 이 맡음). +(고르기 후보 전부)는 안 씀 · 값은 쓰지 않음(단가는 원가 한 벌 `price_book.json` 이 맡음) · +사본 일위대가 자재 · 굳힌 사전조합 자재 코드도(183-1 · 로직을 안 돌아 `get` 에 안 걸림). +고르기가 훑은 후보 줄도 걸림 → 원가 길이 두 번 셈해 쓰는 줄만 남김(`Summary.used_materials`). 구조물 조합 사본 없으면 그 구조물은 없음 · 일위대가 없는 사본은 자원 없음. """ @@ -57,6 +59,10 @@ def used_keys(folder: Path) -> list[str]: labor_doc.run_line( snapshot, one["로직"], one.get("입력값"), one.get("켜쌓기율"), one.get("고름") ) + for mat in [*(doc.get("자재") or []), *(m for d in inner for m in d.get("자재") or [])]: + code = str(mat.get("코드") or "") if isinstance(mat, dict) else "" + if code[:2] in RESOURCE_TABLES and code not in seen: + seen.append(code) return seen diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index ffa23ccb6..b4a7d7185 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -37,7 +37,7 @@ from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs from B09_Estimation.B09_Estimation_Linked import linked_resource_keys from B09_Estimation.B09_Estimation_Rates import rate_candidates, rate_table from B09_Estimation.B09_Estimation_Router_Quotes import router as quotes_router -from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines +from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines, used_materials router = APIRouter(prefix="/api/projects", tags=["B09 Estimation"]) @@ -81,6 +81,19 @@ def compute_sheets(root: Path, inputs: dict[str, Any], user: dict[str, Any]) -> return sheets +def compute_twice(root: Path, inputs: dict[str, Any], user: dict[str, Any]) -> tuple[dict, dict]: + """183-1 두 번 셈 — 1차 셈으로 쓰는 자재만 남기고(`used_materials`) 재료비목록표 수량 = + 1차 역전개 총수량(`material_qty` = 재료비수량금액집계표 수량) → 2차 셈 · 집계표. + `inputs` 를 고쳐 둠(화면 짝이 거른 입력 · 수량을 그대로 받음).""" + first = compute_sheets(root, inputs, user) + summary = compute_summary(first, hopyo_lines(first)) + keep = used_materials(first, inputs, user) + inputs["materials"] = [m for m in inputs.get("materials") or [] if m["key"] in keep] + inputs["material_qty"] = {row["key"]: row["qty"] for row in summary["sum_material"]} + sheets = compute_sheets(root, inputs, user) + return sheets, compute_summary(sheets, hopyo_lines(sheets)) + + async def _demand(root: Path) -> list[str]: """단가 수요 키 — 연결된 사본 + 설계내역서 손 줄 로직(166-2).""" keys = await asyncio.to_thread(linked_resource_keys, root) @@ -103,7 +116,7 @@ async def payload(project_id: UUID, root: Path) -> dict[str, Any]: inputs["hopyo"] = hopyo_prices(root) inputs["daega"] = await asyncio.to_thread(daega_inputs, root) user = await asyncio.to_thread(store.read_user, root) - sheets = await asyncio.to_thread(compute_sheets, root, inputs, user) + sheets, summary = await asyncio.to_thread(compute_twice, root, inputs, user) return { "inputs": inputs, "user": user, @@ -113,7 +126,7 @@ async def payload(project_id: UUID, root: Path) -> dict[str, Any]: "rate_table": rate_table(), "notes": notes, # 집계표 넷 = 서버 단독(145-5) — 짝 셈 결과를 역전개 · 조작 중엔 안 바뀜 - "summary": compute_summary(sheets, hopyo_lines(sheets)), + "summary": summary, } diff --git a/B09_Estimation/B09_Estimation_Summary.py b/B09_Estimation/B09_Estimation_Summary.py index 5569f8f31..9a660b718 100644 --- a/B09_Estimation/B09_Estimation_Summary.py +++ b/B09_Estimation/B09_Estimation_Summary.py @@ -155,6 +155,25 @@ def block_lines(blocks: list[dict[str, Any]] | None) -> dict[str, Any]: return out +def used_materials(sheets: dict[str, Any], inputs: dict[str, Any], user: dict[str, Any]) -> set: + """재료비목록표에 남길 자재 키(183-1) — 대가 · 산근 · 중기 블록 재료 줄(수량 빈칸 · 0 도 = + 집계표 역전개와 같은 줄) · B08 총괄 줄(수량 있는 입력) · 관급 · 별도 표시 줄. + 그 밖(고르기 후보 줄) = 뺌.""" + blocks = [*(sheets.get("sg") or []), *(sheets.get("daega_blocks") or [])] + blocks += sheets.get("machine_blocks") or [] + keep = { + str((line.get("ref") or {}).get("key")) + for block in blocks + for line in block.get("lines") or [] + if line.get("kind") == "material" and (line.get("ref") or {}).get("key") + } + marks = user.get("materials") or {} + for m in inputs.get("materials") or []: + if "qty" in m or m.get("supply") or (marks.get(m["key"]) or {}).get("supply"): + keep.add(m["key"]) + return keep + + def hopyo_lines(sheets: dict[str, Any]) -> dict[str, Any]: """호표 구성(1 단위당 자원 · 겹 호표) — 산근(145-1) + 대가(145-2 · 들어오면).""" return {**block_lines(sheets.get("sg")), **block_lines(sheets.get("daega_blocks"))} diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts index 04670f20d..f5267e0cc 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts @@ -2,7 +2,8 @@ * B09_Estimation_UI_Tabs_Base.ts * B09 바탕 단가 탭 일곱(PLAN 144-2~4) — 노무비 · 경비목록표 · 환율및기초자료 · 자재단가대비표 · * 재료비목록표 · 중기사용료 · 중기목록표(실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Base`). - * 재료비목록표 = 엑셀 꼴 + 수량(B08 합계) · 총액 열(166-3) + * 재료비목록표 = 엑셀 꼴 + 수량 · 총액 열(166-3) · 줄 = 대가 · 산근 · 중기가 쓰는 자재 + B08 줄 · + * 관급 · 별도 줄(183-1 · 서버가 거른 입력) · 수량 = 역전개 총수량 · 비고 = 쓰는 곳(`usesOf`) * * 고칠 칸 = 자재단가대비표 적용 단가(빈칸으로 지우면 최저 자동) · 관급 / 별도 칸 누름 = 표시 바꿈 * · 견적 단가 · 면 손입력 · 적용 「출처」 칸 누름 = 고를 출처 돌림(값 있는 출처만 · 견적이 높아도) @@ -207,10 +208,33 @@ export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => { }; }; +/** 쓰는 곳(183-1) — 자재 키 → 「대가 n호표」 · 「산근 n호표」 · 「중기 n호표」 · 「내역 자재대」 + * (블록 재료 줄 · 설계내역서 자재 항목 · 표시 잇기만 · 셈 아님) */ +function usesOf(sheets: Row): Map { + const out = new Map(); + const add = (key: unknown, label: string): void => { + if (!key) return; + const list = out.get(String(key)) ?? []; + if (!list.includes(label)) out.set(String(key), [...list, label]); + }; + const scan = (blocks: Row[] | undefined, label: (b: Row, n: number) => string): void => + (blocks || []).forEach((b, n) => + (b.lines || []).forEach((ln: Row) => ln.kind === "material" && add(ln.ref?.key, label(b, n))), + ); + scan(sheets.daega_blocks, (b) => `대가 ${b.no}호표`); + scan(sheets.sg, (b) => `산근 ${b.no}호표`); + scan(sheets.machine_blocks, (_b, n) => `중기 ${n + 1}호표`); + for (const r of sheets.bill?.rows || []) + if (r.kind === "item" && r.ref?.tab === "material") add(r.ref.key, "내역 자재대"); + return out; +} + export const buildMaterial: TabBuild = ({ sheets }): TabTable => { + const uses = usesOf(sheets); const rows: DataRow[] = (sheets.material || []).map((r: Row) => ({ ...r, supply: r.supply ?? "", + uses: (uses.get(r.key) ?? []).join(" · ") || (r.origin === "B08" ? "B08 총괄" : ""), })); return { columns: [ @@ -222,14 +246,14 @@ export const buildMaterial: TabBuild = ({ sheets }): TabTable => { money("price", "단가"), money("amount", "총액"), text("supply", "관급 · 별도", 80), - text("origin", "비고", 90), + text("uses", "비고(쓰는 곳)", 240), ], rows, total: rows.length ? { name: "계", amount: rows.reduce((sum, r) => sum + (Number(r.amount) || 0), 0) } : null, frozen: 2, - hint: `수량 = B08 총괄자재수량집계표 합계 열(할증 포함 · 사본 자원 줄은 빈칸) · 단가 = 자재단가대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0) · 계 = 빈칸 뺀 합 · ${LINK_HINT}`, + hint: `줄 = 연결된 일위대가 · 산근 · 중기가 쓰는 자재 + B08 총괄 줄 · 관급 · 별도 줄 · 수량 = 설계내역서 × 호표 역전개 총수량(재료비수량금액집계표와 같음 · B08 할증 값과 다를 수 있음) · 단가 = 자재단가대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0) · 계 = 빈칸 뺀 합 · 비고 = 쓰는 곳 · ${LINK_HINT}`, link: (i) => (rows[i] ? { tab: "compare", key: String(rows[i].key) } : null), }; }; diff --git a/resources/tester/test_183_b09_material_list.py b/resources/tester/test_183_b09_material_list.py new file mode 100644 index 000000000..e6905631c --- /dev/null +++ b/resources/tester/test_183_b09_material_list.py @@ -0,0 +1,88 @@ +"""183-1 재료비목록표 = 연결된 일위대가 · 산근 · 중기가 쓰는 자재만 · 수량 · 단가 한 줄 · 쓰는 곳. + +- 수요: 사본 일위대가 자재 · 굳힌 사전조합 자재 코드도 든다(`Linked.used_keys`) +- 거름: 대가 · 산근 · 중기 블록 재료 줄 · B08 줄 · 관급 · 별도 줄만 남김(`Summary.used_materials`) +- 수량 = 입력 `material_qty`(1차 역전개 총수량) — `Calc_Base` 짝 거울 · 두 번 셈(`Router.compute_twice`) +- 화면 비고 = 쓰는 곳(`UI_Tabs_Base.usesOf`) +""" + +import json +from pathlib import Path + +from B09_Estimation import B09_Estimation_Router as router_mod +from B09_Estimation.B09_Estimation_Calc_Base import compute_base +from B09_Estimation.B09_Estimation_Linked import used_keys +from B09_Estimation.B09_Estimation_Summary import used_materials +from resources.tester.test_144_b09_base_calc import _run_ts + +ROOT = Path(__file__).resolve().parents[2] + +MATERIALS = [ + {"key": "MT1", "name": "쓰는 자재", "prices": {"물가자료": 1000}}, + {"key": "MT2", "name": "고르기 후보", "prices": {"물가자료": 500}}, + {"key": "MT3", "name": "관급 표시", "prices": {"물가자료": 300}}, + {"key": "MO1", "name": "경유", "prices": {"유가": 1800}}, + {"key": "b08:pipe_800", "name": "파형강관", "prices": {}, "qty": 8, "supply": "관급"}, +] +USER = {"materials": {"MT3": {"supply": "관급"}}} +SG = [{"key": "SG1", "no": 1, "lines": [{"kind": "material", "ref": {"key": "MT1"}, "qty": 2}]}] +MACHINE = [{"key": "EQ1", "lines": [{"kind": "material", "ref": {"key": "MO1"}, "qty": None}]}] + + +def test_사본_일위대가_자재_사전조합_자재_코드도_수요(tmp_path): + labor = { + "줄": {"담은로직": [], "자재": [{"코드": "MT000053"}, {"코드": "PM000001"}]}, + "참조사본": {"참조|system||SC-000001": {"담은로직": [], "자재": [{"코드": "MT000008"}]}}, + } + (tmp_path / "일위대가.json").write_text(json.dumps(labor, ensure_ascii=False), "utf-8") + assert used_keys(tmp_path) == ["MT000053", "MT000008"] # 개인 자재(PM) = 원가 표 밖 + + +def test_쓰는_자재만_남김(): + sheets = {"sg": SG, "daega_blocks": [], "machine_blocks": MACHINE} + inputs = {"materials": MATERIALS} + assert used_materials(sheets, inputs, USER) == {"MT1", "MO1", "MT3", "b08:pipe_800"} + + +def test_수량_원천_material_qty_거울(): + inputs = {"labor": [], "machines": [], "materials": MATERIALS} + old = {r["key"]: r["qty"] for r in compute_base(inputs, USER)["material"]} + assert old["b08:pipe_800"] == 8 and old["MT1"] is None # 없으면 입력 줄 수량(지금 길) + got = compute_base({**inputs, "material_qty": {"MT1": 6, "b08:pipe_800": 8}}, USER) + rows = {r["key"]: r for r in got["material"]} + assert (rows["MT1"]["qty"], rows["MT1"]["amount"]) == (6, 6000) + assert rows["MT2"]["qty"] is None and rows["MT2"]["amount"] is None # 없는 키 = 빈칸 + for case in (inputs, {**inputs, "material_qty": {"MT1": 6}}): + assert _run_ts(case, USER) == compute_base(case, USER) + + +def test_두_번_셈_후보_줄_빠지고_수량_역전개(monkeypatch): + calls = [] + + def fake(root, inputs, user): + calls.append([m["key"] for m in inputs["materials"]]) + base = compute_base(inputs, user) + bill = {"rows": [{"kind": "item", "qty": 3, "ref": {"tab": "sg", "key": "SG1"}}]} + return {**base, "sg": SG, "daega_blocks": [], "machine_blocks": MACHINE, "bill": bill} + + monkeypatch.setattr(router_mod, "compute_sheets", fake) + inputs = {"labor": [], "machines": [], "materials": list(MATERIALS)} + sheets, summary = router_mod.compute_twice(Path("."), inputs, USER) + assert calls[0] == [m["key"] for m in MATERIALS] + assert calls[1] == ["MT1", "MT3", "MO1", "b08:pipe_800"] # 후보 MT2 뺌 + assert inputs["material_qty"] == {"MT1": 6.0} # 설계내역서 3 × 호표 2 + rows = {r["key"]: r for r in sheets["material"]} + assert (rows["MT1"]["qty"], rows["MT1"]["amount"]) == (6.0, 6000) + assert [r["qty"] for r in summary["sum_material"]] == [rows["MT1"]["qty"]] # 집계표와 같은 값 + + +def test_화면_비고_쓰는_곳(): + src = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Base.ts").read_text(encoding="utf-8") + for label in ( + "`대가 ${b.no}호표`", + "`산근 ${b.no}호표`", + "`중기 ${n + 1}호표`", + '"내역 자재대"', + ): + assert label in src + assert 'text("uses", "비고(쓰는 곳)"' in src and 'text("origin", "비고"' not in src