fix(B08): 뒷길이를 접지 않음 — 일곱 규격을 다 씀

조율 창 지시로 KeyError 를 막으러 갔다가 더 나쁜 것을 찾음.
죽지는 않고 있었고 대신 조용히 접히고 있었음.

  40㎝  → 45㎝ 계수로 돎     999㎝ → 60㎝ 계수로 돎
  25·30 → 35 로, 75 → 60 으로 접힘

⚠ 접을 까닭이 없었음 — 품셈 13-4-3·13-4-4 [주]① 이 25·30·35·45·55·60·75
  일곱 규격을 다 줌. 우리 표가 네 칸만 들고 있었을 뿐임.

- 표를 일곱 규격으로 채움(건설품셈 참고자료 값, 고임돌은 「깬돌」 열)
- 표에 없는 값(40 등)은 접지 않고 「품셈 표에 없어 물량이 서지 않습니다」
- ⚠ 원문 「-」인 칸은 0 줄을 만들지 않음 — 0 은 「없음」과 구별이 안 되고
  받는 쪽이 「값이 0 인 자재」로 읽음
- ⚠ 고른 종류의 빈 칸은 빈 칸으로 덮음 — None 이라고 안 덮었더니
  「야면석 75㎝」처럼 원문에 「-」인 칸에 종전 값(깬돌 0.25)이 섰음(만들다 잡음)

⚠ 옛 시험이 「덮는 위 칸으로 접는다」를 계약으로 못 박고 있었음 — 오늘 네 번째.

시험 넷 — 접지 않음 · 표에 없으면 물량 안 냄 · 고른 종류의 빈 칸은 줄 없음 ·
일곱 규격이 다 섬.
시험: 723 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 무관).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 12:33:31 +09:00
co-authored by Claude Opus 5
parent e9a25bbf87
commit 48173b8ca4
2 changed files with 159 additions and 35 deletions
@@ -52,11 +52,17 @@ from common_util.common_util_quantity_spread import spread_by_unit
# ── 계수표 — 식에 박지 않고 여기서 고른다 ─────────────────────────────
# 돌 뒷길이(㎝)별 원단위. 출처: `original/실무문서/_원단위라이브러리_울진소광.md` 「돌뒷길이별 원단위표」.
# ⚠ 60㎝ 돌중량은 원본이 비어 있다 — 지어내지 않고 None 으로 둔다(PLAN 8-8 ㉮).
#: ⚠ **일곱 규격**이다 — 품셈 13-4-3·13-4-4 [주]① 이 25·30·35·45·55·60·75 를 다 준다.
#: 앞서 네 칸(35·45·55·60)만 들고 25·30 을 35 로, 75 를 60 으로 **접고** 있었다.
#: `stone_ton_per_m2`(돌중량)는 **실무 관측값**이라 그 넷에만 있다 — 없는 칸은 `None`.
STONE_BACK_LENGTH_TABLE: dict[int, dict[str, float | None]] = {
25: {"fill_concrete_m3_per_m2": 0.11, "wedge_stone_m3_per_m2": None, "stone_ton_per_m2": None},
30: {"fill_concrete_m3_per_m2": 0.14, "wedge_stone_m3_per_m2": 0.10, "stone_ton_per_m2": None},
35: {"fill_concrete_m3_per_m2": 0.16, "wedge_stone_m3_per_m2": 0.12, "stone_ton_per_m2": 0.575},
45: {"fill_concrete_m3_per_m2": 0.20, "wedge_stone_m3_per_m2": 0.15, "stone_ton_per_m2": 0.88},
55: {"fill_concrete_m3_per_m2": 0.25, "wedge_stone_m3_per_m2": 0.18, "stone_ton_per_m2": 1.10},
60: {"fill_concrete_m3_per_m2": 0.27, "wedge_stone_m3_per_m2": 0.20, "stone_ton_per_m2": None},
75: {"fill_concrete_m3_per_m2": 0.34, "wedge_stone_m3_per_m2": 0.25, "stone_ton_per_m2": None},
}
DEFAULT_BACK_LENGTH_CM = 45
@@ -96,6 +102,8 @@ def stone_coefficients(options: dict[str, Any], back_cm: int) -> tuple[dict[str,
kind = str(options.get(STONE_KIND_OPTION) or "").strip()
if not table:
return {}, ""
if key not in [str(x) for x in (table.get("back_lengths_cm") or [])]:
return {}, (f"뒷길이 {back_cm}㎝ 는 품셈 표(25·30·35·45·55·60·75㎝)에 없어 계수가 없습니다")
if not kind:
fallback = table.get("fallback") or {}
return {
@@ -218,11 +226,11 @@ BACK_LENGTH_KEYS = ("back_len_cm", "stone_back_length_cm")
def _back_length(options: dict[str, Any]) -> int:
"""뒷길이(㎝). 표에 없는 값이면 **가장 가까운 아래 칸**이 아니라 기본으로 간다.
"""저장된 뒷길이(㎝)를 **그대로** 돌려준다. 안 정했으면 기본 45.
표는 35·45·55·60 네 칸뿐인데 레지스트리 선택지는 25·30·35·45·55·60·75 일곱이다.
25·30 은 35 로, 75 는 60 으로 접는다 — **원단위표가 그 구간을 그렇게 덮는다**
(품셈 13-4 뒷길이 표준). 접은 사실은 근거 문구에 적는다.
**접지 않는다.** 품셈 13-4-3·13-4-4 [주]① 이 25·30·35·45·55·60·75 **일곱 규격**을
다 주므로 접을 까닭이 없다. 그 밖의 값(40 등)은 **계수가 없다고 드러낸다**
접으면 다른 규격 계수가 조용히 돈다.
"""
for key in BACK_LENGTH_KEYS:
raw = options.get(key)
@@ -232,13 +240,10 @@ def _back_length(options: dict[str, Any]) -> int:
value = int(float(raw))
except (TypeError, ValueError):
continue
if value in STONE_BACK_LENGTH_TABLE:
return value
# 표에 없는 값 — 그 값을 덮는 **가장 가까운 위 칸**으로 접는다(작은 돌이 큰 칸에 들어감).
larger = [cm for cm in sorted(STONE_BACK_LENGTH_TABLE) if cm >= value]
if larger:
return larger[0]
return max(STONE_BACK_LENGTH_TABLE)
# ⚠ **접지 않는다.** 앞서 「가장 가까운 위 칸」으로 접고 있었는데, 그러면 40㎝ 가
# 45㎝ 계수로 **조용히** 돌고 999㎝ 도 60㎝ 로 접혔다(2026-09-08 실측).
# 표에 없으면 그 값을 그대로 돌려주고, 계수를 고르는 쪽이 「없다」고 드러낸다.
return value
return DEFAULT_BACK_LENGTH_CM
@@ -367,6 +372,11 @@ def stone_masonry(
return [], ["높이·연장이 없어 전개하지 않음"]
back_cm = _back_length(options)
if back_cm not in STONE_BACK_LENGTH_TABLE:
# ⚠ 접지 않는다 — 다른 규격 계수가 조용히 도는 것보다 「없다」가 낫다.
return [], [
f"뒷길이 {back_cm}㎝ 는 품셈 표(25·30·35·45·55·60·75㎝)에 없어 물량이 서지 않습니다"
]
table = dict(STONE_BACK_LENGTH_TABLE[back_cm])
# ⚠ **돌 종류로 계수가 갈린다** (품셈 13-4-3 · 13-4-4 [주]① · 교본 7-3).
# 안 고르면 종전 값(건설품셈 참고자료)으로 돌되 그 사실을 알린다 — 값을 못 낸다고
@@ -374,9 +384,15 @@ def stone_masonry(
picked, kind_note = stone_coefficients(options, back_cm)
if kind_note:
notes.append(kind_note)
for key in ("wedge_stone_m3_per_m2", "fill_concrete_m3_per_m2"):
if picked.get(key) is not None:
table[key] = picked[key]
# ⚠ **고른 종류의 빈 칸은 빈 칸으로 덮는다.** `None` 이라고 안 덮으면 종전 값(깬돌 계열)이
# 남아 「야면석 75㎝」처럼 **원문에 「-」인 칸에 값이 서는** 일이 생긴다(만들다 잡음).
if picked.get("kind"):
for key in ("wedge_stone_m3_per_m2", "fill_concrete_m3_per_m2"):
table[key] = picked.get(key)
else:
for key in ("wedge_stone_m3_per_m2", "fill_concrete_m3_per_m2"):
if picked.get(key) is not None:
table[key] = picked[key]
#: ⚠ **표는 「뒤채움 몫」, 우리 식은 「빼는 몫」** — 뜻이 반대라 1 에서 뺀다.
#: 교본은 「뒤채움 = 뒷길이 × (깬돌·잡석 1/2, 야면석 1/3)」이고, 우리 식이 입적에서
#: 빼는 것은 **돌 몸통**이라 `1 − 뒤채움몫` 이다. 종전 2/3 이 곧 야면석(1 − 1/3)이었다.
@@ -411,14 +427,27 @@ def stone_masonry(
DESTINATION["돌쌓기"],
f"정면적 × √(1+{slope_ratio}²) — 비탈면적",
),
Component(
"고임돌",
"",
masonry_area * _num(table["wedge_stone_m3_per_m2"]),
DESTINATION["고임돌"],
f"돌쌓기 × {table['wedge_stone_m3_per_m2']} ㎥/㎡ (뒷길이 {back_cm}㎝)",
),
]
# ⚠ 고임돌 계수가 **원문에서 「-」**인 칸이 있다(견치돌 25·30 · 야면석 75 · 깬돌 25).
# 그 규격에 그 돌을 안 쓴다는 뜻이라 **0 줄을 만들지 않는다** — 0 은 「없음」과
# 구별이 안 되고, 받는 쪽이 「값이 0 인 자재」로 읽는다.
if table["wedge_stone_m3_per_m2"] is None:
notes.append(
f"고임돌 계수가 품셈 표에 없습니다 — 뒷길이 {back_cm}"
+ (f" · {kind_label}" if kind_label else "")
+ " 칸이 「-」입니다"
)
else:
components.append(
Component(
"고임돌",
"",
masonry_area * _num(table["wedge_stone_m3_per_m2"]),
DESTINATION["고임돌"],
f"돌쌓기 × {table['wedge_stone_m3_per_m2']} ㎥/㎡ (뒷길이 {back_cm}㎝)"
+ (f" · {kind_label}" if kind_label else ""),
)
)
stone_ton = table["stone_ton_per_m2"]
if stone_ton is None:
@@ -437,7 +466,7 @@ def stone_masonry(
# 막자갈 = 입적 − (면적 × 뒷길이 × 뒤채움몫 + 고임돌).
# 뒤채움 몫은 **돌 종류로 갈린다** — 깬돌·잡석 1/2 · 야면석 1/3 (교본 7-3).
wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"])
wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"]) # None 이면 0 — 막자갈에서 안 뺌
rubble = volume - (masonry_area * (back_cm / 100.0) * body_ratio + wedge)
if rubble > 0:
components.append(
+108 -13
View File
@@ -29,20 +29,89 @@
"unset_is_flagged": "돌 종류를 안 고르면 **건설품셈 참고자료 한 벌**로 돌되 그 사실을 알린다 — 값을 못 낸다고 멈추면 이미 저장된 프로젝트가 통째로 빈다.",
"no_interpolation": "표에 없는 뒷길이·「-」 칸은 **지어내지 않는다.**"
},
"kinds": ["야면석·호박돌", "깬잡석", "깬돌", "견치돌"],
"kinds": [
"야면석·호박돌",
"깬잡석",
"깬돌",
"견치돌"
],
"wedge_stone_m3_per_m2": {
"note": "고임돌 — 품셈 13-4-3. 키는 뒷길이(㎝). `null` 은 원문 「-」.",
"야면석·호박돌": { "25": 0.06, "30": 0.07, "35": 0.09, "45": 0.11, "55": 0.14, "60": 0.15, "75": null },
"깬잡석": { "25": 0.09, "30": 0.11, "35": 0.13, "45": 0.16, "55": 0.19, "60": 0.21, "75": 0.26 },
"깬돌": { "25": null, "30": 0.10, "35": 0.12, "45": 0.15, "55": 0.18, "60": 0.20, "75": 0.25 },
"견치돌": { "25": null, "30": null, "35": 0.12, "45": 0.15, "55": 0.18, "60": 0.20, "75": 0.25 }
"야면석·호박돌": {
"25": 0.06,
"30": 0.07,
"35": 0.09,
"45": 0.11,
"55": 0.14,
"60": 0.15,
"75": null
},
"깬잡석": {
"25": 0.09,
"30": 0.11,
"35": 0.13,
"45": 0.16,
"55": 0.19,
"60": 0.21,
"75": 0.26
},
"깬돌": {
"25": null,
"30": 0.1,
"35": 0.12,
"45": 0.15,
"55": 0.18,
"60": 0.2,
"75": 0.25
},
"견치돌": {
"25": null,
"30": null,
"35": 0.12,
"45": 0.15,
"55": 0.18,
"60": 0.2,
"75": 0.25
}
},
"fill_concrete_m3_per_m2": {
"note": "채움 콘크리트 — 품셈 13-4-4 [주]①. **두 줄뿐**이라 견치돌·깬잡석·깬돌이 한 값을 쓴다.",
"야면석·호박돌": { "25": 0.08, "30": 0.10, "35": 0.12, "45": 0.15, "55": 0.18, "60": 0.20, "75": 0.25 },
"깬잡석": { "25": 0.11, "30": 0.14, "35": 0.16, "45": 0.20, "55": 0.25, "60": 0.27, "75": 0.34 },
"깬돌": { "25": 0.11, "30": 0.14, "35": 0.16, "45": 0.20, "55": 0.25, "60": 0.27, "75": 0.34 },
"견치돌": { "25": 0.11, "30": 0.14, "35": 0.16, "45": 0.20, "55": 0.25, "60": 0.27, "75": 0.34 }
"야면석·호박돌": {
"25": 0.08,
"30": 0.1,
"35": 0.12,
"45": 0.15,
"55": 0.18,
"60": 0.2,
"75": 0.25
},
"깬잡석": {
"25": 0.11,
"30": 0.14,
"35": 0.16,
"45": 0.2,
"55": 0.25,
"60": 0.27,
"75": 0.34
},
"깬돌": {
"25": 0.11,
"30": 0.14,
"35": 0.16,
"45": 0.2,
"55": 0.25,
"60": 0.27,
"75": 0.34
},
"견치돌": {
"25": 0.11,
"30": 0.14,
"35": 0.16,
"45": 0.2,
"55": 0.25,
"60": 0.27,
"75": 0.34
}
},
"backfill_ratio_of_back_length": {
"note": "뒤채움이 뒷길이에서 차지하는 몫 — 교본 7-3. 나머지가 잡석(막자갈)이 된다.",
@@ -52,9 +121,25 @@
"견치돌": 0.5
},
"fallback": {
"note": "돌 종류 미지정일 때 — 건설품셈 참고자료 한 벌. **지금까지 쓰던 값 그대로**라 저장된 프로젝트의 값이 안 바뀐다.",
"wedge_stone_m3_per_m2": { "35": 0.12, "45": 0.15, "55": 0.18, "60": 0.20 },
"fill_concrete_m3_per_m2": { "35": 0.16, "45": 0.20, "55": 0.25, "60": 0.27 },
"note": "돌 종류 미지정일 때 — 건설품셈 [참고자료] 돌쌓기 규격별 소요량 한 벌(고임돌은 「깬돌」 열). ⚠ 일곱 규격을 다 싣는다 — 앞서 네 칸만 들고 있어 25·30·75 를 가까운 칸으로 **접어 올리고** 있었다.",
"wedge_stone_m3_per_m2": {
"25": null,
"30": 0.1,
"35": 0.12,
"45": 0.15,
"55": 0.18,
"60": 0.2,
"75": 0.25
},
"fill_concrete_m3_per_m2": {
"25": 0.11,
"30": 0.14,
"35": 0.16,
"45": 0.2,
"55": 0.25,
"60": 0.27,
"75": 0.34
},
"backfill_ratio_of_back_length": 0.3333333333333333,
"backfill_note": "⚠ 이 값은 **뒤채움이 차지하는 몫**이다. 우리 막자갈 식은 입적에서 **돌 몸통**을 빼므로 코드가 `1 − 이 값` 을 쓴다. 종전 하드코딩 2/3 이 곧 야면석(1 1/3)이었다.",
"message": "돌 종류를 안 정해 건설품셈 참고자료 값으로 섰습니다 — 구조물 상세 입력에서 고르면 산림품셈 계수로 바뀝니다"
@@ -65,5 +150,15 @@
"돌중량(ton/㎡) — 우리 값 0.575/0.88/1.10 은 **실무 관측**(울진 라이브러리)이다. 품셈은 「돌의 중량은 형상·종류·부피를 고려하고 건설품셈 1-3-3 재료의 단위중량을 참고하여 계상한다」로만 두어 **표를 안 준다.** 돌 종류 축과 짝이 맞는지 사용자 확정 대기.",
"전면면적이 정면적인지 비탈면적인지 — 2026 개정안 [주] 「시공량은 석재의 **전면면적**(㎡)을 기준한다」로 용어만 확인됐다."
]
}
},
"back_lengths_cm": [
25,
30,
35,
45,
55,
60,
75
],
"no_folding": "⚠ 표에 없는 뒷길이를 **가까운 칸으로 접지 않는다.** 접으면 40㎝ 가 45㎝ 계수로 조용히 돌고 999㎝ 도 60㎝ 로 접혔다(2026-09-08 실측). 표에 없으면 **계수가 없다고 드러낸다.**"
}