diff --git a/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py b/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py index f7bf60ee..869caf3d 100644 --- a/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py +++ b/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py @@ -151,6 +151,7 @@ class MaterialRow: name: str unit: str + spec: str = "" # 규격 — 돌 종류·콘크리트 강도처럼 같은 이름을 가르는 값(명세 13장 Ⓒ) net_amount: float = 0.0 # 순수량 — 할증 전 surcharge_pct: float | None = None # None = 미확보 supply: str = SUPPLY_UNKNOWN @@ -159,6 +160,11 @@ class MaterialRow: basis: str = "" sources: list[str] = field(default_factory=list) + @property + def supply_key(self) -> str: + """관급구분 설정(`material_supply`)을 찾는 열쇠 — 이름 + 규격.""" + return f"{self.name} {self.spec}".strip() + @property def total_amount(self) -> float: """합계 = 순수량 × (1 + 할증률). 미확보면 **순수량 그대로** 두고 비고로 알린다.""" @@ -255,11 +261,36 @@ def surcharge_lookup_name(name: str, concrete_placing_method: str | None) -> tup return name, "" +def _add(rows: dict[tuple[str, str, str], MaterialRow], item: dict[str, Any]) -> MaterialRow: + """(이름·규격·단위)가 같은 줄에 더한다 — 규격이 다르면 다른 자재다(돌 종류·강도).""" + name = str(item.get("name") or "").strip() + spec = str(item.get("spec") or "").strip() + unit = str(item.get("unit") or "").strip() + row = rows.setdefault((name, spec, unit), MaterialRow(name=name, unit=unit, spec=spec)) + row.net_amount += float(item.get("amount") or 0.0) + if item.get("surcharge_included"): + row.surcharge_included = True + return row + + +def _supply_setting(supply: dict[str, Any], row: MaterialRow) -> Any: + """그 줄의 관급구분 설정 — 이름+규격이 먼저, 없으면 옛 열쇠. + + ⚠ 옛 열쇠 둘을 받는다 — 이름만(규격이 갈리기 전 「채움콘크리트」) · 규격만(돌 줄이 + 종류 이름으로 서던 때 「야면석·호박돌」, 2026-09-13 걷음). + 안 받으면 사용자가 정한 값이 사라진다. + """ + for key in (row.supply_key, row.name, row.spec): + if key and key in supply: + return supply[key] + return None + + def _collect( unit_quantity_table: dict[str, Any], -) -> tuple[dict[tuple[str, str], MaterialRow], dict[str, int]]: +) -> tuple[dict[tuple[str, str, str], MaterialRow], dict[str, int]]: """`destination == "material"` 만 모은다. 나머지는 세어서 보인다.""" - rows: dict[tuple[str, str], MaterialRow] = {} + rows: dict[tuple[str, str, str], MaterialRow] = {} skipped: dict[str, int] = {} for structure in unit_quantity_table.get("structures", []): label = str(structure.get("name") or structure.get("type_id") or "") @@ -268,12 +299,7 @@ def _collect( if destination != ACCEPTED_DESTINATION: skipped[destination] = skipped.get(destination, 0) + 1 continue - name = str(component.get("name") or "").strip() - unit = str(component.get("unit") or "").strip() - row = rows.setdefault((name, unit), MaterialRow(name=name, unit=unit)) - row.net_amount += float(component.get("amount") or 0.0) - if component.get("surcharge_included"): - row.surcharge_included = True + row = _add(rows, component) if label and label not in row.sources: row.sources.append(label) return rows, skipped @@ -298,12 +324,7 @@ def build_table( for item in extra_materials: if str(item.get("destination") or ACCEPTED_DESTINATION) != ACCEPTED_DESTINATION: continue - name = str(item.get("name") or "").strip() - unit = str(item.get("unit") or "").strip() - row = rows.setdefault((name, unit), MaterialRow(name=name, unit=unit)) - row.net_amount += float(item.get("amount") or 0.0) - if item.get("surcharge_included"): - row.surcharge_included = True + row = _add(rows, item) source = str(item.get("source") or "") if source and source not in row.sources: row.sources.append(source) @@ -312,13 +333,14 @@ def build_table( missing_rate: list[str] = [] missing_supply: list[str] = [] missing_install_by: list[str] = [] - for (name, _unit), row in rows.items(): - row.supply, row.install_by = _supply_of(supply.get(name)) + for row in rows.values(): + name = row.name + row.supply, row.install_by = _supply_of(_supply_setting(supply, row)) if row.supply == SUPPLY_UNKNOWN: - missing_supply.append(name) + missing_supply.append(row.supply_key) # ⚠ 설치 주체는 관급 줄에만 묻는다. 사급은 애초에 대상액 밖이라 비워 두는 것이 맞다. if row.supply == SUPPLY_OWNER and row.install_by is None: - missing_install_by.append(name) + missing_install_by.append(row.supply_key) if row.surcharge_included: continue lookup, alias_note = surcharge_lookup_name(name, concrete_placing_method) @@ -328,7 +350,7 @@ def build_table( if rate is None: missing_rate.append(name) - ordered = sorted(rows.values(), key=lambda item: (item.name, item.unit)) + ordered = sorted(rows.values(), key=lambda item: (item.name, item.spec, item.unit)) return { "columns": [ "자재명", @@ -343,6 +365,9 @@ def build_table( "rows": [ { "name": row.name, + "spec": row.spec, + # 관급구분을 고르는 열쇠 — 화면이 이 이름으로 설정에 적는다. + "supply_key": row.supply_key, "unit": row.unit, "net_amount": row.net_amount, "surcharge_pct": row.surcharge_pct, diff --git a/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py b/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py index 7bc01918..aa14fe80 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py @@ -133,17 +133,6 @@ def _library_rows( return rows -def _downstream_name(result: dict[str, Any]) -> str: - """뒤 단계(자재총괄·인계)가 찾는 이름 — 돌 줄만 종류 이름으로. - - ⚠ 명세 13장 Ⓒ 는 이름 고정 「돌」 + `spec` 이지만, 자재총괄·할증표·인계가 아직 **이름으로** - 찾음(1장 「문자열에서 코드로」 일감이 끝나기 전). ⏳ 부채 — 걷는 시점은 PLAN 10장. - """ - if result["name"] == "돌" and result.get("spec"): - return str(result["spec"]) - return str(result["name"]) - - def replace_with_templates( quantities: list[Any], inputs: list[dict[str, Any]], @@ -213,7 +202,8 @@ def replace_with_templates( if result.get("error"): quantity.notes.append(f"양식 줄 「{result['name']}」이 안 섬 — {result['error']}") continue - name = _downstream_name(result) + # 돌 줄도 이름 「돌」 + 규격에 종류 그대로(명세 13장 Ⓒ) — 자재총괄이 규격까지 보고 묶음. + name = str(result["name"]) user = source.get("source") == "user" basis = ( f"사용자 식 = {source.get('formula')} (양식 식 {source.get('default_formula')})" @@ -228,7 +218,7 @@ def replace_with_templates( str(source.get("destination") or ""), basis, source="user" if user else engine_source.get(name, ""), - spec="" if name != result["name"] else str(result.get("spec") or ""), + spec=str(result.get("spec") or ""), ) ) quantity.components = components diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 12ddea71..16574589 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -383,7 +383,6 @@ DESTINATION = { "돌쌓기": "unit_price", "돌붙임": "unit_price", "깬돌": "material", - "야면석": "material", "고임돌": "material", "막자갈": "material", # ⭐ 2026-09-09 사용자 확정 3차 ⑥ — **콘크리트는 자재 축에 세운다**(사용자 명시). @@ -406,11 +405,8 @@ DESTINATION = { # 보여 주기 줄이고, 「소광리에만 있는 줄」임을 사유에 적는다. "입적": "reference", "석적": "reference", - # 돌 종류를 고르면 그 이름으로 줄이 선다 — 자재총괄이 이름으로 찾으므로 넷 다 둔다. - "야면석·호박돌": "material", - "깬잡석": "material", - "견치돌": "material", - # 종류를 안 고른 경우의 이름 — 정본 계산표 줄 이름 그대로다(「돌 ℓ3=45cm」). + # 돌 줄은 **이름 「돌」 + 규격(`spec`)에 종류**다(명세 13장 Ⓒ) — 정본 계산표 줄 이름 그대로 + # (「돌 ℓ3=45cm」). 자재총괄이 (이름·규격·단위)로 묶으므로 종류 이름을 따로 두지 않는다. "돌": "material", # 기초잡석(품셈 12-25) — 운반·부설·다짐 품이 붙는 **공종**이라 일위대가로 간다. # 자재총괄로 보내면 같은 잡석이 재료로 한 번 더 선다. @@ -901,8 +897,8 @@ def stone_masonry( # 두되(다른 자리에서 쓸 수 있다) 여기서는 안 쓴다. kind_label = str(picked.get("kind") or "") # ⭐ 2026-09-13 브레인 판정 — **계수 열 고르기와 돌종류는 다른 축.** 「실무 관행」은 계수 열만 - # 바꾸고 종류는 지워선 안 됨(야면석이 「돌」·계산식 무게로 조용히 서던 결함). 돌 이름·무게는 - # **저장 제원의 종류**를 따름 — 아는 종류일 때만(모르는 글은 종전대로 「돌」). + # 바꾸고 종류는 지워선 안 됨(야면석이 「돌」·계산식 무게로 조용히 서던 결함). 돌 규격·무게는 + # **저장 제원의 종류**를 따름 — 아는 종류일 때만(모르는 글은 종전대로 규격 빈칸). chosen_kind = str(options.get(STONE_KIND_OPTION) or "").strip() stone_kind_name = ( chosen_kind if chosen_kind in (load_stone_kind_table().get("kinds") or []) else kind_label @@ -1001,12 +997,13 @@ def stone_masonry( else: components.append( Component( - stone_name, + "돌", "ton", masonry_area * stone_ton, - DESTINATION.get(stone_name, "material"), + DESTINATION["돌"], weight_basis, source=weight_source, + spec=stone_kind_name, ) ) @@ -1551,11 +1548,12 @@ def build_table( totals: dict[str, dict[str, Any]] = {} for item in quantities: for component in item.components: - key = f"{component.name}|{component.unit}" + key = f"{component.name}|{component.spec}|{component.unit}" entry = totals.setdefault( key, { "name": component.name, + "spec": component.spec, "unit": component.unit, "amount": 0.0, "destination": component.destination, diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py index bf0136ca..6fa008f6 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py @@ -73,7 +73,8 @@ BED_SILL_FORMS: dict[str, dict[str, Any]] = { }, "돌붙임(메)": { "back_len_m": 0.30, - "stone_name": "야면석", + "stone_name": "돌", + "stone_kind": "야면석", # 규격 칸으로(명세 13장 Ⓒ) "stone_spec": "20×20×30㎝", "stone_ton_per_m2": 0.42, "wedge_stone_m3_per_m2": 0.07, @@ -283,7 +284,7 @@ def erosion_check_dam( facing = top_m * (top_t - back_m) base_area = masonry + facing - rows: list[tuple[str, str, float, str]] = [ + rows: list[tuple] = [ ("돌쌓기", "㎡", masonry, f"정면적 {front_area:.4f}㎡ × √(1+n²) · 기울기 {slope_note}"), ("돌붙임", "㎡", facing, f"상장 {top_m:g}m × (상부두께 {top_t:g} − 뒷길이 {back_m:g})"), ( @@ -305,10 +306,11 @@ def erosion_check_dam( else: rows.append( ( - stone_name, + "돌", "ton", _ceil2(base_area * stone_ton), f"(돌쌓기+돌붙임) {base_area:.4f}㎡ {weight_tail}", + kind, # 종류는 규격 칸으로(명세 13장 Ⓒ) ) ) wedge_per = coeff["wedge_stone_m3_per_m2"] @@ -358,8 +360,8 @@ def erosion_check_dam( ) components = [ - Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis) - for name, unit, amount, basis in rows + Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis, spec="".join(spec)) + for name, unit, amount, basis, *spec in rows ] mpa, mpa_basis = fill_concrete_mpa(options) fill_per = coeff["fill_concrete_m3_per_m2"] @@ -548,7 +550,7 @@ def bed_sill(area_m2: float, options: dict[str, Any]) -> tuple[list[Component], return [], ["면적이 없어 전개하지 않음"] back = table["back_len_m"] - rows: list[tuple[str, str, float, str]] = [ + rows: list[tuple] = [ ("돌붙임", "㎡", area_m2, f"면적 {area_m2:g}㎡ (평면적 — 기울기 몫 없음)"), ("입적", "㎥", area_m2 * back, f"면적 × 두께 {back:g}m"), ( @@ -561,6 +563,7 @@ def bed_sill(area_m2: float, options: dict[str, Any]) -> tuple[list[Component], if form == "돌붙임(찰)" else " (원문 직접값 — 야면석은 유도식이 안 맞음)" ), + table.get("stone_kind", ""), ), ( "고임돌", @@ -581,8 +584,8 @@ def bed_sill(area_m2: float, options: dict[str, Any]) -> tuple[list[Component], rows.append(("터파기", "㎥", area_m2 * back, f"면적 × 두께 {back:g}m")) components = [ - Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis) - for name, unit, amount, basis in rows + Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis, spec="".join(spec)) + for name, unit, amount, basis, *spec in rows ] notes = [f"뒷길이 {back:g}m 는 정본 탭 붙박이 값입니다 — 바닥막이 제원에 뒷길이 칸이 없습니다"] if form == "돌붙임(찰)": diff --git a/B08_Quantity/B08_Quantity_UI_MaterialGrid.ts b/B08_Quantity/B08_Quantity_UI_MaterialGrid.ts index ce71a8cf..da19e5e9 100644 --- a/B08_Quantity/B08_Quantity_UI_MaterialGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_MaterialGrid.ts @@ -21,6 +21,10 @@ import { export interface MaterialRow { name: string; + /** 규격 — 돌 종류·콘크리트 강도(명세 13장 Ⓒ). 같은 이름도 규격이 다르면 다른 줄. */ + spec?: string; + /** 관급구분 설정 열쇠 — 이름 + 규격(서버 `MaterialRow.supply_key`). */ + supply_key?: string; unit: string; net_amount: number; surcharge_pct: number | null; @@ -101,6 +105,8 @@ export interface UnitQuantityStructure { /** 값이 어디서 왔나 — `derived`(치수에서 식으로) / `observed`(실무 관측 원단위표). */ basis_kind?: string; source?: string; + /** 규격 — 돌 종류·채움 강도. 이름은 「돌」로 고정이고 종류는 여기(명세 13장 Ⓒ). */ + spec?: string; /** 거푸집 줄만 — 몇 회짜리인가(품셈 1-7-1). 횟수별 재료 환산은 B09 몫이다. */ reuse_count?: number | null; reuse_note?: string; @@ -326,7 +332,8 @@ export function renderMaterialGrid( const body = document.createElement("tbody"); for (const row of table.rows) { const tr = document.createElement("tr"); - tr.append(textCell(row.name, "b08-grid__station")); + const key = row.supply_key ?? row.name; + tr.append(textCell(key, "b08-grid__station")); tr.append(textCell(row.unit, "b08-grid__unit")); tr.append(textCell(num(row.net_amount, 2))); // 미확보는 빈칸이 아니라 「-」 — 빈칸이면 0 % 로 오해된다. @@ -334,7 +341,7 @@ export function renderMaterialGrid( tr.append(textCell(num(row.total_amount, 2))); if (options) { // 관급/사급은 **자재마다 갈리는 발주 결정**이라 줄에서 고른다(2026-09-07 확정). - const chosen = options.choices[row.name] ?? { + const chosen = options.choices[key] ?? { supply: row.supply, install_by: row.install_by, }; @@ -343,20 +350,20 @@ export function renderMaterialGrid( INSTALL_BY_OPTIONS, chosen.supply !== "owner_supplied", // 관급 줄에만 고를 수 있다 (value) => { - const current = options.choices[row.name] ?? chosen; - options.choices[row.name] = { supply: current.supply, install_by: value || null }; + const current = options.choices[key] ?? chosen; + options.choices[key] = { supply: current.supply, install_by: value || null }; options.onChange(); }, ); tr.append( choiceCell(chosen.supply, SUPPLY_OPTIONS, false, (value) => { - const current = options.choices[row.name] ?? chosen; + const current = options.choices[key] ?? chosen; const next = { // 사급으로 되돌리면 설치 주체는 뜻을 잃으므로 비운다. supply: value, install_by: value === "owner_supplied" ? (current.install_by ?? null) : null, }; - options.choices[row.name] = next; + options.choices[key] = next; // ⚠ 표를 다시 그리지 않으므로 **여기서 바로 열고 닫는다** — 안 그러면 관급을 골라도 // 설치 주체 칸이 잠긴 채 남아 사용자가 못 정한다(만들고 화면에서 걸린 자리). const select = installCell.querySelector("select") as HTMLSelectElement | null; @@ -501,7 +508,7 @@ export function renderUnitQuantityGrid(response: MaterialResponse): HTMLElement tr.append(textCell(first ? structure.name : "", "b08-grid__station")); tr.append(textCell(first ? spec : "")); first = false; - tr.append(textCell(component.name)); + tr.append(textCell(`${component.name} ${component.spec ?? ""}`.trim())); tr.append(textCell(component.unit, "b08-grid__unit")); tr.append(textCell(num(component.amount, 3))); tr.append(textCell(DESTINATION_LABELS[component.destination] ?? component.destination)); diff --git a/resources/tester/test_b08_build_table_templates.py b/resources/tester/test_b08_build_table_templates.py index 5fb24a33..adbbf5ae 100644 --- a/resources/tester/test_b08_build_table_templates.py +++ b/resources/tester/test_b08_build_table_templates.py @@ -70,8 +70,9 @@ def test_양식_갈음은_값을_안_움직인다() -> None: want["spec"], ) assert got["amount"] == pytest.approx(want["amount"], rel=1e-9, abs=1e-12), got["name"] - # 돌 줄은 뒤 단계가 찾는 종류 이름으로(1장 코드 잇기 끝나기 전) · 관측 출처는 그대로. - stone = next(c for c in wet_after["components"] if c["name"] == "야면석·호박돌") + # 돌 줄은 이름 「돌」 + 규격에 종류(명세 13장 Ⓒ · 2026-09-13 과도기 부채 걷음) · 관측 출처는 그대로. + stone = next(c for c in wet_after["components"] if c["name"] == "돌") + assert stone["spec"] == "야면석·호박돌" assert stone["source"] == "uljin_library" diff --git a/resources/tester/test_b08_coeff_and_strength.py b/resources/tester/test_b08_coeff_and_strength.py index d129c7e9..b7f46c41 100644 --- a/resources/tester/test_b08_coeff_and_strength.py +++ b/resources/tester/test_b08_coeff_and_strength.py @@ -77,7 +77,8 @@ def test_돌_중량은_값을_두고_근거를_남긴다() -> None: **어느 조건에서 다는지**가 달라졌으므로 조건을 명시한다. """ got, _ = 성분(stone_kind="야면석·호박돌") - 야면석 = got["야면석·호박돌"] + 야면석 = got["돌"] # 이름 「돌」 + 규격에 종류(명세 13장 Ⓒ) + assert 야면석.spec == "야면석·호박돌" assert abs(야면석.amount - got["돌쌓기"].amount * 0.88) < 1e-9 assert "울진" in 야면석.basis and "품셈·교본에는 돌중량표가 없음" in 야면석.basis assert 야면석.source == "uljin_library" diff --git a/resources/tester/test_b08_material_summary.py b/resources/tester/test_b08_material_summary.py index 07c62699..98d71ebc 100644 --- a/resources/tester/test_b08_material_summary.py +++ b/resources/tester/test_b08_material_summary.py @@ -153,6 +153,43 @@ def test_단위가_다르면_다른_줄() -> None: assert table["row_count"] == 2 +def test_규격이_다르면_다른_줄이고_옛_관급_열쇠도_받음() -> None: + """돌 줄은 이름 「돌」 + 규격에 종류(명세 13장 Ⓒ · 2026-09-13 과도기 부채 걷음). + + ⚠ 옛 설정 열쇠 — 종류 이름(「야면석·호박돌」)·이름만(「채움콘크리트」)도 받아야 + 사용자가 정한 값이 안 사라짐. + """ + table = build_table( + 원단위표( + 성분("돌", "ton", 3.0, spec="야면석·호박돌"), + 성분("돌", "ton", 2.0, spec="깬잡석"), + 성분("채움콘크리트", "㎥", 1.0, spec="180"), + 성분("채움콘크리트", "㎥", 4.0, spec="210"), + ), + supply_map={ + "야면석·호박돌": SUPPLY_CONTRACTOR, # 옛 열쇠(종류 이름) + "돌 깬잡석": SUPPLY_OWNER, # 새 열쇠(이름 + 규격) + "채움콘크리트": SUPPLY_CONTRACTOR, # 옛 열쇠(이름만) + }, + ) + by = {(r["name"], r["spec"]): r for r in table["rows"]} + assert table["row_count"] == 4 + assert by[("돌", "야면석·호박돌")]["net_amount"] == pytest.approx(3.0) + assert by[("돌", "야면석·호박돌")]["supply"] == SUPPLY_CONTRACTOR + assert by[("돌", "깬잡석")]["supply"] == SUPPLY_OWNER + assert by[("돌", "깬잡석")]["supply_key"] == "돌 깬잡석" + assert by[("채움콘크리트", "210")]["supply"] == SUPPLY_CONTRACTOR + assert table["missing_install_by_materials"] == ["돌 깬잡석"] + + +def test_바닥막이_메붙임_돌은_규격에_야면석() -> None: + from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import bed_sill + + components, _notes = bed_sill(10.0, {"form": "돌붙임(메)"}) + stone = next(c for c in components if c.unit == "ton") + assert (stone.name, stone.spec, stone.destination) == ("돌", "야면석", "material") + + # ── 할증률은 데이터에서, 없으면 드러낸다 ──────────────────────────── diff --git a/resources/tester/test_b08_structure_template_parity.py b/resources/tester/test_b08_structure_template_parity.py index c7524e3c..0fc802b8 100644 --- a/resources/tester/test_b08_structure_template_parity.py +++ b/resources/tester/test_b08_structure_template_parity.py @@ -104,12 +104,9 @@ def test_양식_값이_전개와_같다(case: dict) -> None: ) # ⓘ 「실무 관행」 계수가 돌종류를 지우던 전개 결함은 2026-09-13 고침(브레인 판정) — 차이 없음. for row, component in zip(rows, components): - if row["name"] == "돌": - # 명세 13장 Ⓒ — 양식은 이름 고정 + 규격에 종류, 전개는 종류를 이름으로 씀. - assert component["name"] == (row["spec"] or "돌") - else: - assert row["name"] == component["name"] - assert row["spec"] == component["spec"], (row, component) + # 명세 13장 Ⓒ — 돌 줄도 양식·전개 모두 이름 고정 「돌」 + 규격에 종류(2026-09-13). + assert row["name"] == component["name"] + assert row["spec"] == component["spec"], (row, component) assert template["rows"][row["seq"] - 1]["destination"] == component["destination"], row assert float(row["amount"]) == pytest.approx(component["amount"], rel=1e-9, abs=1e-12), ( row["name"], diff --git a/resources/tester/test_b08_unit_quantity.py b/resources/tester/test_b08_unit_quantity.py index 050f6bcc..8e7249ef 100644 --- a/resources/tester/test_b08_unit_quantity.py +++ b/resources/tester/test_b08_unit_quantity.py @@ -275,7 +275,7 @@ def test_실무_관행_계수가_돌종류를_지우지_않는다() -> None: """2026-09-13 브레인 판정 — 계수 열 고르기와 돌종류는 다른 축. 옛 전개는 「실무 관행」을 고르면 야면석이 「돌」·계산식 무게(0.918)로 조용히 섰음. - 이제 계수만 참고자료 열(고임돌 0.15)로 가고, 돌은 야면석 이름·관측 무게(0.88)를 지킴. + 이제 계수만 참고자료 열(고임돌 0.15)로 가고, 돌은 야면석 규격·관측 무게(0.88)를 지킴. """ result = expand( 돌쌓기찰( @@ -288,9 +288,10 @@ def test_실무_관행_계수가_돌종류를_지우지_않는다() -> None: ) ) 돌쌓기 = 성분(result, "돌쌓기").amount - assert 성분(result, "야면석·호박돌").amount == pytest.approx(돌쌓기 * 0.88) + 돌 = 성분(result, "돌") # 이름 「돌」 + 규격에 종류(명세 13장 Ⓒ) + assert 돌.spec == "야면석·호박돌" + assert 돌.amount == pytest.approx(돌쌓기 * 0.88) assert 성분(result, "고임돌").amount == pytest.approx(돌쌓기 * 0.15) # 관행 열 - assert not any(c.name == "돌" for c in result.components) def test_물구멍관_길이는_평균두께() -> None: