Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
415 lines
14 KiB
Python
415 lines
14 KiB
Python
"""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"],
|
|
},
|
|
}
|