feat(B07): 표준도 제원에 「기초」 칸 + 그림을 새 두께 식에 맞춤

**기초 칸**(`foundation` — 기초유 / 기초버림, 정본 xls 탭 제목 그대로)
터파기 기초 몫이 0.5×(0.7+0.2)=0.45 대 0.1×(0.7+0.0)=0.07 ㎥/m 로 갈리고, 횡단도
터파기 선도 같은 칸을 봄. 고를 자리가 어디에도 없어 비어 있던 값임.
정본에 없는 갈래를 넣으면 **막지 않고 알림**(기존 네 칸과 같은 결). 비우면 키를 지움.

**그림을 새 두께 식에 맞춤** — 데스크탑 메인이 `STONE_MASONRY` 를 갈면서
`thickness_base_m`·`thickness_top_coeff`·`thickness_bottom_coeff` 가 사라져
그림이 `KeyError` 로 죽었음(도면이 「읽지 못했습니다」로 뜸). 실화면에서 잡았음.
  상부 = 뒷길이 + 0.30 · 하부 = 상부 + 0.30 × (H − 1.0)
  H=1.0 ℓ45 → 0.75/0.75 · H=1.5 ℓ35 → 0.65/0.80 · H=2.0 ℓ35 → 0.65/0.95 ·
  H=2.0 ℓ45 → 0.75/1.05 · H=3.0 ℓ45 → 0.75/1.35   ← 실무 원본과 전부 일치

⚠ **뒷길이를 안 정한 장은 두께를 못 내므로 그리지 않음** — 0 으로 때우면 거짓 그림이 됨.

⚠ 배운 것 — 「그림이 상수를 직접 읽으니 저절로 따라온다」가 **키 이름이 바뀌면 깨진다**는
   것으로 드러남. 값은 한 벌이 맞으나 **이름이 바뀌는 것은 예고가 필요함.**

실화면 확인 — 「표준도 3장」에서 기초 「기초유」 저장 → 「2개소에 반영했습니다」,
정본 실측 `[기초유, null, 기초유, null, null]`(그 장의 두 개소에만).

자체검증 — 새 시험 3건 + 회귀 583 통과 · 0 실패, `tsc --noEmit` 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 21:44:28 +09:00
co-authored by Claude Opus 5
parent 2c48ca6b1c
commit e128f651fc
5 changed files with 66 additions and 19 deletions
@@ -11,12 +11,14 @@
⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를
직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장).
상부 두께 = 0.45 + 0.10·H 하부 두께 = 0.45 + 0.40·H
상부 두께 = 뒷길이 + 0.30 하부 두께 = 상부 + 0.30 × (H − 1.0)
터파기 폭 = 평균두께 + 0.2 되메우기 두께 = 0.2
ⓘ 이 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻한다 — 하부−상부 = 0.30·H 이고 전면이 뒤로
기우는 양도 0.3·H 라 딱 맞는다. 기울기가 0.3 이 아니면 그 차이만큼 **뒷면이 기운다**.
지어낸 것이 아니라 상수에서 따라 나오는 결과라 그대로 그린다.
⚠ **뒷길이가 두께에 들어간다**(확정 2차 ②, 실무 구조물도 식). 뒷길이가 다르면 벽이
두꺼워지고 그림도 그만큼 넓어진다 — 예전 식(0.45+0.10H / 0.45+0.40H)은 뒷길이를
아예 안 봐서 35㎝ 든 45㎝ 든 같은 그림이 나왔다.
⚠ 뒷길이를 안 정한 장은 두께를 못 낸다 — 그때는 **그리지 않는다**(0 으로 때우면
거짓 그림이 된다).
⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서
몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다.
@@ -69,12 +71,30 @@ def slope_of(sheet: dict[str, Any]) -> tuple[float, str]:
)
def section_points(height_m: float, slope_ratio: float) -> list[tuple[float, float]]:
"""벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다."""
top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m
bottom_t = (
STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m
def wall_thickness(height_m: float, back_cm: float) -> tuple[float, float]:
"""(상부, 하부) 두께 — **수량이 쓰는 그 식**(`stone_masonry`)과 같은 상수를 읽는다."""
top_t = back_cm / 100.0 + STONE_MASONRY["thickness_top_add_m"]
bottom_t = top_t + STONE_MASONRY["thickness_slope_per_m"] * max(
height_m - STONE_MASONRY["thickness_height_base_m"], 0.0
)
return top_t, bottom_t
def back_length_cm(sheet: dict[str, Any]) -> float | None:
"""장의 뒷길이(㎝). 안 정했으면 `None` — 그때는 두께를 못 내므로 그리지 않는다."""
options = sheet.get("options") or {}
raw = options.get("back_len_cm") or options.get("stone_back_length_cm")
try:
return float(raw) if raw not in (None, "") else None
except (TypeError, ValueError):
return None
def section_points(
height_m: float, slope_ratio: float, back_cm: float
) -> list[tuple[float, float]]:
"""벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다."""
top_t, bottom_t = wall_thickness(height_m, back_cm)
lean = slope_ratio * height_m
return [
(0.0, 0.0),
@@ -101,7 +121,10 @@ def build_figure(
if height_m <= 0:
return [], 0.0
options = sheet.get("options") or {}
back_cm = back_length_cm(sheet)
if back_cm is None:
# 뒷길이가 없으면 두께를 못 낸다 — 0 으로 때우지 않고 그리지 않는다.
return [], 0.0
slope, slope_note = slope_of(sheet)
scale = SCALE_MM_PER_M
ox, oy = origin
@@ -109,11 +132,8 @@ def build_figure(
def mm(point: tuple[float, float]) -> tuple[float, float]:
return (ox + point[0] * scale, oy + point[1] * scale)
points = section_points(height_m, slope)
top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m
bottom_t = (
STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m
)
points = section_points(height_m, slope, back_cm)
top_t, bottom_t = wall_thickness(height_m, back_cm)
average_t = (top_t + bottom_t) / 2.0
dig_width = average_t + STONE_MASONRY["excavation_extra_m"]
backfill_t = STONE_MASONRY["backfill_thickness_m"]
@@ -177,9 +197,7 @@ def build_figure(
),
("막자갈(뒷채움) — 수량은 아래 표", (dig_width + 0.16, height_m * 0.55), "left"),
]
back = options.get("back_len_cm") or options.get("stone_back_length_cm")
if back:
labels.append((f"뒷길이 ℓ₃ = {int(back)}㎝", (dig_width + 0.16, height_m * 0.72), "left"))
labels.append((f"뒷길이 ℓ₃ = {int(back_cm)}㎝", (dig_width + 0.16, height_m * 0.72), "left"))
for index, (text, point, align) in enumerate(labels):
x, y = mm(point)