feat(B07): 횡단도 수량표 바로 위 B06 [상세] 글 여러 줄 · 칸 이름 「도면 표 위 글」(150-3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
This commit is contained in:
2026-10-09 19:02:11 +09:00
co-authored by Claude Opus 5.5
parent 7e874b876a
commit 1850fb2e32
5 changed files with 143 additions and 3 deletions
+3 -2
View File
@@ -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");
@@ -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)
)
@@ -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)
@@ -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
@@ -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"]))