fix(b08): 자재총괄 화면·인계를 한 벌로 — material_table_for 가 규준틀 재료까지 얹고 두 라우터가 같이 부름(화면 6줄·인계 9줄 갈림 닫음)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -146,6 +146,33 @@ async def _section_modes(project_id: UUID) -> dict[float, str]:
|
||||
return section_modes_from_designs(designs)
|
||||
|
||||
|
||||
def material_table_for(
|
||||
unit_table: dict[str, Any],
|
||||
settings: dict[str, Any],
|
||||
preparation_table: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""자재총괄 **한 벌** — 화면(`material-summary`)·인계(`handoff`)가 같이 부름(브레인 판정).
|
||||
|
||||
⚠ 규준틀 재료는 **준비공 개소가 선 뒤에야** 서므로 준비공 표를 받아 여기서 얹음. 종전엔 인계만
|
||||
얹어 화면 6줄 · 인계 9줄로 갈림 → 화면에서 규준틀 재료의 관급구분·할증을 못 만짐.
|
||||
⚠ 화면 쪽에 따로 복사하지 말 것 — 두 벌이 되면 또 갈림.
|
||||
⚠ 값은 **제안값(실무 관측)**이고 산출 조건에서 고칠 수 있음 — 그 사실이 줄 사유에 적힘.
|
||||
"""
|
||||
frame_rows = [
|
||||
row
|
||||
for row in ((preparation_table or {}).get("rows") or [])
|
||||
if str(row.get("item") or "").endswith("규준틀")
|
||||
]
|
||||
return build_material_table(
|
||||
unit_table,
|
||||
supply_map=settings.get("material_supply") or {},
|
||||
surcharge_overrides=settings.get("material_surcharge") or {},
|
||||
# 콘크리트 할증은 **레미콘일 때만** 붙는다 — 방식이 이름을 가른다(확정 3차 ⑥).
|
||||
concrete_placing_method=settings.get("concrete_placing_method"),
|
||||
extra_materials=frame_material_rows(frame_rows, settings.get("frame_material") or {}),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/quantity/material-summary")
|
||||
async def get_material_summary(project_id: UUID) -> JSONResponse:
|
||||
"""구조물 원단위와 자재총괄을 **한 응답**으로 낸다.
|
||||
@@ -183,13 +210,9 @@ async def get_material_summary(project_id: UUID) -> JSONResponse:
|
||||
# 프로젝트에 박힌 양식(PLAN 4장) — 같은 까닭.
|
||||
structure_templates=project_templates(project_root),
|
||||
)
|
||||
material_table = build_material_table(
|
||||
unit_table,
|
||||
supply_map=settings.get("material_supply") or {},
|
||||
surcharge_overrides=settings.get("material_surcharge") or {},
|
||||
# 콘크리트 할증은 **레미콘일 때만** 붙는다 — 방식이 이름을 가른다(확정 3차 ⑥).
|
||||
concrete_placing_method=settings.get("concrete_placing_method"),
|
||||
)
|
||||
# 인계와 같은 한 벌 — 규준틀 재료가 준비공 표를 따라 서므로 토공 표를 받음.
|
||||
earthwork = await _earthwork_tables(project_id)
|
||||
material_table = material_table_for(unit_table, settings, earthwork.get("preparation"))
|
||||
# 묶음으로 서는 구조물의 조각을 화면에도 보인다 — 코드만으로는 사람이 검증 못 한다.
|
||||
handoff = build_handoff(unit_quantity_table=unit_table)
|
||||
composite = [
|
||||
@@ -437,30 +460,10 @@ async def get_handoff(project_id: UUID) -> JSONResponse:
|
||||
structure_formulas=settings.get("structure_formula_overrides"),
|
||||
structure_templates=project_templates(project_root),
|
||||
)
|
||||
material_table = build_material_table(
|
||||
unit_table,
|
||||
supply_map=settings.get("material_supply") or {},
|
||||
surcharge_overrides=settings.get("material_surcharge") or {},
|
||||
concrete_placing_method=settings.get("concrete_placing_method"),
|
||||
)
|
||||
|
||||
# 토공·운반 표는 토적표 라우터의 것을 그대로 쓴다 — 여기서 다시 만들지 않는다.
|
||||
earthwork = await _earthwork_tables(project_id)
|
||||
|
||||
# 규준틀 재료 — **개소가 선 뒤에야 설 수 있어** 준비공 표를 받은 다음 자재 축에 얹는다.
|
||||
# ⚠ 값은 **제안값(실무 관측)**이고 산출 조건에서 고칠 수 있다 — 그 사실이 줄 사유에 적힌다.
|
||||
frame_rows = [
|
||||
row
|
||||
for row in ((earthwork.get("preparation") or {}).get("rows") or [])
|
||||
if str(row.get("item") or "").endswith("규준틀")
|
||||
]
|
||||
material_table = build_material_table(
|
||||
unit_table,
|
||||
supply_map=settings.get("material_supply") or {},
|
||||
surcharge_overrides=settings.get("material_surcharge") or {},
|
||||
concrete_placing_method=settings.get("concrete_placing_method"),
|
||||
extra_materials=frame_material_rows(frame_rows, settings.get("frame_material") or {}),
|
||||
)
|
||||
# 자재총괄 — 화면과 같은 한 벌(규준틀 재료 포함).
|
||||
material_table = material_table_for(unit_table, settings, earthwork.get("preparation"))
|
||||
|
||||
handoff = build_handoff(
|
||||
summary_table=earthwork.get("summary"),
|
||||
|
||||
@@ -85,6 +85,28 @@ def test_자재_축으로_간다() -> None:
|
||||
assert all(row["destination"] == "material" for row in frame_material_rows(규준틀))
|
||||
|
||||
|
||||
def test_화면과_인계가_같은_한_벌을_부른다() -> None:
|
||||
"""자재총괄 화면(6줄)·인계(9줄)가 갈리던 자리 — 규준틀 재료를 한 곳에서 얹음(2026-09-14).
|
||||
|
||||
⚠ 두 라우터가 이 함수를 부르는지까지 본다 — 한쪽이 옛 길로 돌아가면 다시 갈림.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
from B08_Quantity import B08_Quantity_Router_Material as router
|
||||
|
||||
table = router.material_table_for(
|
||||
{"structures": []},
|
||||
{"material_supply": {"각재 50×50": "owner_supplied"}},
|
||||
{"rows": 규준틀},
|
||||
)
|
||||
keys = {row["supply_key"]: row for row in table["rows"]}
|
||||
assert set(keys) == {"각재 50×50", "판재 T12", "못"}
|
||||
assert keys["각재 50×50"]["supply"] == "owner_supplied" # 화면에서 고른 값이 먹음
|
||||
for route in (router.get_material_summary, router.get_handoff):
|
||||
source = inspect.getsource(route)
|
||||
assert "material_table_for(" in source and "build_material_table(" not in source
|
||||
|
||||
|
||||
def test_제안값이_실무_관측값과_같다() -> None:
|
||||
"""울진 소광 §8 — 각재 50×50 0.0044㎥ · 판재 T12 0.0029㎥ · 못 0.03㎏(개소당)."""
|
||||
assert FRAME_MATERIAL_SUGGESTED["각재 50×50"][0] == 0.0044
|
||||
|
||||
Reference in New Issue
Block a user