feat(b08): 구조물도 그림 — 뒷길이 미정 장도 표가 선 기본값으로 그림 · 표 사유와 같은 말

- 뒷길이를 안 고른 돌쌓기 장: 표가 쓴 그 뒷길이(양식 장은 양식이 푼 값, 전개 장은 기본 45㎝)로 그림
- 「⚠ 뒷길이를 안 골라 기본 N㎝ 로 섰습니다」 한 줄을 한 곳에 두고 표 사유·그림이 같이 씀(골막이 문구도 같은 함수)
- 돌쌓기 표 사유에 그 한 줄이 새로 붙음 — 전에는 기본값인 줄 표에 안 드러났음
- 그림 까닭은 정말 못 그리는 장(큰돌쌓기 직경 축·옹벽)만

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 04:42:28 +09:00
co-authored by Claude Opus 5
parent 415a76a97b
commit a9ddcd5b1b
5 changed files with 74 additions and 24 deletions
@@ -15,7 +15,8 @@
터파기 폭 = 평균두께 + 0.2 기초 0.5×0.9 (기초유) · 0.1×0.7 (기초버림)
⚠ 터파기 치수는 **`common_util_excavation` 한 벌**을 읽는다 — 횡단도가 쓰는 그 상수다.
⚠ 뒷길이를 안 정한 장은 **그리지 않고 까닭을 적는다**(브레인 판정 — 빈칸 대신 까닭).
⚠ 뒷길이를 안 정한 장은 **표가 선 그 기본값으로 그리고 표 사유와 같은 말을 적는다**
(2026-09-14 브레인 판정 — 표가 서면 그림도 섬). 까닭은 정말 못 그리는 장(큰돌쌓기 등)만.
⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서
몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다.
@@ -27,7 +28,8 @@ from typing import Any
from B08_Quantity.B08_Quantity_Engine_StructureSheet import slope_of
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import (
BACK_LENGTH_KEYS,
_back_length,
back_length_default_note,
wall_thickness,
weep_hole_spec,
)
@@ -64,16 +66,15 @@ _LABEL_SIZE = 4.0 / SCALE_MM_PER_M
_DIM_SIZE = 3.4 / SCALE_MM_PER_M
def back_length_cm(sheet: dict[str, Any]) -> float | None:
"""장의 뒷길이(㎝). 안 정했으면 `None` — 그때는 그리지 않는다."""
def back_length_cm(sheet: dict[str, Any]) -> tuple[int, str | None]:
"""(표가 쓴 뒷길이 ㎝, 기본으로 섰으면 표 사유와 같은 한 줄).
양식 장은 양식이 푼 값(`formula_sheet.vars.L3`), 전개 장은 `_back_length` — 표와 한 벌.
"""
options = sheet.get("options") or {}
raw = next(
(options[key] for key in BACK_LENGTH_KEYS if options.get(key) not in (None, "")), None
)
try:
return float(raw) if raw is not None else None
except (TypeError, ValueError):
return None
solved = ((sheet.get("formula_sheet") or {}).get("vars") or {}).get("L3")
back = int(float(solved)) if isinstance(solved, (int, float)) else _back_length(options)
return back, back_length_default_note(options, back)
def figure_reason(sheet: dict[str, Any]) -> str | None:
@@ -83,8 +84,6 @@ def figure_reason(sheet: dict[str, Any]) -> str | None:
return _NO_FIGURE_REASONS.get(type_id, _NO_FIGURE_DEFAULT)
if float(sheet.get("height_m") or 0.0) <= 0:
return "그림 없음 — 높이가 없습니다(제원에서 높이를 넣으면 그림이 섭니다)"
if back_length_cm(sheet) is None:
return "그림 없음 — **뒷길이를 고르면 그림이 섭니다**(두께가 뒷길이에서 나옵니다)"
return None
@@ -104,7 +103,7 @@ def build_figure(sheet: dict[str, Any]) -> list[dict[str, Any]] | None:
return None
options = sheet.get("options") or {}
height_m = float(sheet.get("height_m") or 0.0)
back_cm = back_length_cm(sheet) or 0.0
back_cm, default_note = back_length_cm(sheet)
slope, slope_note = slope_of(sheet)
top_t, bottom_t, _basis = wall_thickness(options, back_cm=back_cm, height_m=height_m)
lean = slope * height_m
@@ -156,7 +155,11 @@ def build_figure(sheet: dict[str, Any]) -> list[dict[str, Any]] | None:
(right, height_m * 0.92),
"left",
),
_text(f"뒷길이 ℓ₃ = {back_cm:g}", (right, height_m * 0.72), "left"),
_text(
f"뒷길이 ℓ₃ = {back_cm}{' (기본)' if default_note else ''}",
(right, height_m * 0.72),
"left",
),
_text("막자갈(뒷채움) — 수량은 아래 표", (right, height_m * 0.55), "left"),
_text(
f"물구멍 Ø{weep_diameter_mm}{weep_area_m2:g}㎡당 1개소",
@@ -179,6 +182,9 @@ def build_figure(sheet: dict[str, Any]) -> list[dict[str, Any]] | None:
_LABEL_SIZE,
),
]
if default_note:
# 「이 값이 어디서 왔나」 — 표 사유 줄과 **글자까지 같게**(브레인 판정).
shapes.append(_text(default_note, (0.0, -0.80 - base_d), "left"))
return shapes
@@ -627,6 +627,16 @@ def _back_length(options: dict[str, Any]) -> int:
return DEFAULT_BACK_LENGTH_CM
def back_length_default_note(options: dict[str, Any], back_cm: int) -> str | None:
"""뒷길이를 안 골라 기본으로 섰을 때의 한 줄 — 표 사유·구조물도 그림이 **같은 말**을 씀."""
if any(options.get(key) not in (None, "") for key in BACK_LENGTH_KEYS):
return None
return (
f"⚠ 뒷길이를 안 골라 기본 {back_cm}㎝ 로 섰습니다 "
"— 그 값이 두께식·고임돌·돌 무게·채움콘크리트 계수를 모두 가릅니다"
)
def _num(value: Any, fallback: float = 0.0) -> float:
return float(value) if isinstance(value, (int, float)) else fallback
@@ -877,6 +887,9 @@ def stone_masonry(
f"뒷길이 {back_cm}㎝ 는 품셈 표(25·30·35·45·55·60·75㎝)에 없어 물량이 서지 않습니다"
]
table = dict(STONE_BACK_LENGTH_TABLE[back_cm])
default_note = back_length_default_note(options, back_cm)
if default_note:
notes.append(default_note)
# ⚠ **돌 종류로 계수가 갈린다** (품셈 13-4-3 · 13-4-4 [주]① · 교본 7-3).
# 안 고르면 종전 값(건설품셈 참고자료)으로 돌되 그 사실을 알린다 — 값을 못 낸다고
# 멈추면 이미 저장된 프로젝트가 통째로 빈다.
@@ -210,6 +210,7 @@ def erosion_check_dam(
STONE_BACK_LENGTH_TABLE,
STONE_MASONRY,
_back_length,
back_length_default_note,
fill_concrete_mpa,
stone_weight_per_m2,
weep_hole_spec,
@@ -240,11 +241,9 @@ def erosion_check_dam(
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}㎝ 로 섰습니다 "
"— 그 값이 두께식·고임돌·돌 무게·채움콘크리트 계수를 모두 가릅니다"
)
default_note = back_length_default_note(options, back_cm)
if default_note:
notes.append(default_note)
back_m = back_cm / 100.0
given = _num(options.get("face_slope_ratio"), 0.0)
@@ -45,9 +45,9 @@ def _sheet(type_id: str, **options) -> dict:
}
def test_뒷길이가_없으면_무엇을_고르면_되는지_적는() -> None:
reason = figure_reason(_sheet("masonry_wet"))
assert reason is not None and "뒷길이" in reason and "섭니다" in reason
def test_뒷길이가_없어도_표처럼_기본값으로_서니_까닭이_없() -> None:
"""2026-09-14 브레인 판정 — 표가 기본 45㎝ 로 서면 그림도 섬(까닭은 정말 못 그리는 장만)."""
assert figure_reason(_sheet("masonry_wet")) is None
def test_뒷길이가_있으면_까닭이_없다() -> None:
+33 -1
View File
@@ -16,6 +16,10 @@ from B08_Quantity.B08_Quantity_Engine_StructureFigure import ( # noqa: E402
attach_figures,
build_figure,
)
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( # noqa: E402
back_length_default_note,
stone_masonry,
)
def _sheet(**options) -> dict:
@@ -65,6 +69,34 @@ def test_못_그리는_장은_그림_대신_까닭이_실린다() -> None:
}
attach_figures(payload)
no_back, boulder, drawn = payload["sheets"]
assert no_back["figure"] is None and "뒷길이" in no_back["figure_reason"]
# 뒷길이 미정은 기본값이 있어 **그림이 섬**(2026-09-14 브레인 판정) — 까닭은 직경 축만.
assert no_back["figure_reason"] is None and no_back["figure"]
assert boulder["figure"] is None and "직경" in boulder["figure_reason"]
assert drawn["figure_reason"] is None and drawn["figure"]
def _texts(shapes: list[dict]) -> list[str]:
return [shape["text"] for shape in shapes if shape["kind"] == "text"]
def test_기본값으로_그린_장은_표_사유와_같은_말을_적는다() -> None:
options = {"height_m": 2.0}
_rows, table_notes = stone_masonry(2.0, 1.0, options, wet=False)
table_default = [note for note in table_notes if "뒷길이를 안 골라" in note]
assert table_default == [back_length_default_note(options, 45)]
texts = _texts(build_figure(_sheet()))
assert table_default[0] in texts
assert "뒷길이 ℓ₃ = 45㎝ (기본)" in texts
# 두께도 기본 45 로 — 상부 0.45+0.30
assert "상부 0.75 m" in texts
chosen = _texts(build_figure(_sheet(back_len_cm=45)))
assert "뒷길이 ℓ₃ = 45㎝" in chosen and not any("안 골라" in text for text in chosen)
assert back_length_default_note({"back_len_cm": 45}, 45) is None
def test_양식_장은_양식이_푼_뒷길이로_그린다() -> None:
sheet = {**_sheet(), "formula_sheet": {"vars": {"L3": 55}}}
texts = _texts(build_figure(sheet))
assert "뒷길이 ℓ₃ = 55㎝ (기본)" in texts and "상부 0.85 m" in texts