From dd8c86fa492c58a4d5db20ee587b5b4459f7641b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 23:50:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(B07):=20=ED=91=9C=EC=A4=80=EB=8F=84=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=BC=EC=9D=B4=20=EC=95=88=20=EC=84=9C=EB=8A=94=20?= =?UTF-8?q?=EC=9E=A5=EC=97=90=20=EA=B9=8C=EB=8B=AD=EC=9D=84=20=EC=A0=81?= =?UTF-8?q?=EC=9D=8C=20+=20=ED=81=B0=EB=8F=8C=EC=8C=93=EA=B8=B0=EB=8A=94?= =?UTF-8?q?=20=EA=B7=B8=EB=A6=BC=20=EB=8C=80=EC=83=81=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=BA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 계획서 4-12. **빈 자리를 그냥 두면 사용자가 「고장인가」로 읽음** — 장마다 왜 그림이 없는지와 **무엇을 받아야 서는지**를 표 위에 한 줄 적음. - 돌쌓기(찰) — 「뒷길이를 고르면 그림이 섭니다」 · 옹벽 — 「단면이 달라 아직 안 그림」 ⚠ **큰돌쌓기를 `FIGURE_TYPE_IDS` 에서 뺌** — 그림은 두께를 뒷길이에서 내는데 큰돌쌓기는 규격이 **직경**이라 목록에만 있고 결과는 늘 빈 그림이었음(품셈 13-6 직경 ↔ 13-4 뒷길이, 수량 쪽에서 이미 갈라 둔 자리). **식은 만들지 않음** — 「직경에서 두께를 내는 법」은 도메인 판단이라 사용자 몫이고, 지금은 사유만 띄움. ⚠ 곁가지 하나를 화면에서 잡아 함께 고침 — 그림 대상 목록을 **기울기 판정**에도 쓰고 있어 큰돌쌓기를 빼자 **장 제목의 「1:0.3」까지 사라졌음**. `SLOPE_TYPE_IDS` 로 갈라 되살림. 화면 실측 — 표준도 1장에 「그림 없음 — 큰돌쌓기는 규격이 직경이라 …」 한 줄이 표 위에 뜸. 시험 다섯 추가, 전체 1225 통과·실패 0. Co-Authored-By: Claude Opus 5 (1M context) --- ..._DesignDetail_Engine_Cad_StandardFigure.py | 40 ++++++++- ...7_DesignDetail_Engine_Cad_StandardSheet.py | 23 +++++- .../B07_DesignDetail_Engine_Standard_Sheet.py | 5 +- .../tester/test_b07_standard_figure_reason.py | 81 +++++++++++++++++++ 4 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 resources/tester/test_b07_standard_figure_reason.py diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py index 92d55f35..bf871ce8 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py @@ -50,7 +50,45 @@ from common_util.common_util_excavation import ( SCALE_MM_PER_M = 40.0 #: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다. -FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry", "boulder_masonry"}) +#: ⚠ **큰돌쌓기(`boulder_masonry`)를 뺐다**(2026-09-09). 여기 있었지만 그림이 두께를 +#: **뒷길이**에서 내는데 큰돌쌓기는 규격 축이 **직경**(`stone_cm`)이라 **영영 못 그렸다** — +#: 목록에만 있고 결과는 늘 빈 그림이었다. 품셈 **13-6(직경) ↔ 13-4(뒷길이)** 로 축이 +#: 갈리는 자리이고 수량 쪽에서 이미 갈라 둔 그것이다(`EXPANDERS` 주석). +#: ⇒ 「직경에서 두께를 내는 식」은 **도메인 판단이라 사용자 몫** — 계획서 4-12 답 대기 줄. +FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry"}) + +#: **전면 기울기 판정** 대상 — 그림 대상과 **다르다**. 큰돌쌓기는 그림은 못 그려도 +#: 기울기는 판정된다(품셈 13-6 「1:0.3 이상」). 한 목록으로 묶어 두었더니 큰돌쌓기를 +#: 그림에서 뺄 때 **장 제목의 「1:0.3」까지 사라졌다**(2026-09-09 화면 실측에서 잡음). +SLOPE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry", "boulder_masonry"}) + +#: 그림이 못 서는 장에 **까닭을 적는다** — 빈 자리를 그냥 두면 사용자가 「고장」으로 읽는다 +#: (2026-09-09 사용자 지시). 「무엇을 받아야 서는지」까지 적는다. +_NO_FIGURE_REASONS: dict[str, str] = { + "boulder_masonry": ( + "그림 없음 — 큰돌쌓기는 규격이 **직경**이라 두께를 낼 식이 아직 없습니다" + "(뒷길이로 내는 돌쌓기 식과 축이 다름 · 사용자 확정 대기)" + ), + "retaining_wall": "그림 없음 — 옹벽은 단면이 달라 아직 그리지 않습니다(표준도 3단계)", +} +_NO_FIGURE_DEFAULT = "그림 없음 — 이 형식은 아직 단면을 그리지 않습니다" + + +def figure_reason(sheet: dict[str, Any]) -> str | None: + """그림이 **안 서는 까닭** 한 줄. 서는 장이면 `None`. + + ⚠ 「무엇을 받아야 서는지」까지 적는다 — 뒷길이만 고르면 서는 장과, 식 자체가 없는 장은 + 사용자가 할 일이 다르다. + """ + type_id = str(sheet.get("type_id") or "") + if type_id not in FIGURE_TYPE_IDS: + 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 + _LABEL_FONT = 4.0 _DIM_FONT = 3.4 diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py index 4e2389d9..66d2d998 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py @@ -22,6 +22,7 @@ from typing import Any from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( DRAWING_FORMAT, + _text_entity, TABLE_LABEL_COLOR, TABLE_LINE_COLOR, TABLE_VALUE_COLOR, @@ -32,7 +33,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import ( SCALE_MM_PER_M as FIGURE_SCALE_MM_PER_M, ) -from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import build_figure +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import build_figure, figure_reason from B07_DesignDetail.B07_DesignDetail_Engine_Template import usable_bbox logger = logging.getLogger(__name__) @@ -58,6 +59,8 @@ def sheet_drawing_id(number: int, total: int) -> str: _COLUMN_WIDTHS: tuple[float, ...] = (90.0, 535.0, 70.0, 40.0) _ROW_HEIGHT = 9.0 _FONT = 4.2 +#: 그림이 없는 장에 까닭을 적는 글자 크기(㎜) — 표 글자보다 살짝 작게. +_NOTE_FONT = 4.0 #: 표 사이 간격 — 장이 여럿일 때 서로 붙어 보이지 않게. _GAP = 14.0 #: 그림을 왼쪽 여백에서 조금 들여 놓는다 — 치수 글자가 왼쪽으로 나가기 때문. @@ -181,6 +184,24 @@ def build_standard_drawing(drawing_id: str, label: str, payload: dict[str, Any]) if figure: entities.extend(figure) top -= used + _GAP + else: + # ⚠ **빈 자리를 그냥 두지 않는다** — 사용자가 「고장인가」로 읽는다(2026-09-09 + # 사용자 지시). 왜 안 서는지와 **무엇을 받아야 서는지**를 표 위에 한 줄 적는다. + reason = figure_reason(sheet) + if reason: + entities.append( + _text_entity( + f"{drawing_id}:sheet:{index}:nofigure", + reason.replace("**", ""), + x0, + top - _NOTE_FONT, + STANDARD_LAYER_ID, + _NOTE_FONT, + TABLE_LABEL_COLOR, + "left", + ) + ) + top -= _NOTE_FONT + _GAP grid = _sheet_rows(sheet) entities.append( table_entity( diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py b/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py index 4bfe8988..6d4f3c10 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py @@ -79,11 +79,12 @@ def sheet_key(structure: dict[str, Any]) -> str: def _slope_or_none(structure: dict[str, Any]) -> float | None: """돌쌓기 계열이면 판정된 전면 기울기, 아니면 `None`. 판정은 그림 모듈이 가진 한 벌.""" from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import ( - FIGURE_TYPE_IDS, + SLOPE_TYPE_IDS, slope_of, ) - if str(structure.get("type_id") or "") not in FIGURE_TYPE_IDS: + # ⚠ **그림 대상 목록을 쓰지 않는다** — 큰돌쌓기는 그림은 못 그려도 기울기는 판정된다. + if str(structure.get("type_id") or "") not in SLOPE_TYPE_IDS: return None return slope_of(structure)[0] diff --git a/resources/tester/test_b07_standard_figure_reason.py b/resources/tester/test_b07_standard_figure_reason.py new file mode 100644 index 00000000..047bdd70 --- /dev/null +++ b/resources/tester/test_b07_standard_figure_reason.py @@ -0,0 +1,81 @@ +"""표준도 — 그림이 **안 서는 까닭을 화면에 드러낸다** (2026-09-09 사용자 지시). + +빈 자리를 그냥 두면 사용자가 「고장인가」로 읽는다. 장마다 **왜 그림이 없는지와 무엇을 +받아야 서는지**를 표 위에 한 줄 적는다. + +⚠ **큰돌쌓기를 그림 대상에서 뺐다** — 그림은 두께를 **뒷길이**에서 내는데 큰돌쌓기는 규격이 + **직경**이라 영영 못 그렸다(품셈 13-6 직경 ↔ 13-4 뒷길이). 식은 만들지 않는다 — + 「직경에서 두께를 내는 법」은 도메인 판단이라 사용자 몫이다. +⚠ 그림 대상과 **기울기 판정 대상은 다른 목록**이다 — 한 벌로 묶었더니 큰돌쌓기를 그림에서 + 뺄 때 **장 제목의 「1:0.3」까지 사라졌다**(화면 실측에서 잡음). +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import ( # noqa: E402 + FIGURE_TYPE_IDS, + SLOPE_TYPE_IDS, + figure_reason, +) +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardSheet import ( # noqa: E402 + build_standard_drawing, +) +from B07_DesignDetail.B07_DesignDetail_Engine_Standard_Sheet import sheet_title # noqa: E402 + + +def _sheet(type_id: str, **options) -> dict: + return { + "key": type_id, + "title": type_id, + "type_id": type_id, + "height_m": 2.5, + "options": {"height_m": 2.5, **options}, + "rows": [{"name": "콘크리트", "unit": "㎥", "amount": 1.0, "basis": "시험"}], + "member_count": 1, + "unit_label": "m당", + } + + +def test_뒷길이가_없으면_무엇을_고르면_되는지_적는다() -> None: + reason = figure_reason(_sheet("masonry_wet")) + assert reason is not None and "뒷길이" in reason and "섭니다" in reason + + +def test_뒷길이가_있으면_까닭이_없다() -> None: + assert figure_reason(_sheet("masonry_dry", back_len_cm=35)) is None + + +def test_큰돌쌓기는_그림_대상에서_빠지고_까닭이_적힌다() -> None: + assert "boulder_masonry" not in FIGURE_TYPE_IDS + reason = figure_reason(_sheet("boulder_masonry", stone_cm="60~80")) + assert reason is not None and "직경" in reason + + +def test_기울기_판정은_큰돌쌓기에도_남는다() -> None: + """그림 대상 목록으로 판정하면 장 제목에서 「1:0.3」이 사라진다 — 목록을 갈라 둔 자리.""" + assert "boulder_masonry" in SLOPE_TYPE_IDS + title = sheet_title( + {"name": "큰돌쌓기", "type_id": "boulder_masonry", "height_m": 2.5, "options": {}} + ) + assert "1:0.3" in title + + +def test_까닭이_도면에_한_줄로_실린다() -> None: + drawing = build_standard_drawing( + "standard_sheet_1", + "표준도 1장", + {"sheets": [_sheet("retaining_wall")], "structure_count": 1}, + ) + texts = [ + (entity.get("shapeData") or {}).get("label", "") + for entity in drawing["entities"] + if entity.get("type") == "Text" + ] + assert any("그림 없음" in text for text in texts), texts