"""Z01 기초데이터 아홉 — 품셈 근거 8 + 돌쌓기 갈래 1(2026-09-16 사용자 지시 · 브레인 승인). 사용자 잣대: 기초데이터 = **로직으로 계산되는 값이 아니라 값만으로 정의되는 데이터셋.** 태움 — 토량환산 · 자재 할증 · 거푸집 전용 · 철근 갈래 · 목재 갈래 · 돌쌓기 경사·뒷길이·돌 종류·갈래 안 태움 — 이음표(`type_map`·`form_map`·`bond.codes`) · 품셈 원문 표(coef `surcharge_*`) · 글·방침 · 구조물 실측(울진 한 현장 관찰값) · 기슭막이 교본값(값과 글이 한 줄에 섞임) ⚠ 축 칸(`@axis`)은 못 고침 — 그 칸이 `@id` 를 이루므로 바꾸면 덮개가 줄을 못 찾음. ⚠ 원문 중복(coef 「암괴…점토」 두 줄)은 **값이 같을 때만** 한 줄로 합침 — 갈리면 `DuplicateRowError`(브레인 조건). """ from __future__ import annotations from typing import Any _ROW = dict[str, Any] def _brackets(steps: list[float]) -> list[str]: """직고 구간 이름 — 원문 표기(`∼1.5 · ∼3 · ∼5 · ∼7 · 7이상`)를 그대로 씀.""" return [f"~{s:g}" for s in steps] + [f"{steps[-1]:g}이상"] def _row(row_id: str, source: str, axis: tuple[str, ...], **cells: Any) -> _ROW: return {"@id": row_id, "@source": source, "@axis": axis, **cells} def _coef(doc: dict[str, Any], name: str) -> list[_ROW]: """토량환산계수 L·C — 품셈 체적변화율. 원문 표(`surcharge_*`)·공구손료율은 여기 아님.""" rows = [] for table in ("coef_soil_L", "coef_soil_C"): for record in doc["variables"][table]["records"]: rows.append( _row( f"{table}/{record['soil_type']}", name, ("table", "soil_type"), table=table, soil_type=record["soil_type"], min=record.get("min"), max=record.get("max"), rule=record.get("rule"), selection=record.get("selection"), ) ) return rows def _material_surcharge(doc: dict[str, Any], name: str) -> list[_ROW]: return [ _row( f"rates_pct/{r['material']}", name, ("material",), material=r["material"], rate=r.get("rate"), condition=r.get("condition"), alt_rate=r.get("alt_rate"), alt_condition=r.get("alt_condition"), pumsem=r.get("pumsem"), ) for r in doc["rates_pct"] ] def _formwork_reuse(doc: dict[str, Any], name: str) -> list[_ROW]: rows = [] for r in doc["reuse_by_class"]: rows.append( _row( f"reuse_by_class/{r['class']}", name, ("table", "class"), table="reuse_by_class", **{"class": r["class"]}, reuse_count=r.get("reuse_count"), examples=r.get("examples"), ) ) ratios = doc["reuse_ratio_pct"] for material in ("plywood", "timber"): for count, percent in (ratios.get(material) or {}).items(): rows.append( _row( f"reuse_ratio_pct/{material}/{count}", name, ("table", "material", "reuse_count"), table="reuse_ratio_pct", material=material, reuse_count=int(count), ratio_pct=percent, ) ) for r in doc["euroform_type"]["classes"]: rows.append( _row( f"euroform_type/classes/{r['key']}", name, ("table", "class"), table="euroform_type/classes", **{"class": r["key"]}, daily_area_m2=r.get("daily_area_m2"), examples=r.get("examples"), ) ) return rows def _rebar_complexity(doc: dict[str, Any], name: str) -> list[_ROW]: rows = [ _row( f"classes/{r['key']}", name, ("table", "class"), table="classes", **{"class": r["key"]}, examples=r.get("examples"), ) for r in doc["classes"] ] for key, price in (doc["price_hint_krw_per_ton"].get("values") or {}).items(): rows.append( _row( f"price_hint_krw_per_ton/{key}", name, ("table", "class"), table="price_hint_krw_per_ton", **{"class": key}, price_krw_per_ton=price, ) ) return rows def _timber_structure_class(doc: dict[str, Any], name: str) -> list[_ROW]: return [ _row( f"classes/{r['key']}", name, ("class",), **{"class": r["key"]}, carpenter=r.get("carpenter"), laborer=r.get("laborer"), examples=r.get("examples"), ) for r in doc["classes"] ] def _masonry_slope(doc: dict[str, Any], name: str) -> list[_ROW]: brackets = _brackets(doc["steps_m"]) rows = [] for bond, faces in doc["table"].items(): for face, values in faces.items(): for bracket, ratio in zip(brackets, values): rows.append( _row( f"table/{bond}/{face}/{bracket}", name, ("bond", "face", "height_bracket"), bond=bond, face=face, height_bracket=bracket, face_slope_ratio=ratio, ) ) return rows def _masonry_back_length(doc: dict[str, Any], name: str) -> list[_ROW]: brackets = _brackets(doc["steps_m"]) rows = [] for bond, pairs in doc["table_cm"].items(): for bracket, pair in zip(brackets, pairs): rows.append( _row( f"table_cm/{bond}/{bracket}", name, ("bond", "height_bracket"), bond=bond, height_bracket=bracket, min_cm=pair[0], max_cm=pair[1], # 원문 「-」 는 그대로 빈 칸 ) ) return rows def _stone_kind(doc: dict[str, Any], name: str) -> list[_ROW]: rows = [] def unit_rows(table: str, holder: dict[str, Any]) -> None: for kind, lengths in holder.items(): if not isinstance(lengths, dict): continue for cm, value in lengths.items(): rows.append( _row( f"{table}/{kind}/{cm}", name, ("table", "stone_kind", "back_length_cm"), table=table, stone_kind=kind, back_length_cm=int(cm), m3_per_m2=value, ) ) for table in ("wedge_stone_m3_per_m2", "fill_concrete_m3_per_m2"): unit_rows(table, doc[table]) for kind, ratio in doc["backfill_ratio_of_back_length"].items(): if isinstance(ratio, (int, float)): rows.append( _row( f"backfill_ratio_of_back_length/{kind}", name, ("table", "stone_kind"), table="backfill_ratio_of_back_length", stone_kind=kind, ratio=ratio, ) ) fallback = doc["fallback"] # 돌 종류 미지정일 때 쓰는 한 벌(건설품셈 참고자료) for table in ("wedge_stone_m3_per_m2", "fill_concrete_m3_per_m2"): for cm, value in (fallback.get(table) or {}).items(): rows.append( _row( f"fallback/{table}/{cm}", name, ("table", "back_length_cm"), table=f"fallback/{table}", back_length_cm=int(cm), m3_per_m2=value, ) ) if isinstance(fallback.get("backfill_ratio_of_back_length"), (int, float)): rows.append( _row( "fallback/backfill_ratio_of_back_length", name, ("table",), table="fallback/backfill_ratio_of_back_length", ratio=fallback["backfill_ratio_of_back_length"], ) ) return rows def _masonry_class(doc: dict[str, Any], name: str) -> list[_ROW]: rows = [ _row( f"back_length/classes/{r['key']}", name, ("table", "class"), table="back_length/classes", **{"class": r["key"]}, max_cm=r.get("max_cm"), ) for r in doc["back_length"]["classes"] ] for value in doc["boulder_diameter"]["classes"]: rows.append( _row( f"boulder_diameter/{value}", name, ("table", "class"), table="boulder_diameter", **{"class": value}, ) ) for type_id, ratio in doc["face_slope"]["by_type"].items(): rows.append( _row( f"face_slope/{type_id}", name, ("table", "type_id"), table="face_slope", type_id=type_id, face_slope_ratio=ratio, ) ) return rows def _machine_productivity(doc: dict[str, Any], name: str) -> list[_ROW]: """기계 작업량 밑값 — 코드에 박혀 있던 값을 꺼낸 자리(불도저 속도·삽날 · 덤프 운반·적재 계수). ⚠ 식(`Q = n·q·f·E` 따위)은 값이 아니라 로직이라 계산 모듈에 남는다 — 여기는 계수만. """ rows = [] for r in doc["variables"]["dozer_speed"]["records"]: rows.append( _row( f"dozer_speed/{r['track']}/{r['tonnage_ton']}/{r['gear']}", name, ("table", "track", "tonnage_ton", "gear"), table="dozer_speed", track=r["track"], tonnage_ton=float(r["tonnage_ton"]), gear=int(r["gear"]), forward_m_per_min=float(r["forward_m_per_min"]), reverse_m_per_min=float(r["reverse_m_per_min"]), ) ) for r in doc["variables"]["dozer_blade"]["records"]: rows.append( _row( f"dozer_blade/{r['track']}/{r['tonnage_ton']}", name, ("table", "track", "tonnage_ton"), table="dozer_blade", track=r["track"], tonnage_ton=float(r["tonnage_ton"]), blade_m3=float(r["blade_m3"]), ) ) for r in doc["variables"]["dump_haul"]["records"]: rows.append( _row( f"dump_haul/{r['key']}", name, ("table", "key"), table="dump_haul", key=r["key"], value=float(r["value"]), unit=r.get("unit"), meaning=r.get("meaning"), ) ) for r in doc["variables"]["dump_material"]["records"]: rows.append( _row( f"dump_material/{r['work_item_code']}", name, ("table", "work_item_code"), table="dump_material", work_item_code=r["work_item_code"], label=r["label"], unit_weight_ton_per_m3=float(r["unit_weight_ton_per_m3"]), loose_factor=float(r["loose_factor"]), bucket_factor=float(r["bucket_factor"]), loader_efficiency=float(r["loader_efficiency"]), truck_efficiency=float(r["truck_efficiency"]), loading_efficiency=float(r["loading_efficiency"]), notes=r.get("notes"), loading_note=r.get("loading_note"), ) ) return rows #: kind → 원본 파일 id · 줄 만드는 길 · 고칠 칸. 이름은 **자료 이름 그대로**(브레인). SPEC: dict[str, dict[str, Any]] = { "coef": {"file": "coef", "build": _coef, "editable": ["min", "max"]}, "material_surcharge": { "file": "material_surcharge", "build": _material_surcharge, "editable": ["rate", "alt_rate"], }, "formwork_reuse": { "file": "formwork_reuse", "build": _formwork_reuse, "editable": ["reuse_count", "ratio_pct", "daily_area_m2"], }, "rebar_complexity": { # ⚠ 단가 참고값은 **B09 가 자재 단가로 셈한 값**이라 고칠 칸이 아님(코덱스 검증 1 · 2026-09-16). # 사용자 잣대 「마스터 = 계산으로 나오는 값이 아니라 값 그 자체」와 어긋나 editable 에서 뺌. "file": "rebar_complexity", "build": _rebar_complexity, "editable": [], "locked": { "price_krw_per_ton": "계산값 — B09 가 자재 단가로 셈(표시 전용 · 밑값을 고쳐야 바뀜)" }, "formula": { "price_krw_per_ton": "갈래별 이형철근 단가 참고값 = B09 가 자재 단가에서 셈 — 표시 전용(B08 계산에 안 듦)" }, }, "timber_structure_class": { "file": "timber_structure_class", "build": _timber_structure_class, "editable": ["carpenter", "laborer"], }, "masonry_slope": { "file": "masonry_slope", "build": _masonry_slope, "editable": ["face_slope_ratio"], }, "masonry_back_length": { "file": "masonry_back_length", "build": _masonry_back_length, "editable": ["min_cm", "max_cm"], }, "stone_kind": {"file": "stone_kind", "build": _stone_kind, "editable": ["m3_per_m2", "ratio"]}, "machine_productivity": { "file": "machine_productivity", "build": _machine_productivity, "editable": [ "forward_m_per_min", "reverse_m_per_min", "blade_m3", "value", "unit_weight_ton_per_m3", "loose_factor", "bucket_factor", "loader_efficiency", "truck_efficiency", "loading_efficiency", ], }, "masonry_class": { "file": "masonry_class", "build": _masonry_class, "editable": ["max_cm", "face_slope_ratio"], }, }