From 48173b8ca454db8bd3d6ce1fd2c85268134c498d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 12:33:31 +0900 Subject: [PATCH] =?UTF-8?q?fix(B08):=20=EB=92=B7=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=91=EC=A7=80=20=EC=95=8A=EC=9D=8C=20=E2=80=94?= =?UTF-8?q?=20=EC=9D=BC=EA=B3=B1=20=EA=B7=9C=EA=B2=A9=EC=9D=84=20=EB=8B=A4?= =?UTF-8?q?=20=EC=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 조율 창 지시로 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) --- .../B08_Quantity_Engine_UnitQuantity.py | 73 +++++++---- .../data_masonry/stone_kind_2026-01-01.json | 121 ++++++++++++++++-- 2 files changed, 159 insertions(+), 35 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 16edc900..2ba08c1a 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -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( diff --git a/resources/data_masonry/stone_kind_2026-01-01.json b/resources/data_masonry/stone_kind_2026-01-01.json index 13611713..3821fa2a 100644 --- a/resources/data_masonry/stone_kind_2026-01-01.json +++ b/resources/data_masonry/stone_kind_2026-01-01.json @@ -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 실측). 표에 없으면 **계수가 없다고 드러낸다.**" }