From 1850fb2e324c5f44bafcbcfef0906c8e4947f26f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:02:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(B07):=20=ED=9A=A1=EB=8B=A8=EB=8F=84=20?= =?UTF-8?q?=EC=88=98=EB=9F=89=ED=91=9C=20=EB=B0=94=EB=A1=9C=20=EC=9C=84=20?= =?UTF-8?q?B06=20[=EC=83=81=EC=84=B8]=20=EA=B8=80=20=EC=97=AC=EB=9F=AC=20?= =?UTF-8?q?=EC=A4=84=20=C2=B7=20=EC=B9=B8=20=EC=9D=B4=EB=A6=84=20=E3=80=8C?= =?UTF-8?q?=EB=8F=84=EB=A9=B4=20=ED=91=9C=20=EC=9C=84=20=EA=B8=80=E3=80=8D?= =?UTF-8?q?(150-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU --- B06_Section/B06_Section_UI_Cross_Detail.ts | 5 +- .../B07_DesignDetail_Engine_Cad.py | 7 ++ .../B07_DesignDetail_Engine_Cad_Sheet.py | 4 +- .../B07_DesignDetail_Engine_Cad_Table.py | 39 ++++++++ resources/tester/test_150_b07_cross_note.py | 91 +++++++++++++++++++ 5 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 resources/tester/test_150_b07_cross_note.py diff --git a/B06_Section/B06_Section_UI_Cross_Detail.ts b/B06_Section/B06_Section_UI_Cross_Detail.ts index 49de677c9..25e6c2f6f 100644 --- a/B06_Section/B06_Section_UI_Cross_Detail.ts +++ b/B06_Section/B06_Section_UI_Cross_Detail.ts @@ -2,7 +2,8 @@ * B06_Section_UI_Cross_Detail.ts * 횡단 카드 측구 줄 맨 오른쪽 [상세] → 모달(138-4). * - * 맨 위 = 측점 구조물 글(`design.structure_note` · 세션 초안 → [저장]이 patch 로 내보냄) · + * 맨 위 = 도면 표 위 글(`design.structure_note` · 세션 초안 → [저장]이 patch 로 내보냄 · + * B07 횡단도 수량표 바로 위에 줄마다 찍힘 — `_Engine_Cad_Table.cross_note_lines`) · * 가운데 = 카드 그림 복제(어두운 바탕 · CAD 꼴) · 아래 = B07 횡단 수량표(값 · 칸 배치 모두 * 서버 `cross-quantity-table` 한 길 · 캐시 설계로 · 열 때 부름). * 막은 투명(누름 통과) · 자리는 그 카드를 안 가리는 넓은 쪽(좌/우 → 위/아래). @@ -179,7 +180,7 @@ export function openCrossDetail(section: CrossSection): void { const note = document.createElement("textarea"); note.className = "ui-input b06-detail__note"; note.rows = 3; - note.placeholder = "구조물 메모 — 이 측점에 남길 글"; + note.placeholder = "도면 표 위 글 — B07 횡단도 수량표 바로 위에 줄마다 찍힘"; note.value = noteOf(section); note.addEventListener("input", () => writeNote(state.section, note.value)); const cad = document.createElement("div"); diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py index 52f2c246e..e8f696edb 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py @@ -518,7 +518,10 @@ def build_cross_drawing( # 표 모듈은 이 모듈의 직렬화 헬퍼를 쓰므로 순환을 피해 함수 안에서 들여온다. from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import ( + _cross_note_entities, _cross_table_entities, + cross_note_height, + cross_note_lines, cross_table_height, cross_table_width, ) @@ -561,6 +564,10 @@ def build_cross_drawing( table_bottom = oy - half_height if quantity_table is not None: table_top = oy - half_height - 8.0 + # 표 위 글(150-3) — 줄 수만큼 표를 내림 + note_lines = cross_note_lines(design) + entities.extend(_cross_note_entities(drawing_id, note_lines, table_top, center_x)) + table_top -= cross_note_height(note_lines) entities.extend( _cross_table_entities(drawing_id, quantity_table, table_top, title_label, center_x) ) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Sheet.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Sheet.py index 6155e953e..3758fbd04 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Sheet.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Sheet.py @@ -26,6 +26,8 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( points_from_samples, ) from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import ( + cross_note_height, + cross_note_lines, cross_table_height, cross_table_width, ) @@ -90,7 +92,7 @@ def section_block_size( top = half_height + _BORDER_PAD_MM below = half_height + _BORDER_PAD_MM if with_table: - below += 8.0 + cross_table_height() + below += 8.0 + cross_note_height(cross_note_lines(design_spec)) + cross_table_height() return (width, top + below) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Table.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Table.py index c8e605c58..b92b8f5c3 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Table.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Table.py @@ -14,6 +14,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( TABLE_LINE_COLOR, TABLE_VALUE_COLOR, _format, + _text_entity, table_entity, ) @@ -318,6 +319,44 @@ def extract_quantity_table( return table +# 표 위 글(150-3) — B06 [상세] 글(`design.structure_note`)을 줄마다 표 왼쪽 맞춤으로 찍음. +_CROSS_NOTE_LINE_HEIGHT = 3.0 + + +def cross_note_lines(design: dict[str, Any] | None) -> list[str]: + """설계의 표 위 글 → 줄 목록(앞뒤 빈 줄 · 줄 끝 공백 지움 · 없으면 빈 목록).""" + note = design.get("structure_note") if isinstance(design, dict) else None + if not isinstance(note, str): + return [] + return [line.rstrip() for line in note.strip().splitlines()] + + +def cross_note_height(lines: list[str]) -> float: + """표 위 글 높이(mm) — 줄 수 × 줄 높이.""" + return _CROSS_NOTE_LINE_HEIGHT * len(lines) + + +def _cross_note_entities( + drawing_id: str, lines: list[str], top: float, center_x: float = 0.0 +) -> list[dict[str, Any]]: + """표 위 글 줄마다 Text — 표 왼쪽 끝 맞춤 · 위에서 아래로(id `{도면}:note:{n}`).""" + left = center_x - _CROSS_TABLE_WIDTH / 2.0 + return [ + _text_entity( + f"{drawing_id}:note:{index}", + line, + left, + top - _CROSS_NOTE_LINE_HEIGHT * (index + 0.5), + CROSS_TABLE_LAYER_ID, + _CROSS_TABLE_FONT, + TABLE_LABEL_COLOR, + align="left", + ) + for index, line in enumerate(lines) + if line + ] + + def cross_table_width() -> float: """수량 산출표 가로 폭(mm).""" return _CROSS_TABLE_WIDTH diff --git a/resources/tester/test_150_b07_cross_note.py b/resources/tester/test_150_b07_cross_note.py new file mode 100644 index 000000000..1e7b3fb80 --- /dev/null +++ b/resources/tester/test_150_b07_cross_note.py @@ -0,0 +1,91 @@ +"""B07 횡단도 — B06 [상세] 글(`design.structure_note`)이 수량표 바로 위 여러 줄로 찍힘(150-3). + +글 줄마다 Text(표 왼쪽 맞춤 · 표 윗변 아래로 줄 수만큼 내림) · 블록 높이도 같은 만큼 · 글 없으면 그대로. +""" + +import pytest + +from B07_DesignDetail.B07_DesignDetail_Engine_Cad import FRAME_LAYER_ID, build_cross_drawing +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import section_block_size +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import ( + cross_note_height, + cross_note_lines, +) +from B07_DesignDetail.B07_DesignDetail_Engine_Template import entities_bbox + +_SOURCE = { + "chainage_m": 20.0, + "samples": [ + {"valid": True, "offset_m": float(x), "elevation_m": 100.0 + 0.3 * x} + for x in range(-10, 11) + ], +} +_TABLE = {"ground": 100.0, "planned": 99.5} + + +def _draw(design: dict | None) -> dict: + return build_cross_drawing(_SOURCE, "cross_00020m", None, design, _TABLE, "No.1") + + +def _table(drawing: dict) -> dict: + return next(e for e in drawing["entities"] if e["type"] == "Table") + + +def _frame_height(drawing: dict) -> float: + frame = [e for e in drawing["entities"] if e.get("layerId") == FRAME_LAYER_ID] + _x0, y0, _x1, y1 = entities_bbox(frame) + return y1 - y0 + + +def test_글_줄_나눔() -> None: + assert cross_note_lines(None) == [] + assert cross_note_lines({"structure_note": " "}) == [] + assert cross_note_lines({"structure_note": "\nL형 측구 \n\n집수정 보강\n"}) == [ + "L형 측구", + "", + "집수정 보강", + ] + + +def test_글_없으면_그대로() -> None: + plain = _draw(None) + empty = _draw({"structure_note": ""}) + assert _table(plain)["shapeData"] == _table(empty)["shapeData"] + assert not [e for e in empty["entities"] if e["type"] == "Text"] + + +def test_글_두_줄은_표_위_Text_둘_표는_내려감() -> None: + plain = _draw(None) + noted = _draw({"structure_note": "L형 측구\n집수정 보강"}) + texts = [e for e in noted["entities"] if e["type"] == "Text"] + assert [t["shapeData"]["label"] for t in texts] == ["L형 측구", "집수정 보강"] + + plain_top = _table(plain)["shapeData"]["origin"]["y"] + noted_top = _table(noted)["shapeData"]["origin"]["y"] + drop = cross_note_height(["a", "b"]) + assert plain_top - noted_top == pytest.approx(drop) + + # 표 왼쪽 맞춤 · 위에서 아래로 · 모두 표 윗변과 옛 윗변 사이 + left = _table(noted)["shapeData"]["origin"]["x"] + for text in texts: + base = text["shapeData"]["basePoint"] + assert base["x"] == pytest.approx(left) + assert text["shapeData"]["options"]["textAlign"] == "left" + assert noted_top < base["y"] < plain_top + assert texts[0]["shapeData"]["basePoint"]["y"] > texts[1]["shapeData"]["basePoint"]["y"] + + # 블록(테두리) 높이 = 글 높이만큼 커짐 · 블록 크기 계산도 같은 규칙 + assert _frame_height(noted) - _frame_height(plain) == pytest.approx(drop) + _w, plain_h = section_block_size(_SOURCE, None) + _w, noted_h = section_block_size( + _SOURCE, None, design={"structure_note": "L형 측구\n집수정 보강"} + ) + assert noted_h - plain_h == pytest.approx(drop) + + +def test_빈_줄은_자리만() -> None: + noted = _draw({"structure_note": "윗줄\n\n아랫줄"}) + texts = [e for e in noted["entities"] if e["type"] == "Text"] + assert [t["shapeData"]["label"] for t in texts] == ["윗줄", "아랫줄"] + gap = texts[0]["shapeData"]["basePoint"]["y"] - texts[1]["shapeData"]["basePoint"]["y"] + assert gap == pytest.approx(cross_note_height(["a", "b"]))