정본이 줄마다 INT(x*100)/100 으로 자르고 다음 줄이 그 잘린 값을 받으므로 표시용이 아니라 계산의 일부임. 안 자르면 막자갈 10.55 · 잔토 2.75 로 정본과 갈림. round(4.905) 가 짝수 반올림으로 4.90 이 되는 자리(물구멍관)도 함께 적음. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
580 lines
29 KiB
Python
580 lines
29 KiB
Python
"""기슭막이 전개 — **형태로 갈라 돌쌓기 식을 그대로 쓴다**(3단계, 2026-09-09).
|
||
|
||
실무 정본이 근거다 — `구조물도/기슭막이/04.구조도(기슭막이).xls` 의 탭 제목이
|
||
**「돌기슭막이(H=2.0m, 찰쌓기, 기초유)」** 이고, 그 안의 계산이 돌쌓기와 **같은 식**이다.
|
||
|
||
정면적 → 비탈면적(×√(1+n²)) → 평균두께 → 입적 → 돌 · 막자갈 · 고임돌 →
|
||
터파기(기초 + 비탈) → 되메우기 → 잔토
|
||
|
||
⇒ **기슭막이는 새 식이 아니라 「형태가 돌쌓기면 돌쌓기 식」**이다. 여기서 식을 다시 짜면
|
||
같은 계산이 두 벌이 되어 돌쌓기와 갈린다(CLAUDE.md 5장).
|
||
|
||
⚠ **형태가 돌쌓기가 아닌 갈래는 식이 없다** — 콘크리트·돌망태·통나무/목재틀·바자.
|
||
지어내지 않고 **왜 없는지**를 줄로 남긴다.
|
||
|
||
⚠ **뒷길이·돌 종류 칸이 기슭막이 등록부에 아직 없다**(2026-09-09 실측). 돌쌓기 식은 그 둘로
|
||
계수가 갈리는데, 없으면 `_back_length` 가 **조용히 45㎝ 로 돈다**. 값이 나오므로 아무도
|
||
못 알아채는 자리라 **그 사실을 줄로 드러낸다** — 「값이 있기는 하니 안 보이는」 그것.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from decimal import ROUND_HALF_UP, Decimal
|
||
from typing import Any
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import Component, _num, stone_masonry
|
||
|
||
#: 형태 → 돌쌓기 갈래(찰이면 True). 여기 없는 형태는 전개식이 없다.
|
||
STONE_FORMS: dict[str, bool] = {
|
||
"돌쌓기(찰)": True,
|
||
"돌쌓기(메)": False,
|
||
}
|
||
|
||
#: 식이 없는 형태와 **왜 없는지**. 품셈 장이 다르거나 원단위가 원문에 없다.
|
||
WITHHELD_FORMS: dict[str, str] = {
|
||
"콘크리트": (
|
||
"콘크리트 기슭막이는 돌쌓기(품셈 13-4)가 아니라 콘크리트 구조물이라 전개식이 다름 "
|
||
"— 벽 두께·저판이 정본에 없어 물량이 서지 않음"
|
||
),
|
||
"돌망태": ("돌망태는 품셈 13-8 이고 규격 축이 망태 치수라 돌쌓기 표를 못 씀 — 원단위 미확보"),
|
||
"통나무·목재틀": (
|
||
"목재틀은 품셈 13-13 이고 밑수가 ㎥당 목공 품이라 돌쌓기 표를 못 씀 "
|
||
"— 각재·판재 자재가 카탈로그에 없어 값이 모자람"
|
||
),
|
||
"바자": "바자얽기(품셈 5-15)는 별도 일위대가를 만들어 잇기로 확정(2026-09-09 ⑧-4)",
|
||
}
|
||
|
||
#: 돌붙임 원단위 — 소광리 정본 「돌붙임L3=45(사방)」(찰) · 「돌붙임L3=30(야면석메붙임)」(메).
|
||
#: ⚠⚠ **돌쌓기 표(13-4)와 합치지 말 것.** 값이 겹쳐 보여 합치고 싶어지는 자리다:
|
||
#:
|
||
#: 돌쌓기(13-4) 밑수 **비탈면적** · 뒷길이 **일곱 규격** · **기울기 몫 √(1+n²)**
|
||
#: 돌붙임(13-5) 밑수 **평면적** · 뒷길이 **넷** · **기울기 몫 없음**
|
||
#: 메는 모르타르·채움콘크리트가 통째로 빠진다
|
||
#:
|
||
#: ⚠ 45㎝ 에서 고임돌 0.15 · 채움 0.20 이 **우연히 같다** — 그것이 합치고 싶어지는 까닭이고,
|
||
#: 합치면 뒷길이 25·30·60·75 에서 조용히 틀린다(돌붙임 표에 없는 규격이다).
|
||
#:
|
||
#: ⚠ **뒷길이는 지금 붙박이다** — 찰 0.45m · 메 0.30m. 정본 탭이 그 둘로만 있고 바닥막이
|
||
#: 등록부에 뒷길이 칸이 없다. 칸이 생기면 이 표를 열면 된다.
|
||
#: ⓘ 돌중량은 **관측값이 아니라 유도값**이다 — `뒷길이 × 0.77(채움률) × 2.65(비중)`.
|
||
#: 찰 0.45 × 0.77 × 2.65 = 0.918 ton/㎡. 메(야면석)는 0.42 로 **원문에 값이 직접 적혀 있어**
|
||
#: 같은 유도식이 안 맞는다(야면석은 공극이 커 ㎥당 무게가 다르다) — 그대로 옮긴다.
|
||
BED_SILL_FORMS: dict[str, dict[str, Any]] = {
|
||
"돌붙임(찰)": {
|
||
"back_len_m": 0.45,
|
||
"stone_name": "돌",
|
||
"stone_spec": "30×30×45㎝",
|
||
"stone_ton_per_m2": 0.45 * 0.77 * 2.65,
|
||
"wedge_stone_m3_per_m2": 0.15,
|
||
"mortar_m3_per_m2": 0.009,
|
||
"fill_concrete_m3_per_m2": 0.2,
|
||
"blinding_m3_per_m2": 0.1,
|
||
},
|
||
"돌붙임(메)": {
|
||
"back_len_m": 0.30,
|
||
"stone_name": "야면석",
|
||
"stone_spec": "20×20×30㎝",
|
||
"stone_ton_per_m2": 0.42,
|
||
"wedge_stone_m3_per_m2": 0.07,
|
||
# 메붙임은 모르타르·채움콘크리트·버림이 **없다**(정본 탭에 줄 자체가 없음).
|
||
"mortar_m3_per_m2": None,
|
||
"fill_concrete_m3_per_m2": None,
|
||
"blinding_m3_per_m2": None,
|
||
},
|
||
}
|
||
|
||
#: 돌쌓기 식이 계수를 가르는 데 쓰는 칸 — 기슭막이 등록부에 아직 없는 것을 알린다.
|
||
STONE_SPEC_KEYS: tuple[tuple[str, str], ...] = (
|
||
("back_len_cm", "뒷길이"),
|
||
("stone_kind", "돌 종류"),
|
||
)
|
||
|
||
|
||
def revetment(
|
||
height_m: float,
|
||
length_m: float,
|
||
options: dict[str, Any],
|
||
face: str | None = None,
|
||
face_reason: str = "",
|
||
) -> tuple[list[Component], list[str]]:
|
||
"""기슭막이 1구간 전개 — 형태가 돌쌓기면 그 식, 아니면 사유만."""
|
||
form = str(options.get("form") or "").strip()
|
||
if form not in STONE_FORMS:
|
||
why = WITHHELD_FORMS.get(form)
|
||
if why:
|
||
return [], [f"기슭막이 형태 「{form}」 — {why}"]
|
||
return [], [
|
||
f"기슭막이 형태가 정해지지 않았습니다(지금 「{form or '빈 값'}」) "
|
||
"— 형태를 골라야 물량이 섭니다"
|
||
]
|
||
|
||
components, notes = stone_masonry(
|
||
height_m, length_m, options, STONE_FORMS[form], face, face_reason
|
||
)
|
||
# ⚠ 계수를 가르는 칸이 없으면 **기본값으로 조용히 돈다** — 그 사실을 드러낸다.
|
||
missing = [label for key, label in STONE_SPEC_KEYS if not options.get(key)]
|
||
if missing and components:
|
||
notes.append(
|
||
f"⚠ 기슭막이 제원에 {' · '.join(missing)} 칸이 없어 **돌쌓기 기본값으로 섰음** "
|
||
"— 그 값이 고임돌·야면석·채움콘크리트 계수를 가름"
|
||
)
|
||
return components, notes
|
||
|
||
|
||
#: 돌골막이 원단위 — 소광리 정본 「골막이(찰)(치수조서연결)」, **개소당**.
|
||
#: 「돌-골막이치수」 탭이 개소별 상장·하장·높이를 받아 **평균치수**를 내고, 이 표가 그 평균
|
||
#: 하나로 개소당 수량을 낸다. 우리는 개소마다 제 치수로 돌린다 — 밑수가 같으므로 식은 그대로다.
|
||
#:
|
||
#: ⚠⚠ **돌쌓기(기슭막이) 식과 합치지 말 것.** 넷이 다르다:
|
||
#:
|
||
#: 정면적 **사다리꼴** (상장+하장)÷2×H − 파형강관 ↔ 기슭막이 직사각 H×1
|
||
#: 두께식 (ℓ3+0.1H) / (ℓ3+0.4H) 의 평균 ↔ 기슭막이 ℓ3+0.30 / +0.30(H−1)
|
||
#: 밑수 **돌쌓기 + 돌붙임** ↔ 기슭막이 돌쌓기만
|
||
#: 단위 **개소당** ↔ 기슭막이 m당 (확정 ⑦ 「통일 안 함」)
|
||
#:
|
||
#: ⓘ 앞서 이 두께식을 「0.45 + 0.1H」로 적어 두었으나 **0.45 는 상수가 아니라 뒷길이 ℓ3**
|
||
#: 이다(정본 C26 이 `P6/100` 을 읽는다 — P6 = 45㎝). 두 식 모두 뒷길이 기반이고
|
||
#: **더하는 몫만 다르다.**
|
||
#: ⓘ 같은 것 — 막자갈 **2/3** · 야면석 **0.88 ton/㎡** · 물구멍 **2㎡/개소 × 0.5m** ·
|
||
#: 고임돌 **0.15** · 채움콘크리트 **0.2** 는 두 시트가 같은 값이라 표를 함께 쓴다.
|
||
#:
|
||
#: ⚠ **엑셀의 자름·반올림을 그대로 옮긴다** — 정본이 줄마다 `INT(x*100)/100` 으로 자르고
|
||
#: **다음 줄이 그 잘린 값을 받는다**(막자갈·잔토가 그렇다). 안 자르면 정본과 갈린다.
|
||
#:
|
||
#: ⚠ **방수로 파형강관을 늘 뺀다** — 정본이 상수 `(0.4 × 0.4) × 3.14` 로 박아 두었고
|
||
#: (Ø0.8 파형강관, 울진 관급 목록에 있는 규격) 방수로 치수가 0 인 개소에서도 뺀다.
|
||
#: **「방수로 없음」을 고를 칸이 등록부에 없어** 그대로 따른다 — 그 사실을 줄로 낸다.
|
||
EROSION_CHECK_DAM = {
|
||
"spillway_pipe_r_m": 0.4, # 정본 상수 — 0.4 × 0.4 × 3.14
|
||
"spillway_pipe_pi": 3.14,
|
||
"thickness_top_per_m": 0.1, # 상부 두께 = ℓ3 + 0.1H
|
||
"thickness_bottom_per_m": 0.4, # 하부 두께 = ℓ3 + 0.4H
|
||
"rubble_body_ratio": 2 / 3, # 막자갈에서 뺄 몸통 몫
|
||
"trench_extra_m": 0.3, # 바닥파기 폭 = 평균두께 + 0.3
|
||
"trench_depth_m": 0.5, # 바닥파기 깊이
|
||
"default_slope_ratio": 0.3, # 반수면 비탈 1:0.3 — 정본 붙박이
|
||
}
|
||
|
||
#: 골막이가 세우는 형식. 정본 탭이 **찰쌓기 하나**뿐이다.
|
||
EROSION_CHECK_FORMS: frozenset[str] = frozenset({"돌"})
|
||
|
||
#: 나머지 형식과 **왜 없는지**. 지어내지 않는다.
|
||
EROSION_CHECK_WITHHELD: dict[str, str] = {
|
||
"돌망태": "돌망태는 품셈 13-8 이고 규격 축이 망태 치수라 돌쌓기 표를 못 씀 — 원단위 미확보",
|
||
"콘크리트": "콘크리트 골막이는 몸체 두께·저판이 정본에 없어 물량이 서지 않음",
|
||
"통나무": (
|
||
"통나무 골막이는 품셈 13-13(목재틀)이고 밑수가 ㎥당 목공 품이라 축이 다름 — 원단위 미확보"
|
||
),
|
||
"바자": "바자얽기(품셈 5-15)는 별도 일위대가를 만들어 잇기로 확정(2026-09-09 ⑧-4)",
|
||
"흙": "흙골막이는 정본에 탭이 없고 품셈 장도 다름 — 원단위 미확보",
|
||
}
|
||
|
||
|
||
# ── 엑셀 자름·반올림 ────────────────────────────────────────────────────────
|
||
# ⚠⚠ **걷어 내지 말 것.** 「자릿수 맞추기」로 보여 지우고 싶어지는 자리인데, 정본이
|
||
# **줄마다 자르고 다음 줄이 그 잘린 값을 받는다** — 표시용이 아니라 **계산의 일부**다.
|
||
# 입적 = INT(정면적 × 평균두께) ← 평균두께가 이미 잘린 값(1.09)
|
||
# 막자갈 = INT(입적 − (…)) ← 입적이 이미 잘린 값(20.56)
|
||
# 잔토 = ROUND(바닥파기 − (…)) ← 바닥파기가 이미 잘린 값(8.61)
|
||
# 안 자르면 막자갈이 10.55, 잔토가 2.75 로 나와 **정본과 갈린다.**
|
||
# 「정본과 같은 값」의 뜻을 **소수점까지**로 잡은 것이고, 거울 시험이 그것을 잡는다.
|
||
|
||
|
||
def _floor2(value: float) -> float:
|
||
"""엑셀 `INT(x*100)/100` · `ROUNDDOWN(x,2)`."""
|
||
return math.floor(value * 100) / 100
|
||
|
||
|
||
def _round2(value: float) -> float:
|
||
"""엑셀 `ROUND(x,2)`.
|
||
|
||
⚠ 파이썬 `round` 는 **짝수 반올림**이라 4.905 를 4.90 으로 내린다(엑셀은 4.91).
|
||
물구멍관이 정확히 그 값이라 바꾸면 정본과 갈린다.
|
||
"""
|
||
return float(Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
|
||
|
||
|
||
def _ceil2(value: float) -> float:
|
||
"""엑셀 `ROUNDUP(x,2)` — 야면석만 이쪽이다(정본이 그 줄만 올림)."""
|
||
return math.ceil(value * 100) / 100
|
||
|
||
|
||
def erosion_check_dam(
|
||
height_m: float, options: dict[str, Any]
|
||
) -> tuple[list[Component], list[str]]:
|
||
"""돌골막이 1개소 전개 — 정본 「골막이(찰)(치수조서연결)」의 식을 그대로 돌린다."""
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import (
|
||
DESTINATION,
|
||
STONE_BACK_LENGTH_TABLE,
|
||
STONE_MASONRY,
|
||
_back_length,
|
||
fill_concrete_mpa,
|
||
)
|
||
|
||
form = str(options.get("form") or "").strip()
|
||
if form not in EROSION_CHECK_FORMS:
|
||
why = EROSION_CHECK_WITHHELD.get(form)
|
||
if why:
|
||
return [], [f"골막이 형식 「{form}」 — {why}"]
|
||
return [], [
|
||
f"골막이 형식이 정해지지 않았습니다(지금 「{form or '빈 값'}」) "
|
||
"— 형식을 골라야 물량이 섭니다"
|
||
]
|
||
|
||
top_m = _num(options.get("top_length_m"), 0.0)
|
||
bottom_m = _num(options.get("bottom_length_m"), 0.0)
|
||
if top_m <= 0 or bottom_m <= 0 or height_m <= 0:
|
||
return [], [
|
||
"골막이 정면적이 **사다리꼴**이라 상장ⓐ·하장ⓑ·높이가 다 있어야 셈이 섭니다 "
|
||
f"(지금 상장 {top_m:g}m · 하장 {bottom_m:g}m · 높이 {height_m:g}m)"
|
||
]
|
||
|
||
const = EROSION_CHECK_DAM
|
||
notes: list[str] = []
|
||
|
||
back_cm = _back_length(options)
|
||
coeff = STONE_BACK_LENGTH_TABLE.get(back_cm)
|
||
if coeff is None:
|
||
return [], [f"뒷길이 {back_cm}㎝ 는 품셈 표(25·30·35·45·55·60·75㎝)에 없어 계수가 없습니다"]
|
||
if not any(options.get(key) is not None for key in ("back_len_cm", "stone_back_length_cm")):
|
||
notes.append(
|
||
f"⚠ 골막이 제원에 **뒷길이 칸이 없어** 기본 {back_cm}㎝ 로 섰습니다 "
|
||
"— 그 값이 두께식·고임돌·야면석·채움콘크리트 계수를 모두 가릅니다"
|
||
)
|
||
back_m = back_cm / 100.0
|
||
|
||
given = _num(options.get("face_slope_ratio"), 0.0)
|
||
if given > 0:
|
||
slope, slope_note = given, f"사용자 지정 1:{given:g}"
|
||
else:
|
||
slope = const["default_slope_ratio"]
|
||
slope_note = f"1:{slope:g} — 정본 「반수면비탈」 붙박이(안 정함)"
|
||
|
||
# ① 정면적(사다리꼴) − ② 방수로 파형강관 단면
|
||
trapezoid = _floor2((top_m + bottom_m) / 2 * height_m)
|
||
pipe = const["spillway_pipe_r_m"] ** 2 * const["spillway_pipe_pi"]
|
||
front_area = trapezoid - pipe
|
||
notes.append(
|
||
f"방수로 파형강관 {pipe:.4f}㎡ 를 정면적에서 뺐습니다 — 정본 붙박이(Ø0.8) "
|
||
"⚠ 「방수로 없음」을 고를 칸이 등록부에 없어 늘 뺍니다"
|
||
)
|
||
|
||
masonry = _floor2(front_area * _round2(math.hypot(slope, 1.0)))
|
||
top_t = _floor2(back_m + const["thickness_top_per_m"] * height_m)
|
||
thickness = _floor2(
|
||
(
|
||
(back_m + const["thickness_top_per_m"] * height_m)
|
||
+ (back_m + const["thickness_bottom_per_m"] * height_m)
|
||
)
|
||
/ 2
|
||
)
|
||
volume = _floor2(front_area * thickness)
|
||
facing = top_m * (top_t - back_m)
|
||
base_area = masonry + facing
|
||
|
||
rows: list[tuple[str, str, float, str]] = [
|
||
("돌쌓기", "㎡", masonry, f"정면적 {front_area:.4f}㎡ × √(1+n²) · 기울기 {slope_note}"),
|
||
("돌붙임", "㎡", facing, f"상장 {top_m:g}m × (상부두께 {top_t:g} − 뒷길이 {back_m:g})"),
|
||
(
|
||
"입적",
|
||
"㎥",
|
||
volume,
|
||
f"정면적 × 평균두께 {thickness:g}m "
|
||
f"[{{(ℓ3+0.1H)+(ℓ3+0.4H)}}÷2 · ℓ3={back_m:g} · H={height_m:g}]",
|
||
),
|
||
]
|
||
|
||
stone_ton = coeff["stone_ton_per_m2"]
|
||
if stone_ton is None:
|
||
notes.append(f"뒷길이 {back_cm}㎝ 에 야면석 중량 칸이 비어 있어 돌을 못 세웠습니다")
|
||
else:
|
||
rows.append(
|
||
(
|
||
"야면석",
|
||
"ton",
|
||
_ceil2(base_area * stone_ton),
|
||
f"(돌쌓기+돌붙임) {base_area:.4f}㎡ × {stone_ton} ton/㎡ (뒷길이 {back_cm}㎝)",
|
||
)
|
||
)
|
||
wedge_per = coeff["wedge_stone_m3_per_m2"]
|
||
wedge = _round2(base_area * _num(wedge_per)) if wedge_per is not None else 0.0
|
||
if wedge_per is None:
|
||
notes.append(f"뒷길이 {back_cm}㎝ 에 고임돌 칸이 비어 있습니다 — 막자갈에서 안 뺐습니다")
|
||
else:
|
||
rows.append(("고임돌", "㎥", wedge, f"(돌쌓기+돌붙임) × {wedge_per} ㎥/㎡"))
|
||
|
||
rows.append(
|
||
(
|
||
"막자갈",
|
||
"㎥",
|
||
_floor2(volume - (base_area * back_m * const["rubble_body_ratio"] + wedge)),
|
||
"입적 − ((돌쌓기+돌붙임) × 뒷길이 × 2/3 + 고임돌)",
|
||
)
|
||
)
|
||
rows.append(
|
||
(
|
||
"물구멍관",
|
||
"m",
|
||
_round2(
|
||
masonry / STONE_MASONRY["weep_hole_area_m2"] * STONE_MASONRY["weep_hole_length_m"]
|
||
),
|
||
f"돌쌓기 ÷ {STONE_MASONRY['weep_hole_area_m2']:g}㎡/개소 × "
|
||
f"{STONE_MASONRY['weep_hole_length_m']:g}m/개소",
|
||
)
|
||
)
|
||
|
||
# 바닥파기 — 비탈 사면장 두 쪽 + 하장 한 쪽, 폭은 평균두께 + 0.3.
|
||
slant = _floor2(math.hypot((top_m - bottom_m) / 2, height_m))
|
||
width = thickness + const["trench_extra_m"]
|
||
depth = const["trench_depth_m"]
|
||
trench = _floor2(slant * depth * width * 2 + bottom_m * depth * width)
|
||
spoil = _round2(trench - (slant * depth * width + bottom_m * depth * width))
|
||
rows.extend(
|
||
[
|
||
(
|
||
"터파기",
|
||
"㎥",
|
||
trench,
|
||
f"(사면장 {slant:g} × {depth:g} × {width:g}) × 2 + (하장 {bottom_m:g} × "
|
||
f"{depth:g} × {width:g}) · 정본 이름 「바닥파기」",
|
||
),
|
||
("잔토처리", "㎥", spoil, f"바닥파기 − (사면장 + 하장) × {depth:g} × {width:g}"),
|
||
("되메우기", "㎥", trench - spoil, "바닥파기 − 잔토"),
|
||
]
|
||
)
|
||
|
||
components = [
|
||
Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis)
|
||
for name, unit, amount, basis in rows
|
||
]
|
||
mpa, mpa_basis = fill_concrete_mpa(options)
|
||
fill_per = coeff["fill_concrete_m3_per_m2"]
|
||
components.append(
|
||
Component(
|
||
"채움콘크리트",
|
||
"㎥",
|
||
_round2(base_area * _num(fill_per)),
|
||
DESTINATION["채움콘크리트"],
|
||
f"(돌쌓기+돌붙임) × {fill_per} ㎥/㎡ · {mpa_basis}",
|
||
spec=f"{mpa}",
|
||
)
|
||
)
|
||
if not options.get("stone_kind"):
|
||
notes.append(
|
||
"돌 종류 칸이 골막이 등록부에 없어 **야면석**으로 섰습니다 — 정본 탭이 그 돌입니다"
|
||
)
|
||
return components, notes
|
||
|
||
|
||
#: 개거(겉도랑) 원단위 — 소광리 정본 「개거(150-200) (2)」·「L형수로-(201)」, **둘 다 m당**.
|
||
#: ⚠⚠ **두 표가 담는 것이 다르다.** 값이 비슷해 보여 합치고 싶어지는 자리다:
|
||
#:
|
||
#: 콘크리트 개거 150×200 터파기 · 유로폼 · 면목 **세 줄뿐** — **콘크리트 본체 줄이 없다**
|
||
#: 콘크리트 L형수로 H=0.2 터파기 · 되메우기 · 잔토 · **콘크리트** · PVC Φ50 · 이형철근 D13 ·
|
||
#: 거푸집 · 면목 **여덟 줄**
|
||
#:
|
||
#: ⓘ 같은 것 — **둘 다 m당**이고 거푸집 계열(유로폼·거푸집)과 면목이 있다.
|
||
#: ⚠ 개거 표에 콘크리트가 없는 것은 **빠뜨린 것이 아니라 원문 그대로**다. 0 으로 때우거나
|
||
#: L형수로 값을 옮겨 채우지 않는다(2026-09-09 판단 — 돌붙임(메)에서 세 줄을 안 만든 그 자리).
|
||
#:
|
||
#: ⓘ **「다른 탭에 본체가 있나」를 찾아봤고 없었다**(2026-09-09 53탭 전수):
|
||
#: · 「개거」 글자가 나오는 탭은 **둘뿐** — 이 탭과 「콘크리트개거」.
|
||
#: 뒤엣것은 **제목만 「콘크리트개거 (물넘이형)」이고 내용이 국가지점번호판 표**다.
|
||
#: · 탭 이름의 「(2)」는 **짝 번호가 아니라 엑셀 사본 표식**이다 — `Φ1000` ↔ `Φ1000(2)`,
|
||
#: `떼수로(윤주1.08)` ↔ `…(2)` 가 **내용까지 똑같다**(대조 확인). 「(1)」은 없다.
|
||
#: · 같은 시트의 「측구수로400/500」도 제목이 **「터파기 계산서」**이고 본체가 없다 —
|
||
#: **한 구조물을 부분 계산서로 쪼개는 버릇**이 있는 시트다.
|
||
#: ⇒ 그러므로 이 표는 **부분 계산서일 가능성이 남아 있다.** 다만 이 원본 안에서는
|
||
#: 본체 표를 못 찾았으므로 **「원문에 없음」으로 두고 그 사실을 화면에 적는다** —
|
||
#: 「콘크리트 개거인데 콘크리트가 0」이 우리 결함으로 읽히지 않게.
|
||
#:
|
||
#: ⚠ **규격이 붙박이다** — 정본에 「150×200」 하나뿐이고 `open_ditch` 등록부에 **옵션이 하나도
|
||
#: 없다**(규격 칸 없음). 칸이 생기면 이 표를 열면 된다.
|
||
OPEN_DITCH_FORMS: dict[str, dict[str, Any]] = {
|
||
"콘크리트 개거 150×200": {
|
||
"rows": (
|
||
("터파기", "㎥", 0.3 * 0.1 * 1.0, "0.3 × 0.1 × 1"),
|
||
("유로폼", "㎡", (0.2 + 0.2 + 0.15) * 1.0, "(0.2 + 0.2 + 0.15) × 1"),
|
||
("면목", "m", 2.0 * 1.0, "2 × 1"),
|
||
),
|
||
},
|
||
"콘크리트 L형수로 H=0.2": {
|
||
"rows": (
|
||
("터파기", "㎥", 0.31645, "[{(1.11+1.51)÷2×0.2} + {(0.33×0.33)÷2}] × 1.0"),
|
||
("되메우기", "㎥", 0.129975, "(0.2+0.30)÷2×0.25×1 + (0.2+0.19)÷2×0.33×1.0"),
|
||
("잔토처리", "㎥", 0.186475, "터파기 − 되메우기"),
|
||
(
|
||
"콘크리트",
|
||
"㎥",
|
||
0.1508,
|
||
"{(0.2+0.15)÷2×0.7} + (0.21×0.13)×1 + {(0.15+0.21)÷2×0.20}×1",
|
||
),
|
||
("이형철근 D13", "kg", 0.398, "0.2 × 2 × 0.995"),
|
||
("거푸집", "㎡", 0.738, "0.2 + 0.33 + √(0.2² + 0.06²)"),
|
||
("면목", "m", 1.0, "1"),
|
||
),
|
||
# ⚠ 정본에 줄은 있으나 **수량 칸이 비어 있다**(PVC Φ50). 0 으로 만들지 않고 알린다.
|
||
"blank_rows": (("PVC 파이프 Φ50㎜", "m"),),
|
||
},
|
||
}
|
||
|
||
|
||
#: 떼흙막이 원단위 — 소광리 정본 「떼흙막이」, **개소당**. 치수는 전부 **평균 붙박이**다
|
||
#: (상단 1.5 · 하단 1.1 · 높이 0.5 · 두께 0.2 · 기슭 0.54 — 원문에 「(평균)」이라 적혀 있다).
|
||
#: ✅ **형식 칸에 「떼」가 들어와 이 표가 선다**(2026-09-09 랩탑 메인 `513aff3e`).
|
||
#: ⚠ 나머지 일곱 형식은 **정본에 탭이 없다.** 돌(찰)/(메)에 기슭막이 식을 빌려 쓰지 않는다 —
|
||
#: 흙막이는 교본상 앞면 1:0.3·뒷면 수직·천단 30㎝ 라 축이 다르고, 무엇보다 **근거가 없다.**
|
||
#: ⚠⚠ **사용자 확정 4차**(2026-09-09) — 「흙막이는 횡단도에서 표현방식·옵션이 기슭막이와
|
||
#: 동일, 형상도 동일. 다만 **데이터는 분리하여 계산**」. ⇒ **그림·옵션은 한 벌로 쓰되
|
||
#: 수량은 여기서 제 것으로 낸다.** 그림이 같다고 기슭막이 원단위를 끌어오면 그 확정을 어긴다.
|
||
SOIL_GUARD_SOD: dict[str, Any] = {
|
||
"unit_label": "개소당",
|
||
"rows": (
|
||
(
|
||
"떼",
|
||
"㎡",
|
||
1.39,
|
||
"머리떼 1.5×0.2 + 바닥떼 1.1×0.2 + 정면떼 (1.5+1.1)÷2×0.5 "
|
||
"+ 양기슭바닥떼 0.54×0.2×2 · 규격 20×20㎝",
|
||
),
|
||
# ⚠ 이름을 **터파기**로 낸다 — 정본 이름은 「바닥파기」이나 그대로 두면 토공 축에
|
||
# 안 실린다(`DESTINATION` 이 이름으로 가른다). 골막이도 같은 자리를 그렇게 냈다.
|
||
("터파기", "㎥", 0.17, "(0.54×0.4×0.2×2) + (1.1×0.4×0.2) · 정본 이름 「바닥파기」"),
|
||
),
|
||
}
|
||
|
||
|
||
def soil_guard(options: dict[str, Any]) -> tuple[list[Component], list[str]]:
|
||
"""흙막이 전개 — **「떼」만 선다**(정본 「떼흙막이」, 개소당). 나머지는 사유만.
|
||
|
||
⚠ 치수가 **전부 평균 붙박이**다 — 정본 표에 「(평균)」이라 적혀 있고 개소별 치수조서가
|
||
따로 없다. 그래서 높이·길이를 받지 않는다. 개소별로 갈리게 하려면 치수 칸이 먼저다.
|
||
"""
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import DESTINATION
|
||
|
||
form = str(options.get("form") or "").strip()
|
||
if not form:
|
||
return [], ["흙막이 형식이 정해지지 않았습니다 — 형식을 골라야 물량이 섭니다"]
|
||
if form != "떼":
|
||
return [], [
|
||
f"흙막이 형식 「{form}」 — 정본(소광리 53탭)에 그 형식의 산출식이 없습니다"
|
||
"(있는 것은 **떼흙막이** 하나). ⚠ 돌(찰)·돌(메)에 기슭막이 식을 빌려 쓰지 않습니다 "
|
||
"— 흙막이는 앞면 1:0.3·뒷면 수직·천단 30㎝(교본)라 축이 다르고 근거가 없습니다"
|
||
]
|
||
|
||
components = [
|
||
Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis)
|
||
for name, unit, amount, basis in SOIL_GUARD_SOD["rows"]
|
||
]
|
||
return components, [
|
||
f"떼흙막이는 **{SOIL_GUARD_SOD['unit_label']}**이고 치수가 정본 평균 붙박이입니다"
|
||
"(상단 1.5 · 하단 1.1 · 높이 0.5 · 두께 0.2 · 기슭 0.54m) — 개소 제원을 안 봅니다"
|
||
]
|
||
|
||
|
||
def open_ditch(length_m: float, options: dict[str, Any]) -> tuple[list[Component], list[str]]:
|
||
"""개거(겉도랑) 전개 — **m당** 원단위를 연장에 곱한다(소광리 정본 두 탭).
|
||
|
||
⚠ 규격을 고를 칸이 없어 지금은 **「콘크리트 개거 150×200」 붙박이**다. 그 사실을 줄로 낸다.
|
||
"""
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import DESTINATION
|
||
|
||
if length_m <= 0:
|
||
return [], ["연장이 없어 전개하지 않음"]
|
||
spec = str(options.get("ditch_spec") or "콘크리트 개거 150×200").strip()
|
||
table = OPEN_DITCH_FORMS.get(spec)
|
||
if table is None:
|
||
return [], [
|
||
f"개거 규격 「{spec}」 원단위가 없습니다 — 있는 것: {' · '.join(OPEN_DITCH_FORMS)}"
|
||
]
|
||
|
||
components = [
|
||
Component(
|
||
name,
|
||
unit,
|
||
per_m * length_m,
|
||
DESTINATION.get(name, "quantity"),
|
||
f"{basis} × 연장 {length_m:g}m (m당 {per_m:g})",
|
||
)
|
||
for name, unit, per_m, basis in table["rows"]
|
||
]
|
||
notes: list[str] = []
|
||
if "ditch_spec" not in options:
|
||
notes.append(f"규격이 「{spec}」 붙박이입니다 — 개거 제원에 규격 칸이 없습니다")
|
||
if spec == "콘크리트 개거 150×200":
|
||
notes.append(
|
||
"⚠ 이 표에는 **콘크리트 본체 줄이 없습니다** — 정본 원문 그대로입니다"
|
||
"(터파기·유로폼·면목 세 줄뿐). 같은 원본 53탭에서 본체 표를 찾았으나 없었고"
|
||
"(「콘크리트개거」 탭은 제목만 그 이름이고 내용이 다른 표), 이 시트가"
|
||
"「측구수로 터파기 계산서」처럼 **부분 계산서를 쓰는 버릇**이 있어"
|
||
"**다른 문서에 본체가 있을 수 있습니다.** 지어내지 않았습니다"
|
||
)
|
||
for name, unit in table.get("blank_rows", ()):
|
||
notes.append(f"{name}({unit}) 은 정본에 줄은 있으나 **수량이 비어 있어** 안 세웠습니다")
|
||
return components, notes
|
||
|
||
|
||
def bed_sill(area_m2: float, options: dict[str, Any]) -> tuple[list[Component], list[str]]:
|
||
"""바닥막이 전개 — **돌붙임 ㎡당** 원단위를 면적에 곱한다(소광리 정본 두 탭).
|
||
|
||
⚠ 밑수가 **면적**이다. 돌쌓기처럼 높이·연장으로 셈하지 않는다 — 정본 표 제목이 「㎡당」이고
|
||
등록부 밑수도 `area_m2` 라 축이 그대로 맞는다.
|
||
⚠ 기울기 몫이 **없다** — 평면에 붙이는 것이라 √(1+n²) 를 곱하지 않는다.
|
||
"""
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import DESTINATION
|
||
|
||
form = str(options.get("form") or "").strip()
|
||
table = BED_SILL_FORMS.get(form)
|
||
if table is None:
|
||
return [], [
|
||
f"바닥막이 형태 「{form or '빈 값'}」 — 돌붙임(찰)·돌붙임(메)만 원단위가 있습니다"
|
||
]
|
||
if area_m2 <= 0:
|
||
return [], ["면적이 없어 전개하지 않음"]
|
||
|
||
back = table["back_len_m"]
|
||
rows: list[tuple[str, str, float, str]] = [
|
||
("돌붙임", "㎡", area_m2, f"면적 {area_m2:g}㎡ (평면적 — 기울기 몫 없음)"),
|
||
("입적", "㎥", area_m2 * back, f"면적 × 두께 {back:g}m"),
|
||
(
|
||
table["stone_name"],
|
||
"ton",
|
||
area_m2 * table["stone_ton_per_m2"],
|
||
f"면적 × {table['stone_ton_per_m2']:.3f} ton/㎡ · {table['stone_spec']}"
|
||
+ (
|
||
f" (= 뒷길이 {back:g} × 0.77 × 2.65)"
|
||
if form == "돌붙임(찰)"
|
||
else " (원문 직접값 — 야면석은 유도식이 안 맞음)"
|
||
),
|
||
),
|
||
(
|
||
"고임돌",
|
||
"㎥",
|
||
area_m2 * table["wedge_stone_m3_per_m2"],
|
||
f"면적 × {table['wedge_stone_m3_per_m2']} ㎥/㎡",
|
||
),
|
||
]
|
||
for name, key, unit in (
|
||
("모르터", "mortar_m3_per_m2", "㎥"),
|
||
("채움콘크리트", "fill_concrete_m3_per_m2", "㎥"),
|
||
("버림콘크리트", "blinding_m3_per_m2", "㎥"),
|
||
):
|
||
value = table[key]
|
||
if value is not None:
|
||
rows.append((name, unit, area_m2 * value, f"면적 × {value} ㎥/㎡"))
|
||
# 터파기 — 정본은 「두께 × 1 × 1」이라 **면적 × 두께**와 같다.
|
||
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
|
||
]
|
||
notes = [f"뒷길이 {back:g}m 는 정본 탭 붙박이 값입니다 — 바닥막이 제원에 뒷길이 칸이 없습니다"]
|
||
if form == "돌붙임(찰)":
|
||
notes.append(
|
||
"버림 콘크리트 0.1 ㎥/㎡ 포함 — 정본 주기 「바닥 10㎝ 이상 콘크리트(버림) 포설 후 "
|
||
"돌붙임」(KDS 44 90 00 의 100㎜ 와 같음)"
|
||
)
|
||
return components, notes
|