feat(b08): 구조물도 옹벽 단면 그림 — 관측 원단위 줄에 단면 치수 · 표 사유와 같은 말

- 반중력식 H=2.0 관측 줄에 단면(기초 1.6×0.4 · 벽체 1.6 상단 0.3 앞면 0.118 뒷면 0.032 · 전단키 0.35×0.3 · 버림 0.1 · 물빼기 관 0.4)을 실음 — 출처 소광리 07-구조도 「옹벽2.0」 산출식·일반도
- 그 치수로 표 수량이 다시 나오는지 시험(콘크리트 1.345→1.35 · 버림 0.145→0.15 · 유로폼 3.205→3.2 · 관 0.32)
- 그림: 기초·벽체·전단키 + 버림(키 자리 비움) + 기초잡석(산출 조건 두께 · 버림과 같은 폭) + 물구멍
- 표 사유에 새 한 줄 둘(그림도 같은 말): 관측 줄에 터파기·되메우기·잔토가 없음 · 물빼기 칸을 채워도 관측 값이 붙박이라 안 닿음
- 까닭은 표가 못 선 장만(형식 미정 · 자료에 없는 규격 · 단면 치수 없는 규격) — 표 사유와 같은 말

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 05:27:49 +09:00
co-authored by Claude Opus 5
parent bc1b2fdad0
commit 5e983018a3
5 changed files with 276 additions and 9 deletions
@@ -33,6 +33,14 @@
"unit": "m",
"source": "uljin_library",
"source_note": "§7 옹벽류 — 반중력식옹벽 H=2.0",
"section": {
"note": "단면 치수(m) — 위 수량이 나온 그 단면. 출처 소광리 07-구조도 「옹벽2.0」 탭 산출식(T22 기초 0.4×1.6+0.3×0.35 · T23 벽체 (0.3+0.45)÷2×1.6 · T25 버림 0.1×(1.8−0.35) · T26·T27 유로폼 √(1.6²+0.032²)+√(1.6²+0.118²) · T31 파이프 1.6÷2×0.4)과 같은 탭 일반도(앞굽 950·뒷굽 200·키 자리 뒷굽 끝에서 500). 원문 1.345·0.145·3.205 를 라이브러리가 1.35·0.15·3.2 로 적음",
"footing": { "width_m": 1.6, "thickness_m": 0.4, "toe_m": 0.95 },
"wall": { "height_m": 1.6, "top_m": 0.3, "front_batter_m": 0.118, "back_batter_m": 0.032 },
"key": { "width_m": 0.35, "depth_m": 0.3, "from_heel_m": 0.5 },
"blinding": { "thickness_m": 0.1, "overhang_m": 0.1 },
"weep": { "area_m2": 2.0, "diameter_mm": 50, "pipe_length_m": 0.4 }
},
"components": [
{
"name": "콘크리트",
@@ -5,6 +5,7 @@
from __future__ import annotations
import math
import sys
from pathlib import Path
@@ -12,9 +13,14 @@ ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import ( # noqa: E402
expand_observed,
load_observed_table,
)
from B08_Quantity.B08_Quantity_Engine_StructureFigure import ( # noqa: E402
attach_figures,
build_figure,
figure_reason,
)
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( # noqa: E402
back_length_default_note,
@@ -96,6 +102,69 @@ def test_기본값으로_그린_장은_표_사유와_같은_말을_적는다() -
assert back_length_default_note({"back_len_cm": 45}, 45) is None
def _wall_sheet(**options) -> dict:
return {
"title": "옹벽 H=2",
"type_id": "retaining_wall",
"height_m": 2.0,
"options": {"height_m": 2.0, "form": "반중력식", **options},
}
def test_옹벽_단면_치수가_표의_관측_수량을_그대로_낸다() -> None:
"""표 = 그림 — 그림이 쓰는 단면으로 표 수량(라이브러리 반올림)이 다시 나와야 함."""
entry = load_observed_table().find("retaining_wall", {"form": "반중력식", "height_m": 2.0})
section = entry["section"]
amounts = {item["name"]: item["amount"] for item in entry["components"]}
footing, wall, key, blinding = (section[k] for k in ("footing", "wall", "key", "blinding"))
bottom = wall["top_m"] + wall["front_batter_m"] + wall["back_batter_m"]
assert abs(bottom - 0.45) < 1e-9
concrete = (
footing["width_m"] * footing["thickness_m"]
+ key["width_m"] * key["depth_m"]
+ (wall["top_m"] + bottom) / 2 * wall["height_m"]
)
blind = blinding["thickness_m"] * (
footing["width_m"] + 2 * blinding["overhang_m"] - key["width_m"]
)
form = math.hypot(wall["height_m"], wall["back_batter_m"]) + math.hypot(
wall["height_m"], wall["front_batter_m"]
)
weep = section["weep"]
pipe = wall["height_m"] / weep["area_m2"] * weep["pipe_length_m"]
assert round(concrete, 2) == amounts["콘크리트"] # 1.345
assert round(blind, 2) == amounts["버림콘크리트"] # 0.145
assert round(form, 1) == amounts["유로폼"] # 3.205
assert round(pipe, 2) == amounts["물구멍관"]
assert footing["thickness_m"] + wall["height_m"] == 2.0
def test_옹벽_그림은_기초잡석_두께를_산출_조건에서_받고_사유는_표와_같다() -> None:
shapes = build_figure(_wall_sheet(), rubble_thickness_m=0.3)
texts = _texts(shapes)
assert "기초잡석 T=0.3 m — 산출 조건 · 폭은 버림과 같음" in texts
assert "버림 T=0.10 · 폭 1.80 − 키 0.35 = 1.45 m" in texts
wall = _paths(shapes)[0]["points"]
assert [0.95, 0.4] in wall and [1.068, 2.0] in wall and [1.368, 2.0] in wall
# 표 사유와 같은 말 — 터파기 줄이 없다는 것
_rows, table_notes = expand_observed(
"retaining_wall",
{"form": "반중력식", "height_m": 2.0},
_wall_sheet() | {"options": {"length_m": 10}},
)
missing = [note for note in table_notes if "터파기" in note]
assert missing and missing[0] in texts
assert not any(text.startswith("기초잡석") for text in _texts(build_figure(_wall_sheet(), 0)))
def test_옹벽_못_그리는_장은_표와_같은_까닭() -> None:
assert "형식" in figure_reason({**_wall_sheet(), "options": {"height_m": 2.0}})
other = figure_reason(_wall_sheet(height_m=1.6) | {"height_m": 1.6})
assert other.startswith("그림 없음 — ") and "자료에 없습니다" in other
note = [t for t in _texts(build_figure(_wall_sheet(weep_hole_area_m2=3))) if "안 닿음" in t]
assert note and "Ø50" in note[0]
def test_양식_장은_양식이_푼_뒷길이로_그린다() -> None:
sheet = {**_sheet(), "formula_sheet": {"vars": {"L3": 55}}}
texts = _texts(build_figure(sheet))