feat(B08): 돌쌓기 기울기를 품셈 표준경사표로 자동 판정 (사용자 확정 ⑨)
⚠ 확정 ⑨ 가 통째로 미구현이었음 — 레지스트리에 `face_slope_ratio` 칸이 없어 **늘 1:0.3** 으로 돌았고, 찰 H=2.5 와 메 H=2.0 이 같은 기울기로 나왔음. 우리 0.3 은 실은 **「메쌓기·성토·직고 1.5m 이하」 한 칸**이었음. 품셈 13-4-4 [주]⑪ <표준경사>를 데이터로 두고 **직고 · 메/찰 · 성토/절토** 셋으로 고름. 메 성토 0.30 0.35 0.40 0.45 0.50 · 메 절토 0.25 0.30 0.35 0.40 0.45 찰 성토 0.25 0.30 0.35 0.40 0.45 · 찰 절토 0.20 0.25 0.30 0.35 0.40 ⚠ 경계값은 **앞 칸** — 원문 표기가 `∼1.5 · ∼3` 이라 이하로 읽음(직고 3.0m → `~3` 칸). 7.0m 은 `~7`, 그보다 커야 마지막 칸. 해석을 데이터·주석·시험에 함께 박음. ⚠ **성토/절토가 저장에 아직 없음** — 레지스트리에 없는 키를 읽지 않고 **성토 열로 잠정** 적용하며 **근거 문구에 그 사실을 적음**. 값이 생기면 한 줄만 바꾸면 됨. ⚠ 저장 제원에 값이 있으면 **그 값이 이김**(확정 ⑨). 자동으로 고른 값은 **저장에 안 박음** — 「정한 적 없음」과 「0.3 으로 정함」이 구별돼야 함. ⚠ 큰돌쌓기는 이 표 대상이 아님 — 교본이 「1:0.3 **이상**」으로만 둠. 그대로 둠. 검증 프로젝트 실측 — 돌쌓기(메) H=2.0 면적 **20.881 → 21.190㎡(+1.5%)**, 찰 H=2.5 는 1:0.3 그대로. 옛 시험 셋이 「늘 1:0.3」을 계약으로 못 박고 있어 기울기를 명시하도록 정정. 시험 761 통과(새 7건 포함). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -258,6 +258,67 @@ class StructureQuantity:
|
||||
billing_quantity: float = 0.0
|
||||
|
||||
|
||||
#: 표준경사표 — 품셈 13-4-4 [주]⑪. 파일이 없으면 종전 기본값(0.3)으로 돈다.
|
||||
SLOPE_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_masonry"
|
||||
SLOPE_PREFIX = "masonry_slope_"
|
||||
#: 사용자가 덮어쓰는 칸 — 값이 있으면 **그 값이 이긴다**(확정 ⑨).
|
||||
FACE_SLOPE_KEYS = ("face_slope_ratio",)
|
||||
#: 성토/절토를 아직 저장이 안 갖고 있다 — 어느 칸을 잠정으로 쓰는지 한 곳에 둔다.
|
||||
DEFAULT_SLOPE_FACE = "성토"
|
||||
LEGACY_FACE_SLOPE_RATIO = 0.3
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_slope_table(path: str | None = None) -> dict[str, Any]:
|
||||
"""표준경사표. 없으면 빈 표 — 그때는 종전 기본값으로 돈다(값을 지어내지 않는다)."""
|
||||
target = Path(path) if path else None
|
||||
if target is None:
|
||||
files = sorted(SLOPE_DIR.glob(SLOPE_PREFIX + "*.json")) if SLOPE_DIR.is_dir() else []
|
||||
target = files[-1] if files else None
|
||||
if target is None or not target.is_file():
|
||||
return {}
|
||||
return json.loads(target.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def face_slope_ratio(
|
||||
options: dict[str, Any], *, wet: bool, height_m: float, table: dict[str, Any] | None = None
|
||||
) -> tuple[float, str]:
|
||||
"""(기울기 1:n 의 n, 근거 문구).
|
||||
|
||||
⚠ **사용자가 정했으면 그 값이 이긴다**(확정 ⑨). 안 정했으면 품셈 13-4-4 [주]⑪
|
||||
<표준경사>에서 **직고 · 메/찰 · 성토/절토** 셋으로 고른다.
|
||||
⚠ 자동으로 고른 값을 **저장에 박지 않는다** — 「정한 적 없음」과 「0.3 으로 정함」이
|
||||
구별돼야 한다(오늘 패널 접힘에서 지킨 것과 같은 자리).
|
||||
⚠ 경계값은 **앞 칸**이다 — 원문이 `∼3` 이라 직고 3.0m 은 `~3` 칸이다.
|
||||
"""
|
||||
for key in FACE_SLOPE_KEYS:
|
||||
given = _num(options.get(key), 0.0)
|
||||
if options.get(key) is not None and given > 0:
|
||||
return given, f"사용자 지정 1:{given:g}"
|
||||
|
||||
found = table if table is not None else load_slope_table()
|
||||
steps = found.get("steps_m") or []
|
||||
bond = "찰쌓기" if wet else "메쌓기"
|
||||
# ⚠ 성토/절토는 **저장이 아직 안 갖고 있다.** 레지스트리에 없는 키를 읽지 않는다
|
||||
# (읽으면 「없는 칸을 읽는」 자리가 되어 옵션 키 검사가 잡는다). 값이 생기면
|
||||
# 여기 한 줄만 바꾸면 된다 — 그때까지 성토 열로 잠정 적용하고 근거에 적는다.
|
||||
face = DEFAULT_SLOPE_FACE
|
||||
row = ((found.get("table") or {}).get(bond) or {}).get(face)
|
||||
if not row:
|
||||
return LEGACY_FACE_SLOPE_RATIO, f"표준경사표를 못 읽어 종전값 1:{LEGACY_FACE_SLOPE_RATIO}"
|
||||
|
||||
index = len(steps)
|
||||
for i, limit in enumerate(steps):
|
||||
if height_m <= float(limit):
|
||||
index = i
|
||||
break
|
||||
ratio = float(row[min(index, len(row) - 1)])
|
||||
label = f"직고 {height_m:g}m" + (f" ≤{steps[index]:g}m" if index < len(steps) else " 7m 초과")
|
||||
note = f"품셈 13-4-4 [주]⑪ 표준경사 · {bond} {face} · {label} → 1:{ratio:g}"
|
||||
note += " · ⚠ 성토/절토가 저장에 없어 **성토 열로 잠정**"
|
||||
return ratio, note
|
||||
|
||||
|
||||
def is_collected_stone(options: dict[str, Any]) -> bool:
|
||||
"""이 구조물의 돌을 **캐서 쓰는가**. 정한 적이 없으면 「캔다」(확정 ②)."""
|
||||
for key in STONE_SUPPLY_KEYS:
|
||||
@@ -383,7 +444,9 @@ def boulder_masonry(
|
||||
f"{' · '.join(BOULDER_DIAMETERS)}㎝ 중 하나를 고르면 값이 섭니다"
|
||||
]
|
||||
|
||||
slope_ratio = _num(options.get("face_slope_ratio"), 0.3) # 레지스트리에 칸 없음 — 기본 0.3
|
||||
# ⚠ 큰돌쌓기는 **표준경사표 대상이 아니다** — 교본이 「1:0.3 **이상**」으로만 두었다.
|
||||
# 저장 제원에 값이 있으면 그것을 쓰고, 없으면 하한 0.3 으로 둔다.
|
||||
slope_ratio = _num(options.get("face_slope_ratio"), 0.3)
|
||||
constants = BOULDER_MASONRY
|
||||
face_area = height_m * length_m
|
||||
# 기울기 몫은 **한 번만** — 13-4 와 같은 자리다(㉘).
|
||||
@@ -492,7 +555,8 @@ def stone_masonry(
|
||||
# **기본 0.3 의 근거** — 교본 7-3 돌흙막이 기준: 「돌 찰쌓기 3.0m 이하 **1:0.3** /
|
||||
# 돌 메쌓기 2.0m 이하 **1:0.3** / 큰돌쌓기 **1:0.3 이상**(전도 방지)」
|
||||
# (`지식DB 02_상세설계/구조물/돌쌓기.md §1`, 값은 `data_masonry` 의 `face_slope`).
|
||||
slope_ratio = _num(options.get("face_slope_ratio"), 0.3) # 전면 기울기 1:0.3 (교본 7-3)
|
||||
# 전면 기울기 — 품셈 표준경사표로 자동 판정하고, 저장 제원에 값이 있으면 그것이 이긴다.
|
||||
slope_ratio, slope_basis = face_slope_ratio(options, wet=wet, height_m=height_m)
|
||||
constants = STONE_MASONRY
|
||||
|
||||
face_area = height_m * length_m # 정면적
|
||||
@@ -511,7 +575,7 @@ def stone_masonry(
|
||||
"㎡",
|
||||
masonry_area,
|
||||
DESTINATION["돌쌓기"],
|
||||
f"정면적 × √(1+{slope_ratio}²) — 비탈면적",
|
||||
f"정면적 × √(1+{slope_ratio:g}²) — 비탈면적 · {slope_basis}",
|
||||
),
|
||||
]
|
||||
# ⚠ 고임돌 계수가 **원문에서 「-」**인 칸이 있다(견치돌 25·30 · 야면석 75 · 깬돌 25).
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "masonry_standard_slope",
|
||||
"effective_date": "2026-01-01",
|
||||
"note": "돌쌓기 표준경사 — **직고 · 메/찰 · 성토/절토** 셋으로 갈린다. 우리는 늘 1:0.3 한 값으로 돌고 있었는데, 그것은 이 표의 「메쌓기·성토·직고 1.5m 이하」 한 칸일 뿐이었다(2026-09-09).",
|
||||
"source": {
|
||||
"doc": "산림사업 표준품셈(산림청고시 제2025-82호) 13-4-4 [주]⑪ <표 준 경 사>",
|
||||
"line": "원문 L7182~7194",
|
||||
"quote": "돌쌓기의 표준경사는 특수한 경우를 제외하고는 쌓기 높이의 최대치를 취하고 표준 뒷길이 상부에서부터 적용한다"
|
||||
},
|
||||
"policy": {
|
||||
"user_override": "저장 제원에 `face_slope_ratio` 가 있으면 **그 값이 이긴다**(사용자 확정 ⑨ 「자동 판정 + 사용자가 바꿀 수 있게」).",
|
||||
"no_write_back": "⚠ 자동으로 고른 값을 **저장에 박지 않는다.** 「정한 적 없음」과 「0.3 으로 정함」이 구별돼야 한다.",
|
||||
"boundary": "⚠ 원문 표기는 `∼1.5 · ∼3 · ∼5 · ∼7 · 7이상` 이다. 「∼」를 **이하**로 읽어 경계값은 **앞 칸**에 넣는다(직고 3.0m → `~3` 칸). 7.0m 은 `~7` 칸이고 그보다 커야 `7이상` 칸이다.",
|
||||
"face_unknown": "⚠ 성토/절토를 **저장이 아직 안 갖고 있다.** 구조물 레지스트리의 설치 측 기본이 「자동(성토 쪽)」이고 사면 계열도 층따기가 성토 전용이라 **성토 열로 잠정 적용**하고, 근거 문구에 그 사실을 적는다 — 값이 들어오면 그 칸만 바꾸면 된다."
|
||||
},
|
||||
"steps_m": [1.5, 3.0, 5.0, 7.0],
|
||||
"table": {
|
||||
"메쌓기": {
|
||||
"성토": [0.3, 0.35, 0.4, 0.45, 0.5],
|
||||
"절토": [0.25, 0.3, 0.35, 0.4, 0.45]
|
||||
},
|
||||
"찰쌓기": {
|
||||
"성토": [0.25, 0.3, 0.35, 0.4, 0.45],
|
||||
"절토": [0.2, 0.25, 0.3, 0.35, 0.4]
|
||||
}
|
||||
},
|
||||
"not_here": {
|
||||
"note": "이 표가 정하지 않는 것.",
|
||||
"items": [
|
||||
"⚠ 교본 부록 4-5:752 는 **시설 종류**로 가른다 — 「돌 기슭막이의 돌쌓기 기울기는 찰쌓기 1:0.3, **메쌓기 1:0.5**」. 품셈은 직고·성절토 축이라 **축이 다르다.** 기슭막이로 놓은 벽은 사용자가 덮어쓰기로 처리한다(확정 ⑨).",
|
||||
"큰돌쌓기(13-6)는 이 표의 대상이 아니다 — 교본이 「1:0.3 이상」으로만 두었다."
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user