From a9ddcd5b1b2ad19f6694b1b728b9ba9608eea793 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 04:42:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC?= =?UTF-8?q?=EB=8F=84=20=EA=B7=B8=EB=A6=BC=20=E2=80=94=20=EB=92=B7=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20=EB=AF=B8=EC=A0=95=20=EC=9E=A5=EB=8F=84=20=ED=91=9C?= =?UTF-8?q?=EA=B0=80=20=EC=84=A0=20=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B7=B8=EB=A6=BC=20=C2=B7=20=ED=91=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9C=A0=EC=99=80=20=EA=B0=99=EC=9D=80=20=EB=A7=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 뒷길이를 안 고른 돌쌓기 장: 표가 쓴 그 뒷길이(양식 장은 양식이 푼 값, 전개 장은 기본 45㎝)로 그림 - 「⚠ 뒷길이를 안 골라 기본 N㎝ 로 섰습니다」 한 줄을 한 곳에 두고 표 사유·그림이 같이 씀(골막이 문구도 같은 함수) - 돌쌓기 표 사유에 그 한 줄이 새로 붙음 — 전에는 기본값인 줄 표에 안 드러났음 - 그림 까닭은 정말 못 그리는 장(큰돌쌓기 직경 축·옹벽)만 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- .../B08_Quantity_Engine_StructureFigure.py | 36 +++++++++++-------- .../B08_Quantity_Engine_UnitQuantity.py | 13 +++++++ ..._Quantity_Engine_UnitQuantity_Revetment.py | 9 +++-- .../tester/test_b07_standard_figure_reason.py | 6 ++-- resources/tester/test_b08_structure_figure.py | 34 +++++++++++++++++- 5 files changed, 74 insertions(+), 24 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_StructureFigure.py b/B08_Quantity/B08_Quantity_Engine_StructureFigure.py index 9aa7803e..4d47aa47 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureFigure.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureFigure.py @@ -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 diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 0354465a..12668476 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -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). # 안 고르면 종전 값(건설품셈 참고자료)으로 돌되 그 사실을 알린다 — 값을 못 낸다고 # 멈추면 이미 저장된 프로젝트가 통째로 빈다. diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py index 6fa008f6..47181b73 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py @@ -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) diff --git a/resources/tester/test_b07_standard_figure_reason.py b/resources/tester/test_b07_standard_figure_reason.py index 85d6da8b..6ecb4ee4 100644 --- a/resources/tester/test_b07_standard_figure_reason.py +++ b/resources/tester/test_b07_standard_figure_reason.py @@ -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: diff --git a/resources/tester/test_b08_structure_figure.py b/resources/tester/test_b08_structure_figure.py index 451e02f4..908677a8 100644 --- a/resources/tester/test_b08_structure_figure.py +++ b/resources/tester/test_b08_structure_figure.py @@ -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