From 8cd0b003e5ef198b6e12fcc18a9a789cf15b5e2d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 19:58:15 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(B07):=20=ED=91=9C=EC=A4=80=EB=8F=84=20?= =?UTF-8?q?2=EB=8B=A8=EA=B3=84=20=E2=80=94=20=EB=8F=8C=EC=8C=93=EA=B8=B0?= =?UTF-8?q?=20=EB=8B=A8=EB=A9=B4=20=EA=B7=B8=EB=A6=BC=20+=20=EC=A0=9C?= =?UTF-8?q?=EC=9B=90=20=EC=A1=B0=ED=95=A9=EB=A7=88=EB=8B=A4=20=ED=95=9C=20?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 실무 탭이 「위 치수조서 + 아래 수량산출서」인데 1단계에서 아래 표가 섰고 이번이 위 그림. 그림이 붙으면서 한 면에 넉 장을 쌓을 수 없게 돼 **장 나눔(㉢)도 같이** 했음. 장 나눔 — 계획평면도가 쓰는 본 그대로 `standard_sheet` / `standard_sheet_2` … 제원 조합 하나가 한 장. 좌측 목록은 `idPrefix` 묶음(계획평면도와 같은 짜임)이고 kind 는 새로 `standard`. 구조물이 없어도 한 장은 남김 — 단추가 사라지면 「없어진 것」처럼 보이고 사유를 읽을 자리도 없어짐. 그림 — `B07_DesignDetail_Engine_Cad_StandardFigure.py` ⚠ **기울기를 값으로 받음. 0.3 을 그림에 박지 않았음.** `face_slope_ratio` 가 오면 그 값으로 기울고, 비어 있으면 교본 7-3 기본 0.3 으로 그리되 **「자동 판정 전」이라고 그림에 적음**. 자동 판정이 붙는 대로 그림이 따라감. ⚠ 치수는 수량이 쓰는 그 상수(`STONE_MASONRY`)를 직접 읽음 — 여기서 다시 적으면 그림과 표가 갈림. 상부 0.45+0.10H · 하부 0.45+0.40H · 터파기 폭 평균두께+0.2 · 되메우기 0.2. ⓘ 그 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻함(하부−상부 = 0.30H = 전면이 기우는 양). 기울기가 0.3 이 아니면 그 차이만큼 뒷면이 기욺 — 상수에서 따라 나오는 결과라 그대로 그림. ⚠ 치수가 없는 것은 그리되 치수를 안 적음 — 막자갈은 「입적 − 몸통 − 고임돌」이라 폭·높이로 정의된 도형이 아님. 자리만 보이고 값은 표를 가리킴. 죽은 길 걷어냄 — `BLANK_DRAWINGS` 가 「표준도」 하나뿐이었는데 그것이 내용 있는 장이 되어 빈 도각 경로가 통째로 안 쓰이게 됨. 상수·목록 루프·분기·import 를 지움(700줄 제한도 걸렸음). 자체검증 — 회귀 573 통과 · 0 실패, `tsc --noEmit` 0. ⚠ 병합 뒤 깨진 시험 둘을 고침(내 폴더 `tmp/tests`, 랩탑 메인 `11394d40` 의 여파) · `stone_supply='채집'` 기본값을 확정 ② 근거와 함께 허용 목록에 등록 · 바닥막이 「모든 옵션이 b05」 단정 → 조달이 detail 로 들어와 B05 셋만 보게 좁힘 Co-Authored-By: Claude Opus 5 (1M context) --- .../B07_DesignDetail_Api_Fetch.ts | 2 + ..._DesignDetail_Engine_Cad_StandardFigure.py | 203 ++++++++++++++++++ ...7_DesignDetail_Engine_Cad_StandardSheet.py | 84 +++++++- .../B07_DesignDetail_Router_Support.py | 36 ++-- B07_DesignDetail/B07_DesignDetail_Schema.py | 4 + .../B07_DesignDetail_UI_Panels.ts | 3 +- 6 files changed, 301 insertions(+), 31 deletions(-) create mode 100644 B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py diff --git a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts index 34ab74a7..0c79a83a 100644 --- a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts +++ b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts @@ -15,6 +15,7 @@ export interface DesignDrawingItem { | "landuse" | "plan_lidar" | "cross_standard" + | "standard" | "blank"; label: string; chainage_m: number | null; @@ -100,6 +101,7 @@ export interface DesignDrawingResponse { | "landuse" | "plan_lidar" | "cross_standard" + | "standard" | "blank"; label: string; drawing: CadDrawing; diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py new file mode 100644 index 00000000..4f67dac4 --- /dev/null +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py @@ -0,0 +1,203 @@ +"""표준도 **위쪽 그림** — 돌쌓기 단면(2단계, 2026-09-09). + +실무 원본 탭이 「위 치수조서 + 아래 수량산출서」인데, 아래 표는 1단계에서 섰고 여기가 위다. + +⚠ **기울기를 값으로 받는다 — 0.3 을 박지 않는다.** + 품셈 표준경사 표 자동 판정이 다른 창에서 붙는 중이고, 붙으면 이 그림이 **그 값으로** + 기울어져야 한다. `face_slope_ratio` 가 비면(=자동 미구현·미입력) 교본 7-3 기본 0.3 으로 + 그리되 **그 사실을 그림에 적는다**. + +⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를 + 직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장). + + 상부 두께 = 0.45 + 0.10·H 하부 두께 = 0.45 + 0.40·H + 터파기 폭 = 평균두께 + 0.2 되메우기 두께 = 0.2 + + ⓘ 이 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻한다 — 하부−상부 = 0.30·H 이고 전면이 뒤로 + 기우는 양도 0.3·H 라 딱 맞는다. 기울기가 0.3 이 아니면 그 차이만큼 **뒷면이 기운다**. + 지어낸 것이 아니라 상수에서 따라 나오는 결과라 그대로 그린다. + +⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서 + 몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다. + 없는 치수를 그림에 적으면 **표와 다른 두 번째 정본**이 생긴다. +""" + +from __future__ import annotations + +from typing import Any + +from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( + _line_entity, + _text_entity, + polyline_entity, +) +from B08_Quantity.B08_Quantity_Engine_UnitQuantity import STONE_MASONRY + +#: 그림 축척 — 1/25(1m = 40㎜). 표(줄 높이 9㎜) 위에 얹어도 한 면에 드는 크기다. +SCALE_MM_PER_M = 40.0 + +#: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다. +FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry", "boulder_masonry"}) + +#: 기울기를 못 받았을 때 쓰는 값 — 교본 7-3 「돌 찰쌓기 3.0m 이하 1:0.3」. +DEFAULT_SLOPE_RATIO = 0.3 + +_LABEL_FONT = 4.0 +_DIM_FONT = 3.4 + + +def slope_ratio_of(options: dict[str, Any] | None) -> tuple[float, bool]: + """전면 기울기와 **그것이 입력된 값인지**. 비어 있으면 `(0.3, False)`.""" + raw = (options or {}).get("face_slope_ratio") + if isinstance(raw, (int, float)) and float(raw) > 0: + return float(raw), True + return DEFAULT_SLOPE_RATIO, False + + +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 + ) + lean = slope_ratio * height_m + return [ + (0.0, 0.0), + (lean, height_m), + (lean + top_t, height_m), + (bottom_t, 0.0), + (0.0, 0.0), + ] + + +def build_figure( + drawing_id: str, + sheet: dict[str, Any], + layer_id: str, + origin: tuple[float, float], + line_color: str, + label_color: str, + guide_color: str, +) -> tuple[list[dict[str, Any]], float]: + """장 하나의 그림. `(엔티티, 그림이 차지한 높이 ㎜)` — 못 그리면 `([], 0)`.""" + if str(sheet.get("type_id") or "") not in FIGURE_TYPE_IDS: + return [], 0.0 + height_m = float(sheet.get("height_m") or 0.0) + if height_m <= 0: + return [], 0.0 + + options = sheet.get("options") or {} + slope, slope_given = slope_ratio_of(options) + scale = SCALE_MM_PER_M + ox, oy = origin + + 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 + ) + average_t = (top_t + bottom_t) / 2.0 + dig_width = average_t + STONE_MASONRY["excavation_extra_m"] + backfill_t = STONE_MASONRY["backfill_thickness_m"] + + entities: list[dict[str, Any]] = [] + wall = polyline_entity( + f"{drawing_id}:fig:wall", [mm(p) for p in points], layer_id, line_color, width=2 + ) + if wall is not None: + entities.append(wall) + + # 터파기 — 우리 수량식(평균두께+0.2) × 높이 를 **그대로** 그린다. 점선. + dig = polyline_entity( + f"{drawing_id}:fig:dig", + [mm(p) for p in ((0.0, 0.0), (0.0, height_m), (dig_width, height_m), (dig_width, 0.0))], + layer_id, + guide_color, + dash=[4, 3], + ) + if dig is not None: + entities.append(dig) + + # 되메우기 두께 선 — 밑면에서 0.2m 위. 값이 어디서 오는지 눈에 보이게. + entities.append( + _line_entity( + f"{drawing_id}:fig:backfill", + mm((0.0, backfill_t)), + mm((dig_width, backfill_t)), + layer_id, + guide_color, + ) + ) + + # 물구멍 — 벽을 가로지르는 짧은 선 하나. 개소 간격은 글자로 적는다(면적당이라 그림에 못 씀). + weep_y = height_m * 0.5 + entities.append( + _line_entity( + f"{drawing_id}:fig:weep", + mm((slope * weep_y, weep_y)), + mm((slope * weep_y + top_t, weep_y)), + layer_id, + guide_color, + ) + ) + + labels: list[tuple[str, tuple[float, float], str]] = [ + (f"H = {height_m:g} m", (-0.28, height_m / 2.0), "right"), + (f"상부 {top_t:.2f} m", (slope * height_m + top_t / 2.0, height_m + 0.14), "center"), + (f"하부 {bottom_t:.2f} m", (bottom_t / 2.0, -0.30), "center"), + ( + f"1 : {slope:g}" + ("" if slope_given else " (자동 판정 전 — 교본 기본값)"), + (slope * height_m / 2.0 - 0.30, height_m * 0.72), + "right", + ), + ( + f"터파기 폭 {dig_width:.2f} m (평균두께 {average_t:.2f} + 0.20)", + (dig_width + 0.16, height_m * 0.92), + "left", + ), + (f"되메우기 {backfill_t:g} m", (dig_width + 0.16, backfill_t), "left"), + ( + f"물구멍 — {STONE_MASONRY['weep_hole_area_m2']:g}㎡당 1개소", + (slope * weep_y + top_t + 0.16, weep_y), + "left", + ), + ("막자갈(뒷채움) — 수량은 아래 표", (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")) + + for index, (text, point, align) in enumerate(labels): + x, y = mm(point) + entities.append( + _text_entity( + f"{drawing_id}:fig:label:{index}", + text, + x, + y, + layer_id, + _DIM_FONT, + label_color, + align, + ) + ) + + title_x, title_y = mm((0.0, height_m + 0.52)) + entities.append( + _text_entity( + f"{drawing_id}:fig:title", + f"{sheet.get('title') or '돌쌓기'} (축척 1/{int(1000 / SCALE_MM_PER_M)})", + title_x, + title_y, + layer_id, + _LABEL_FONT, + label_color, + "left", + ) + ) + + used_mm = (height_m + 0.95) * scale + return entities, used_mm diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py index 17ee4a46..08c60564 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py @@ -29,12 +29,25 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( _layer, table_entity, ) +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_Template import usable_bbox logger = logging.getLogger(__name__) STANDARD_LAYER_ID = "standard" +#: 장 하나가 도면 하나 — 계획평면도와 같은 본을 따른다(`plan_lidar` / `plan_lidar_2`). +SHEET_ID_PREFIX = "standard_sheet" + + +def sheet_drawing_id(number: int, total: int) -> str: + """장이 하나면 `standard_sheet`, 여럿이면 `standard_sheet_2` 처럼 번호를 붙인다.""" + return SHEET_ID_PREFIX if total <= 1 and number <= 1 else f"{SHEET_ID_PREFIX}_{number}" + + # 종이 밀리미터. 실무 시트가 「공종 | 산출근거 | 수량 | 단위」 넉 줄이라 그대로 간다. # ⚠ **산출근거 칸을 넓게 잡는다** — 처음 176㎜ 로 냈더니 물구멍관·버림콘크리트처럼 단서가 # 붙은 근거 문구가 칸을 넘어 수량 칸을 덮었다(2026-09-09 실화면). 표는 글자를 안 접는다. @@ -47,6 +60,16 @@ _ROW_HEIGHT = 9.0 _FONT = 4.2 #: 표 사이 간격 — 장이 여럿일 때 서로 붙어 보이지 않게. _GAP = 14.0 +#: 그림을 왼쪽 여백에서 조금 들여 놓는다 — 치수 글자가 왼쪽으로 나가기 때문. +_FIGURE_INSET = 60.0 +#: 그림 제목이 들어갈 윗여백. +_FIGURE_TOP_PAD = 6.0 + + +def _figure_height(sheet: dict[str, Any]) -> float: + """그림이 차지할 높이(㎜) — 자리를 먼저 비워 두려고 미리 잰다.""" + height_m = float(sheet.get("height_m") or 0.0) + return (height_m + 0.95) * FIGURE_SCALE_MM_PER_M if height_m > 0 else 0.0 def _cell( @@ -145,6 +168,19 @@ def build_standard_drawing(drawing_id: str, label: str, payload: dict[str, Any]) top = y1 for index, sheet in enumerate(sheets, start=1): + # 위 그림 — 실무 탭의 「치수조서」 자리. 못 그리는 종류는 표만 선다(2단계). + figure, used = build_figure( + drawing_id, + sheet, + STANDARD_LAYER_ID, + (x0 + _FIGURE_INSET, top - _FIGURE_TOP_PAD - _figure_height(sheet)), + TABLE_LINE_COLOR, + TABLE_LABEL_COLOR, + TABLE_VALUE_COLOR, + ) + if figure: + entities.extend(figure) + top -= used + _GAP grid = _sheet_rows(sheet) entities.append( table_entity( @@ -168,11 +204,9 @@ def build_standard_drawing(drawing_id: str, label: str, payload: dict[str, Any]) } -def standard_drawing_for(project_root: Path, drawing_id: str, label: str) -> dict[str, Any]: - """프로젝트 구조물을 읽어 표준도 한 장을 만든다 — 라우터가 부르는 문. +def standard_payload(project_root: Path) -> dict[str, Any]: + """프로젝트 구조물을 제원 조합으로 묶은 장 목록. 실패해도 **빈 목록**을 낸다. - ⚠ 실패해도 도면은 연다 — 구조물 정본을 못 읽었다고 화면이 비면 사용자는 「고장」으로 - 읽는다. 그때는 빈 표 대신 사유가 적힌 한 줄이 뜬다. ⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어 모듈 맨 위에서 부르면 맞물린다. """ @@ -182,8 +216,42 @@ def standard_drawing_for(project_root: Path, drawing_id: str, label: str) -> dic try: structures, names, _skipped = _collect_structures(str(project_root)) - payload = build_standard_sheets(build_unit_table(structures, names)) + return build_standard_sheets(build_unit_table(structures, names)) except Exception: - logger.exception("B07 표준도 하단표 실패 — 사유만 실어 연다: %s", drawing_id) - payload = {"sheets": [], "structure_count": 0} - return build_standard_drawing(drawing_id, label, payload) + logger.exception("B07 표준도 장 목록 실패 — 빈 목록으로 둔다: %s", project_root) + return {"sheets": [], "structure_count": 0} + + +def sheet_items(project_root: Path) -> list[tuple[str, str]]: + """좌측 목록에 설 `(도면 id, 이름)`. 장이 없으면 **빈 장 하나**를 남긴다. + + 구조물이 없다고 단추가 통째로 사라지면 「없어진 것」처럼 보인다 — 눌러서 사유를 읽게 한다. + """ + sheets = standard_payload(project_root).get("sheets") or [] + if not sheets: + return [(SHEET_ID_PREFIX, "표준도")] + total = len(sheets) + return [ + (sheet_drawing_id(index, total), f"표준도 {index}장 ({sheet.get('title') or ''})".strip()) + for index, sheet in enumerate(sheets, start=1) + ] + + +def standard_drawing_for(project_root: Path, drawing_id: str, label: str) -> dict[str, Any]: + """프로젝트 구조물을 읽어 표준도 한 장을 만든다 — 라우터가 부르는 문. + + ⚠ 실패해도 도면은 연다 — 구조물 정본을 못 읽었다고 화면이 비면 사용자는 「고장」으로 + 읽는다. 그때는 빈 표 대신 사유가 적힌 한 줄이 뜬다. + ⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어 + 모듈 맨 위에서 부르면 맞물린다. + """ + payload = standard_payload(project_root) + sheets = payload.get("sheets") or [] + # 이 도면 id 가 가리키는 장 하나만 남긴다 — 한 면에 한 장(2026-09-09, 그림이 붙으면서). + total = len(sheets) + picked = [ + sheet + for index, sheet in enumerate(sheets, start=1) + if sheet_drawing_id(index, total) == drawing_id + ] + return build_standard_drawing(drawing_id, label, {**payload, "sheets": picked}) diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index b114350d..556b8cdf 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -17,10 +17,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( station_no_label, ) from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Basin import build_watershed_drawing -from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Cover import ( - build_blank_drawing, - build_cover_drawing, -) +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Cover import build_cover_drawing from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Landuse import ( LANDUSE_LABEL, build_landuse_drawing, @@ -47,11 +44,15 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import ( plan_cross_sheets, section_block_size, ) -from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardSheet import standard_drawing_for from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Standard import ( STANDARD_LABEL, build_standard_cross_drawing, ) +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardSheet import ( + SHEET_ID_PREFIX, + sheet_items, + standard_drawing_for, +) from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import ( QUANTITY_VALUE_KEYS, ) @@ -107,11 +108,6 @@ COVER_ID = "cover" # 표준 횡단면도 — 노선 자료가 아니라 B06 표준 횡단면 설정값으로 그리는 한 장. CROSS_STANDARD_ID = "cross_standard" -# 아직 내용을 만들지 않은 도면 — 도각만 실어 연다(2026-09-01 사용자 지시). -# 화면 순서(DRAWING_GROUPS)와 같은 이름을 쓴다. -BLANK_DRAWINGS: tuple[tuple[str, str], ...] = (("blank_standard", "표준도"),) -BLANK_LABELS: dict[str, str] = dict(BLANK_DRAWINGS) - def _drawing_list( project_root: Path, @@ -208,10 +204,10 @@ def _drawing_list( confirmed=bool(manifest_drawings.get(drawing_id, {}).get("confirmed")), ) ) - # 아직 내용을 만들지 않은 도면도 **빈 도각으로 열린다**(2026-09-01 사용자 지시). - # 목록의 절반이 눌리지 않는 회색 버튼이면 고장난 것처럼 보인다. - for drawing_id, label in BLANK_DRAWINGS: - drawings.append(DesignDrawingItem(id=drawing_id, kind="blank", label=label)) + # 표준도 — **제원 조합마다 한 장**이라 장이 나뉜다(2026-09-09). 구조물이 없어도 한 장은 + # 남긴다: 단추가 사라지면 「없어진 것」처럼 보이고 사유를 읽을 자리도 없어진다. + for drawing_id, label in sheet_items(project_root): + drawings.append(DesignDrawingItem(id=drawing_id, kind="standard", label=label)) _store_drawing_numbers(project_root, drawings) return drawings @@ -444,14 +440,10 @@ def _read_drawing( stored_table = manifest_entry.get("quantity_table") table = stored_table if kind == "cross" and isinstance(stored_table, dict) else None return kind, label, saved, True, table - if drawing_id in BLANK_LABELS: - label = BLANK_LABELS[drawing_id] - if drawing_id == "blank_standard": - # 표준도 — 이제 하단표를 싣는다(2026-09-09). 도각은 두르지 않는다(사용자 지시). - drawing = standard_drawing_for(project_root, drawing_id, label) - return "blank", label, drawing, False, None - # 아직 내용을 만들지 않은 도면 — 도각만 실어 연다. 확정 대상이 아니다. - return "blank", label, build_blank_drawing(drawing_id, label), False, None + if drawing_id == SHEET_ID_PREFIX or drawing_id.startswith(f"{SHEET_ID_PREFIX}_"): + # 표준도 — 제원 조합 한 벌이 한 장. 도각은 두르지 않는다(2026-09-08 사용자 지시). + label = dict(sheet_items(project_root)).get(drawing_id, "표준도") + return "standard", label, standard_drawing_for(project_root, drawing_id, label), False, None if drawing_id == COVER_ID: # 표지는 설계 자료를 쓰지 않는다 — 템플릿 한 장이 곧 도면이다. diff --git a/B07_DesignDetail/B07_DesignDetail_Schema.py b/B07_DesignDetail/B07_DesignDetail_Schema.py index c129d9fc..9e3b2f07 100644 --- a/B07_DesignDetail/B07_DesignDetail_Schema.py +++ b/B07_DesignDetail/B07_DesignDetail_Schema.py @@ -20,6 +20,8 @@ class DesignDrawingItem(BaseModel): "landuse", "plan_lidar", "cross_standard", + # standard: 표준도(구조물도) — 제원 조합 하나가 한 장. + "standard", "blank", ] label: str @@ -54,6 +56,8 @@ class DesignDrawingResponse(BaseModel): "landuse", "plan_lidar", "cross_standard", + # standard: 표준도(구조물도) — 제원 조합 하나가 한 장. + "standard", "blank", ] label: str diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts index 05a5d1fd..79d1458f 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts @@ -37,7 +37,8 @@ export const DRAWING_GROUPS: readonly { { label: "횡단면도", kind: "cross" }, { label: "토적도(유토곡선)", kind: "mass_haul" }, { label: "유역도(배수 유역도)", kind: "watershed" }, - { label: "표준도", blankId: "blank_standard" }, + // 표준도는 **제원 조합마다 한 장**이라 장이 나뉜다(2026-09-09) — 계획평면도와 같은 묶음. + { label: "표준도", idPrefix: "standard_sheet" }, { label: "용지도", idPrefix: "landuse" }, ]; From fa7b4e515ca8f8a26b1c7acaa757231c4c1c5fa2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 20:06:11 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(B07):=20=ED=91=9C=EC=A4=80=EB=8F=84=20?= =?UTF-8?q?=EA=B8=B0=EC=9A=B8=EA=B8=B0=EB=A5=BC=20=ED=8C=90=EC=A0=95?= =?UTF-8?q?=EA=B0=92=EC=9C=BC=EB=A1=9C=20=E2=80=94=20=EA=B7=B8=EB=A6=BC?= =?UTF-8?q?=C2=B7=EC=9E=A5=20=EC=A0=9C=EB=AA=A9=EC=9D=B4=20B08=20=ED=95=9C?= =?UTF-8?q?=20=EB=B2=8C=EC=9D=84=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=EB=B6=80?= =?UTF-8?q?=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 데스크탑 메인이 넣은 품셈 표준경사 판정(13-4-4 [주]⑪)이 **근거 문구 안에만** 있었고 표준도는 못 읽고 있었음. 판정 함수 `face_slope_ratio()` 를 그대로 부르게 바꿈 — 판정을 두 벌로 짜면 그림과 수량이 갈림(CLAUDE.md 5장). 실측 (프로젝트 936be972 구조물 5기) 돌쌓기(찰) H=2.5 1:0.3 뒷길이 45㎝ 깬돌 단면 (0,0)(0.75,2.5)(1.45,2.5)(1.45,0) 돌쌓기(메) H=2.0 1:0.35 뒷길이 35㎝ 야면석 단면 (0,0)(0.70,2.0)(1.35,2.0)(1.25,0) 큰돌쌓기 H=2.5 1:0.3 옹벽 판정 대상 아님 — 안 적음 ⇒ **메쌓기가 찰쌓기와 다른 값으로 갈림**(0.35 대 0.3). 장 제목에 그대로 뜸. 메/찰은 종류에서 옴. 큰돌쌓기는 `bond` 칸이 그것이고 안 고르면 찰쌓기로 봄 — 품셈 13-6 은 둘 다 「1:0.3 이상」이라 그림 기울기가 갈리지 않음. 기울기 근거 문구를 그림 아래 한 줄로 실음 — 「사용자 지정」인지 「품셈 표준경사」인지, 그리고 「성토/절토가 저장에 없어 성토 열로 잠정」인지가 도면에서 보여야 함. ⚠ 시험 제원에서 `face_slope_ratio` 를 뺐음 — 실제 구조물 제원에 그 칸이 없는데 시험만 갖고 있어 **판정 경로가 한 번도 안 돌았음**(늘 「사용자 지정」으로 빠짐). 빼고 나니 메/찰이 갈리는 것이 시험으로 잡힘. 사용자 지정이 이기는 것도 따로 검사함. 자체검증 — 회귀 573 통과 · 0 실패. Co-Authored-By: Claude Opus 5 (1M context) --- ..._DesignDetail_Engine_Cad_StandardFigure.py | 56 ++++++++++++------- .../B07_DesignDetail_Engine_Standard_Sheet.py | 18 +++++- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py index 4f67dac4..30b56f71 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py @@ -3,9 +3,10 @@ 실무 원본 탭이 「위 치수조서 + 아래 수량산출서」인데, 아래 표는 1단계에서 섰고 여기가 위다. ⚠ **기울기를 값으로 받는다 — 0.3 을 박지 않는다.** - 품셈 표준경사 표 자동 판정이 다른 창에서 붙는 중이고, 붙으면 이 그림이 **그 값으로** - 기울어져야 한다. `face_slope_ratio` 가 비면(=자동 미구현·미입력) 교본 7-3 기본 0.3 으로 - 그리되 **그 사실을 그림에 적는다**. + 판정은 **B08 `face_slope_ratio()` 한 벌**을 그대로 부른다(품셈 13-4-4 [주]⑪ 표준경사 — + 직고·메찰·성절토). 사용자가 정했으면 그 값이 이긴다(확정 ⑨). 그림은 **수량이 쓴 바로 + 그 값**으로 기울고, 근거 문구도 같이 받아 그림에 적는다 — 판정을 두 벌로 짜면 그림과 + 수량이 갈린다. ⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를 직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장). @@ -31,7 +32,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( _text_entity, polyline_entity, ) -from B08_Quantity.B08_Quantity_Engine_UnitQuantity import STONE_MASONRY +from B08_Quantity.B08_Quantity_Engine_UnitQuantity import STONE_MASONRY, face_slope_ratio #: 그림 축척 — 1/25(1m = 40㎜). 표(줄 높이 9㎜) 위에 얹어도 한 면에 드는 크기다. SCALE_MM_PER_M = 40.0 @@ -39,19 +40,25 @@ SCALE_MM_PER_M = 40.0 #: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다. FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry", "boulder_masonry"}) -#: 기울기를 못 받았을 때 쓰는 값 — 교본 7-3 「돌 찰쌓기 3.0m 이하 1:0.3」. -DEFAULT_SLOPE_RATIO = 0.3 - _LABEL_FONT = 4.0 _DIM_FONT = 3.4 -def slope_ratio_of(options: dict[str, Any] | None) -> tuple[float, bool]: - """전면 기울기와 **그것이 입력된 값인지**. 비어 있으면 `(0.3, False)`.""" - raw = (options or {}).get("face_slope_ratio") - if isinstance(raw, (int, float)) and float(raw) > 0: - return float(raw), True - return DEFAULT_SLOPE_RATIO, False +def slope_of(sheet: dict[str, Any]) -> tuple[float, str]: + """장 하나의 전면 기울기와 근거 문구 — **판정은 B08 한 벌**을 그대로 쓴다. + + 메/찰은 종류에서 온다. 큰돌쌓기는 `bond` 칸(메쌓기/찰쌓기)이 그것이고, 안 고르면 + 찰쌓기로 본다 — 품셈 13-6 은 둘 다 「1:0.3 **이상**」이라 그림 기울기가 갈리지 않는다. + """ + options = sheet.get("options") or {} + type_id = str(sheet.get("type_id") or "") + if type_id == "masonry_dry": + wet = False + elif type_id == "boulder_masonry": + wet = options.get("bond") != "메쌓기" + else: + wet = True + return face_slope_ratio(options, wet=wet, height_m=float(sheet.get("height_m") or 0.0)) def section_points(height_m: float, slope_ratio: float) -> list[tuple[float, float]]: @@ -87,7 +94,7 @@ def build_figure( return [], 0.0 options = sheet.get("options") or {} - slope, slope_given = slope_ratio_of(options) + slope, slope_note = slope_of(sheet) scale = SCALE_MM_PER_M ox, oy = origin @@ -148,11 +155,7 @@ def build_figure( (f"H = {height_m:g} m", (-0.28, height_m / 2.0), "right"), (f"상부 {top_t:.2f} m", (slope * height_m + top_t / 2.0, height_m + 0.14), "center"), (f"하부 {bottom_t:.2f} m", (bottom_t / 2.0, -0.30), "center"), - ( - f"1 : {slope:g}" + ("" if slope_given else " (자동 판정 전 — 교본 기본값)"), - (slope * height_m / 2.0 - 0.30, height_m * 0.72), - "right", - ), + (f"1 : {slope:g}", (slope * height_m / 2.0 - 0.30, height_m * 0.72), "right"), ( f"터파기 폭 {dig_width:.2f} m (평균두께 {average_t:.2f} + 0.20)", (dig_width + 0.16, height_m * 0.92), @@ -185,6 +188,21 @@ def build_figure( ) ) + # 기울기 근거는 **그림 밑에** 한 줄 — 「사용자 지정」인지 「품셈 표준경사」인지 보여야 한다. + note_x, note_y = mm((0.0, -0.62)) + entities.append( + _text_entity( + f"{drawing_id}:fig:slopenote", + f"전면 기울기 — {slope_note}", + note_x, + note_y, + layer_id, + _DIM_FONT, + label_color, + "left", + ) + ) + title_x, title_y = mm((0.0, height_m + 0.52)) entities.append( _text_entity( diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py b/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py index aabf0165..61fc58da 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py @@ -76,6 +76,18 @@ def sheet_key(structure: dict[str, Any]) -> str: return json.dumps(payload, ensure_ascii=False, sort_keys=True) +def _slope_or_none(structure: dict[str, Any]) -> float | None: + """돌쌓기 계열이면 판정된 전면 기울기, 아니면 `None`. 판정은 그림 모듈이 가진 한 벌.""" + from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import ( + FIGURE_TYPE_IDS, + slope_of, + ) + + if str(structure.get("type_id") or "") not in FIGURE_TYPE_IDS: + return None + return slope_of(structure)[0] + + def sheet_title(structure: dict[str, Any]) -> str: """장 제목 — 「이름 H=2.0 1:0.3 뒷길이 45㎝」처럼 **무엇이 갈랐는지**가 보이게.""" parts = [str(structure.get("name") or structure.get("type_id") or "구조물")] @@ -83,9 +95,11 @@ def sheet_title(structure: dict[str, Any]) -> str: if height > 0: parts.append(f"H={height:g}") options = structure.get("options") or {} - slope = options.get("face_slope_ratio") + # 기울기는 **판정 결과**를 적는다 — 사용자가 안 정해도 품셈 표준경사로 갈린다(확정 ⑨). + # 판정이 안 되는 종류(옹벽 등)는 안 적는다. + slope = _slope_or_none(structure) if slope is not None: - parts.append(f"1:{float(slope):g}") + parts.append(f"1:{slope:g}") back = options.get("back_len_cm") or options.get("stone_back_length_cm") if back: parts.append(f"뒷길이 {int(back)}㎝") From 4280a6d821f9b114707f07edf683d0cce4328767 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 20:09:56 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore(B07):=20=EC=95=84=EB=AC=B4=EB=8F=84?= =?UTF-8?q?=20=EC=95=88=20=EC=93=B0=EB=8A=94=20=EB=B9=88-=EB=8F=84?= =?UTF-8?q?=EA=B0=81=20=EB=AA=A9=EB=A1=9D=20=EC=82=AD=EC=A0=9C=20=E2=80=94?= =?UTF-8?q?=20=EB=8D=AB=EB=A7=8C=20=EB=82=A8=EC=95=84=20=EC=9E=88=EC=97=88?= =?UTF-8?q?=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Router_Support_Io.BLANK_DRAWINGS` 일곱 줄(계획평면도 3종·라이다·표준 횡단면도·표준도· 용지도)이 **어느 모듈도 import 하지 않는 죽은 목록**이었음. 일곱 전부 실제 도면이 되면서 차례로 빠졌고 표준도가 마지막이었음(2026-09-09). 남겨 두면 「계획평면도가 빈 도각으로 열리나?」로 읽히는 덫임. 확인 — 실제 파일로 도면 목록을 만들어 봄(프로젝트 936be972) 도면 36장 · kind 별 longitudinal 3 · cross 20 · plan 3 · plan_lidar 1 · landuse 1 · cover 1 · cross_standard 1 · mass_haul 1 · watershed 1 · **standard 4** **blank 0** — 빈-도각 경로를 타는 도면이 하나도 없음. 표준도 넉 장 제목에 판정된 기울기가 뜸(메 1:0.35 · 찰 1:0.3). `build_blank_drawing` 자체는 남김 — 도각 표제란 시험이 쓰고, 다음에 또 「목록엔 있는데 아직 안 그리는」 도면이 생기면 그 함수가 쓰임. 자체검증 — 회귀 573 통과 · 0 실패. Co-Authored-By: Claude Opus 5 (1M context) --- .../B07_DesignDetail_Router_Support_Io.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py b/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py index b4ebb452..ad9630c9 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support_Io.py @@ -23,18 +23,9 @@ MASS_HAUL_ID = "mass_haul" WATERSHED_ID = "watershed" COVER_ID = "cover" -# 아직 내용을 만들지 않은 도면 — 도각만 실어 연다(2026-09-01 사용자 지시). -# 화면 순서(DRAWING_GROUPS)와 같은 이름을 쓴다. -BLANK_DRAWINGS: tuple[tuple[str, str], ...] = ( - ("blank_plan_terrain", "계획평면도(지형)"), - ("blank_plan_route", "계획평면도(노선배치도)"), - ("blank_plan_layout", "계획평면도(배치도)"), - ("blank_plan_lidar", "계획평면도(라이다)"), - ("blank_cross_standard", "표준 횡단면도"), - ("blank_standard", "표준도"), - ("blank_landuse", "용지도"), -) -BLANK_LABELS: dict[str, str] = dict(BLANK_DRAWINGS) +# ⚠ 빈-도각 목록이 여기 있었다 — 계획평면도·표준 횡단면도·용지도·표준도 일곱 줄. +# 2026-09-09 지웠다: 일곱 **전부 실제 도면이 되어** 아무도 이 목록을 import 하지 않았다 +# (표준도가 마지막이었다). 남겨 두면 「계획평면도가 빈 도각으로 열리나?」로 읽히는 덫이다. def _read_json(path: Path) -> dict[str, Any]: