Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -1,290 +0,0 @@
|
||||
"""표준도 **위쪽 그림** — 돌쌓기 단면(2단계, 2026-09-09).
|
||||
|
||||
실무 원본 탭이 「위 치수조서 + 아래 수량산출서」인데, 아래 표는 1단계에서 섰고 여기가 위다.
|
||||
|
||||
⚠ **기울기를 값으로 받는다 — 0.3 을 박지 않는다.**
|
||||
판정은 **B08 `face_slope_ratio()` 한 벌**을 그대로 부른다(품셈 13-4-4 [주]⑪ 표준경사 —
|
||||
직고·메찰·성절토). 사용자가 정했으면 그 값이 이긴다(확정 ⑨). 그림은 **수량이 쓴 바로
|
||||
그 값**으로 기울고, 근거 문구도 같이 받아 그림에 적는다 — 판정을 두 벌로 짜면 그림과
|
||||
수량이 갈린다.
|
||||
|
||||
⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를
|
||||
직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장).
|
||||
|
||||
상부 두께 = 뒷길이 + 0.30 하부 두께 = 상부 + 0.30 × (H − 1.0)
|
||||
터파기 폭 = 평균두께 + 0.2 기초 0.5×0.9 (기초유) · 0.1×0.7 (기초버림)
|
||||
|
||||
⚠ 터파기 치수는 **`common_util_excavation` 한 벌**을 읽는다 — 횡단도가 쓰는 그 상수다.
|
||||
여기서 다시 적으면 두 도면이 다른 터파기를 그린다.
|
||||
|
||||
⚠ **뒷길이가 두께에 들어간다**(확정 2차 ②, 실무 구조물도 식). 뒷길이가 다르면 벽이
|
||||
두꺼워지고 그림도 그만큼 넓어진다 — 예전 식(0.45+0.10H / 0.45+0.40H)은 뒷길이를
|
||||
아예 안 봐서 35㎝ 든 45㎝ 든 같은 그림이 나왔다.
|
||||
⚠ 뒷길이를 안 정한 장은 두께를 못 낸다 — 그때는 **그리지 않는다**(0 으로 때우면
|
||||
거짓 그림이 된다).
|
||||
|
||||
⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서
|
||||
몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다.
|
||||
없는 치수를 그림에 적으면 **표와 다른 두 번째 정본**이 생긴다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_line_entity,
|
||||
_text_entity,
|
||||
polyline_entity,
|
||||
)
|
||||
|
||||
# 기울기 판정은 구조물도 엔진이 가진 한 벌 — 2026-09-13 B08 로 이관(PLAN 3장). 받아 쓰기만 함.
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureSheet import ( # noqa: F401
|
||||
SLOPE_TYPE_IDS,
|
||||
slope_of,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import (
|
||||
STONE_MASONRY,
|
||||
weep_hole_spec,
|
||||
)
|
||||
from common_util.common_util_excavation import (
|
||||
WALL_BLINDING_DEPTH_M,
|
||||
WALL_BLINDING_WIDTH_M,
|
||||
WALL_FOUNDATION_DEPTH_M,
|
||||
WALL_FOUNDATION_WIDTH_M,
|
||||
WALL_TRENCH_CLEARANCE_M,
|
||||
)
|
||||
|
||||
#: 그림 축척 — 1/25(1m = 40㎜). 표(줄 높이 9㎜) 위에 얹어도 한 면에 드는 크기다.
|
||||
SCALE_MM_PER_M = 40.0
|
||||
|
||||
#: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다.
|
||||
#: ⚠ **큰돌쌓기(`boulder_masonry`)를 뺐다**(2026-09-09). 여기 있었지만 그림이 두께를
|
||||
#: **뒷길이**에서 내는데 큰돌쌓기는 규격 축이 **직경**(`stone_cm`)이라 **영영 못 그렸다** —
|
||||
#: 목록에만 있고 결과는 늘 빈 그림이었다. 품셈 **13-6(직경) ↔ 13-4(뒷길이)** 로 축이
|
||||
#: 갈리는 자리이고 수량 쪽에서 이미 갈라 둔 그것이다(`EXPANDERS` 주석).
|
||||
#: ⇒ 「직경에서 두께를 내는 식」은 **도메인 판단이라 사용자 몫** — 계획서 4-12 답 대기 줄.
|
||||
FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry"})
|
||||
|
||||
#: 그림이 못 서는 장에 **까닭을 적는다** — 빈 자리를 그냥 두면 사용자가 「고장」으로 읽는다
|
||||
#: (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
|
||||
|
||||
|
||||
def wall_thickness(height_m: float, back_cm: float) -> tuple[float, float]:
|
||||
"""(상부, 하부) 두께 — **수량이 쓰는 그 식**(`stone_masonry`)과 같은 상수를 읽는다."""
|
||||
top_t = back_cm / 100.0 + STONE_MASONRY["thickness_top_add_m"]
|
||||
bottom_t = top_t + STONE_MASONRY["thickness_slope_per_m"] * max(
|
||||
height_m - STONE_MASONRY["thickness_height_base_m"], 0.0
|
||||
)
|
||||
return top_t, bottom_t
|
||||
|
||||
|
||||
def back_length_cm(sheet: dict[str, Any]) -> float | None:
|
||||
"""장의 뒷길이(㎝). 안 정했으면 `None` — 그때는 두께를 못 내므로 그리지 않는다."""
|
||||
options = sheet.get("options") or {}
|
||||
raw = options.get("back_len_cm") or options.get("stone_back_length_cm")
|
||||
try:
|
||||
return float(raw) if raw not in (None, "") else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def section_points(
|
||||
height_m: float, slope_ratio: float, back_cm: float
|
||||
) -> list[tuple[float, float]]:
|
||||
"""벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다."""
|
||||
top_t, bottom_t = wall_thickness(height_m, back_cm)
|
||||
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
|
||||
|
||||
back_cm = back_length_cm(sheet)
|
||||
if back_cm is None:
|
||||
# 뒷길이가 없으면 두께를 못 낸다 — 0 으로 때우지 않고 그리지 않는다.
|
||||
return [], 0.0
|
||||
slope, slope_note = slope_of(sheet)
|
||||
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, back_cm)
|
||||
top_t, bottom_t = wall_thickness(height_m, back_cm)
|
||||
average_t = (top_t + bottom_t) / 2.0
|
||||
dig_width = average_t + WALL_TRENCH_CLEARANCE_M
|
||||
# 기초 몫 — 「기초유 / 기초버림」이 폭·깊이를 가른다(정본 탭 제목).
|
||||
foundation = str((sheet.get("options") or {}).get("foundation") or "")
|
||||
if foundation == "기초유":
|
||||
base_w, base_d = WALL_FOUNDATION_WIDTH_M, WALL_FOUNDATION_DEPTH_M
|
||||
elif foundation == "기초버림":
|
||||
base_w, base_d = WALL_BLINDING_WIDTH_M, WALL_BLINDING_DEPTH_M
|
||||
else:
|
||||
base_w = base_d = 0.0 # 안 정한 장은 기초를 안 그린다 — 지어내지 않는다.
|
||||
|
||||
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)
|
||||
|
||||
# 기초 — 벽 밑에 놓이는 칸. 「안 정함」이면 안 그린다.
|
||||
if base_d > 0:
|
||||
base = polyline_entity(
|
||||
f"{drawing_id}:fig:base",
|
||||
[mm(p) for p in ((0.0, 0.0), (0.0, -base_d), (base_w, -base_d), (base_w, 0.0))],
|
||||
layer_id,
|
||||
guide_color,
|
||||
)
|
||||
if base is not None:
|
||||
entities.append(base)
|
||||
|
||||
# 물구멍 — 벽을 가로지르는 짧은 선 하나. 개소 간격은 글자로 적는다(면적당이라 그림에 못 씀).
|
||||
# ⚠ 값은 **그 장의 제원**에서 읽는다 — 수량표와 같은 함수를 쓴다(2026-09-13). 기본 상수를
|
||||
# 적으면 사용자가 지름·면적을 고쳐도 그림 주석만 옛 값으로 남아 표와 갈린다.
|
||||
weep_area_m2, weep_diameter_mm, _weep_basis = weep_hole_spec(sheet.get("options") or {})
|
||||
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}", (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"기초 {base_w:g} × {base_d:g} m ({foundation})"
|
||||
if base_d > 0
|
||||
else "기초 — 안 정함(기초유/기초버림)",
|
||||
(dig_width + 0.16, -base_d / 2.0 if base_d else 0.0),
|
||||
"left",
|
||||
),
|
||||
(
|
||||
f"물구멍 Ø{weep_diameter_mm} — {weep_area_m2:g}㎡당 1개소",
|
||||
(slope * weep_y + top_t + 0.16, weep_y),
|
||||
"left",
|
||||
),
|
||||
("막자갈(뒷채움) — 수량은 아래 표", (dig_width + 0.16, height_m * 0.55), "left"),
|
||||
]
|
||||
labels.append((f"뒷길이 ℓ₃ = {int(back_cm)}㎝", (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,
|
||||
)
|
||||
)
|
||||
|
||||
# 기울기 근거는 **그림 밑에** 한 줄 — 「사용자 지정」인지 「품셈 표준경사」인지 보여야 한다.
|
||||
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(
|
||||
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 + base_d) * scale
|
||||
return entities, used_mm
|
||||
@@ -23,19 +23,21 @@ 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,
|
||||
_format,
|
||||
_layer,
|
||||
_text_entity,
|
||||
polyline_entity,
|
||||
table_entity,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import (
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import usable_bbox
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureFigure import (
|
||||
SCALE_MM_PER_M as FIGURE_SCALE_MM_PER_M,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import build_figure, figure_reason
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import usable_bbox
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureFigure import build_figure as figure_shapes
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureFigure import figure_reason
|
||||
|
||||
STANDARD_LAYER_ID = "standard"
|
||||
|
||||
@@ -66,6 +68,44 @@ def _figure_height(sheet: dict[str, Any]) -> float:
|
||||
return (height_m + 0.95) * FIGURE_SCALE_MM_PER_M if height_m > 0 else 0.0
|
||||
|
||||
|
||||
def build_figure(
|
||||
drawing_id: str, sheet: dict[str, Any], origin: tuple[float, float]
|
||||
) -> tuple[list[dict[str, Any]], float]:
|
||||
"""B08 그림 모양 목록(m)을 CAD 엔티티(종이 ㎜)로. `(엔티티, 높이 ㎜)` — 못 그리면 `([], 0)`."""
|
||||
shapes = figure_shapes(sheet) or []
|
||||
ox, oy = origin
|
||||
scale = FIGURE_SCALE_MM_PER_M
|
||||
entities: list[dict[str, Any]] = []
|
||||
for index, shape in enumerate(shapes):
|
||||
x, y = (shape.get("at") or [0.0, 0.0])[:2]
|
||||
if shape["kind"] == "text":
|
||||
entities.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:fig:{index}",
|
||||
shape["text"],
|
||||
ox + x * scale,
|
||||
oy + y * scale,
|
||||
STANDARD_LAYER_ID,
|
||||
shape["size"] * scale,
|
||||
TABLE_LABEL_COLOR,
|
||||
shape["align"],
|
||||
)
|
||||
)
|
||||
continue
|
||||
poly = polyline_entity(
|
||||
f"{drawing_id}:fig:{index}",
|
||||
[(ox + px * scale, oy + py * scale) for px, py in shape["points"]],
|
||||
STANDARD_LAYER_ID,
|
||||
TABLE_LINE_COLOR if shape["role"] == "wall" else TABLE_VALUE_COLOR,
|
||||
dash=[4, 3] if shape["dash"] else None,
|
||||
width=2 if shape["role"] == "wall" else 1,
|
||||
)
|
||||
if poly is not None:
|
||||
entities.append(poly)
|
||||
ys = [point[1] for shape in shapes for point in shape.get("points") or [shape["at"]]]
|
||||
return entities, ((max(ys) - min(ys)) * scale if ys else 0.0)
|
||||
|
||||
|
||||
def _cell(
|
||||
text: str, color: str, *, align: str = "left", col_span: int = 1, bold: bool = False
|
||||
) -> dict[str, Any]:
|
||||
@@ -166,11 +206,7 @@ def build_standard_drawing(drawing_id: str, label: str, payload: dict[str, Any])
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""구조물도 **상단 그림** — 돌쌓기 단면 (PLAN 12장 · 3장 한 장의 짜임 「상단 그림」).
|
||||
|
||||
⚠ **2026-09-14 B07 에서 이관** — 옛 자리 `B07_DesignDetail_Engine_Cad_StandardFigure.py`.
|
||||
CAD 엔티티 대신 **모양 목록(선·글자, m 단위, 위가 +y)** 을 냄 — 화면이 SVG 로 그리고(읽기 전용),
|
||||
CAD 는 뒤 일감이 같은 목록을 받아 씀(B07 `StandardSheet` 가 이미 옮겨 적음).
|
||||
|
||||
⚠ **기울기를 값으로 받는다 — 0.3 을 박지 않는다.**
|
||||
판정은 B08 `slope_of()` 한 벌(품셈 13-4-4 [주]⑪ 표준경사 — 직고·메찰·성절토). 사용자가 정했으면
|
||||
그 값이 이긴다(확정 ⑨). 그림은 **수량이 쓴 바로 그 값**으로 기울고 근거 문구도 같이 적는다.
|
||||
|
||||
⚠ **두께는 수량표가 쓰는 그 함수**(`UnitQuantity.wall_thickness`) — 사용자가 넣은 상부·하부 두께가
|
||||
식을 이긴다. 옛 B07 그림은 식을 따로 적어 **두께를 넣어도 그림만 옛 두께**로 섰음(이관 때 잡음).
|
||||
|
||||
상부 두께 = 뒷길이 + 0.30 하부 두께 = 상부 + 0.30 × (H − 1.0)
|
||||
터파기 폭 = 평균두께 + 0.2 기초 0.5×0.9 (기초유) · 0.1×0.7 (기초버림)
|
||||
|
||||
⚠ 터파기 치수는 **`common_util_excavation` 한 벌**을 읽는다 — 횡단도가 쓰는 그 상수다.
|
||||
⚠ 뒷길이를 안 정한 장은 **그리지 않고 까닭을 적는다**(브레인 판정 — 빈칸 대신 까닭).
|
||||
|
||||
⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서
|
||||
몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
wall_thickness,
|
||||
weep_hole_spec,
|
||||
)
|
||||
from common_util.common_util_excavation import (
|
||||
WALL_BLINDING_DEPTH_M,
|
||||
WALL_BLINDING_WIDTH_M,
|
||||
WALL_FOUNDATION_DEPTH_M,
|
||||
WALL_FOUNDATION_WIDTH_M,
|
||||
WALL_TRENCH_CLEARANCE_M,
|
||||
)
|
||||
|
||||
#: 그림 축척 — 1/25(1m = 40㎜). CAD 로 옮길 때 글자 크기(㎜)를 m 로 바꾸는 데도 씀.
|
||||
SCALE_MM_PER_M = 40.0
|
||||
|
||||
#: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다.
|
||||
#: ⚠ **큰돌쌓기(`boulder_masonry`)를 뺐다**(2026-09-09). 그림이 두께를 **뒷길이**에서 내는데
|
||||
#: 큰돌쌓기는 규격 축이 **직경**(`stone_cm`)이라 **영영 못 그린다** — 품셈 13-6(직경) ↔
|
||||
#: 13-4(뒷길이). 「직경에서 두께를 내는 식」은 도메인 판단이라 사용자 몫.
|
||||
FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry"})
|
||||
|
||||
#: 그림이 못 서는 장에 **까닭을 적는다** — 빈 자리를 그냥 두면 사용자가 「고장」으로 읽는다
|
||||
#: (2026-09-09 사용자 지시). 「무엇을 받아야 서는지」까지 적는다.
|
||||
_NO_FIGURE_REASONS: dict[str, str] = {
|
||||
"boulder_masonry": (
|
||||
"그림 없음 — 큰돌쌓기는 규격이 **직경**이라 두께를 낼 식이 아직 없습니다"
|
||||
"(뒷길이로 내는 돌쌓기 식과 축이 다름 · 사용자 확정 대기)"
|
||||
),
|
||||
"retaining_wall": "그림 없음 — 옹벽은 단면이 달라 아직 그리지 않습니다",
|
||||
}
|
||||
_NO_FIGURE_DEFAULT = "그림 없음 — 이 형식은 아직 단면을 그리지 않습니다"
|
||||
|
||||
#: 글자 크기(m) — 종이 ㎜ 를 축척으로 나눈 값(옛 CAD 그림과 같은 크기).
|
||||
_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` — 그때는 그리지 않는다."""
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _path(points: list[tuple[float, float]], role: str, *, dash: bool = False) -> dict[str, Any]:
|
||||
return {"kind": "path", "points": [list(p) for p in points], "role": role, "dash": dash}
|
||||
|
||||
|
||||
def _text(
|
||||
text: str, at: tuple[float, float], align: str, size: float = _DIM_SIZE
|
||||
) -> dict[str, Any]:
|
||||
return {"kind": "text", "text": text, "at": list(at), "align": align, "size": size}
|
||||
|
||||
|
||||
def build_figure(sheet: dict[str, Any]) -> list[dict[str, Any]] | None:
|
||||
"""장 하나의 그림 — 모양 목록(밑면 앞끝이 원점). 못 그리면 `None`(까닭은 `figure_reason`)."""
|
||||
if figure_reason(sheet) is not 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
|
||||
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
|
||||
average_t = (top_t + bottom_t) / 2.0
|
||||
dig_width = average_t + WALL_TRENCH_CLEARANCE_M
|
||||
# 기초 몫 — 「기초유 / 기초버림」이 폭·깊이를 가른다(정본 탭 제목). 안 정한 장은 안 그린다.
|
||||
foundation = str(options.get("foundation") or "")
|
||||
base_w, base_d = {
|
||||
"기초유": (WALL_FOUNDATION_WIDTH_M, WALL_FOUNDATION_DEPTH_M),
|
||||
"기초버림": (WALL_BLINDING_WIDTH_M, WALL_BLINDING_DEPTH_M),
|
||||
}.get(foundation, (0.0, 0.0))
|
||||
|
||||
shapes = [
|
||||
_path(
|
||||
[(0.0, 0.0), (lean, height_m), (lean + top_t, height_m), (bottom_t, 0.0), (0.0, 0.0)],
|
||||
"wall",
|
||||
),
|
||||
# 터파기 — 수량식 H × (평균두께 + 0.2) 와 **넓이가 같은** 평행사변형(앞면 따라 기움).
|
||||
_path(
|
||||
[
|
||||
(0.0, 0.0),
|
||||
(lean, height_m),
|
||||
(lean + dig_width, height_m),
|
||||
(dig_width, 0.0),
|
||||
(0.0, 0.0),
|
||||
],
|
||||
"guide",
|
||||
dash=True,
|
||||
),
|
||||
]
|
||||
if base_d > 0:
|
||||
shapes.append(
|
||||
_path([(0.0, 0.0), (0.0, -base_d), (base_w, -base_d), (base_w, 0.0)], "guide")
|
||||
)
|
||||
# 물구멍 — 벽을 가로지르는 짧은 선. 값은 **그 장의 제원**에서(수량표와 같은 함수).
|
||||
weep_area_m2, weep_diameter_mm, _weep_basis = weep_hole_spec(options)
|
||||
weep_y = height_m * 0.5
|
||||
shapes.append(_path([(slope * weep_y, weep_y), (slope * weep_y + top_t, weep_y)], "guide"))
|
||||
|
||||
right = max(lean + dig_width, base_w) + 0.16
|
||||
shapes += [
|
||||
_text(f"H = {height_m:g} m", (-0.28, height_m / 2.0), "right"),
|
||||
_text(f"상부 {top_t:.2f} m", (lean + top_t / 2.0, height_m + 0.14), "center"),
|
||||
_text(f"하부 {bottom_t:.2f} m", (bottom_t / 2.0, -0.30 - base_d), "center"),
|
||||
_text(f"1 : {slope:g}", (lean / 2.0 - 0.30, height_m * 0.72), "right"),
|
||||
_text(
|
||||
f"터파기 폭 {dig_width:.2f} m "
|
||||
f"(평균두께 {average_t:.2f} + {WALL_TRENCH_CLEARANCE_M:g})",
|
||||
(right, height_m * 0.92),
|
||||
"left",
|
||||
),
|
||||
_text(f"뒷길이 ℓ₃ = {back_cm:g}㎝", (right, height_m * 0.72), "left"),
|
||||
_text("막자갈(뒷채움) — 수량은 아래 표", (right, height_m * 0.55), "left"),
|
||||
_text(
|
||||
f"물구멍 Ø{weep_diameter_mm} — {weep_area_m2:g}㎡당 1개소",
|
||||
(right, weep_y - 0.2),
|
||||
"left",
|
||||
),
|
||||
_text(
|
||||
f"기초 {base_w:g} × {base_d:g} m ({foundation})"
|
||||
if base_d > 0
|
||||
else "기초 — 안 정함(기초유/기초버림)",
|
||||
(right, -base_d / 2.0 if base_d else 0.0),
|
||||
"left",
|
||||
),
|
||||
# 기울기 근거는 그림 밑에 한 줄 — 「사용자 지정」인지 「품셈 표준경사」인지 보여야 한다.
|
||||
_text(f"전면 기울기 — {slope_note}", (0.0, -0.62 - base_d), "left"),
|
||||
_text(
|
||||
f"{sheet.get('title') or '돌쌓기'} (축척 1/{int(1000 / SCALE_MM_PER_M)})",
|
||||
(0.0, height_m + 0.52),
|
||||
"left",
|
||||
_LABEL_SIZE,
|
||||
),
|
||||
]
|
||||
return shapes
|
||||
|
||||
|
||||
def attach_figures(payload: dict[str, Any]) -> None:
|
||||
"""장마다 `figure`(모양 목록 또는 `None`)와 `figure_reason` 을 실음 — 화면은 둘 중 하나."""
|
||||
for sheet in payload.get("sheets") or []:
|
||||
sheet["figure_reason"] = figure_reason(sheet)
|
||||
sheet["figure"] = build_figure(sheet)
|
||||
@@ -52,6 +52,7 @@ def project_structure_sheets(
|
||||
⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어
|
||||
모듈 맨 위에서 부르면 맞물린다.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureFigure import attach_figures
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureLibrary import project_templates
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureSheet import build_standard_sheets
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import apply_templates
|
||||
@@ -74,6 +75,8 @@ def project_structure_sheets(
|
||||
payload = build_standard_sheets(unit_table, section_modes)
|
||||
# 양식이 있는 종류는 줄마다 식·설명·반올림·갈 곳을 실음(PLAN 3장 ④ · 명세 13장).
|
||||
apply_templates(payload, settings, templates)
|
||||
# 상단 그림(읽기 전용 SVG 자료) — 못 그리는 장은 까닭(PLAN 12장).
|
||||
attach_figures(payload)
|
||||
# 왜 안 실렸는지 — 「구조물이 없다」와 「걸러졌다」를 화면이 가릴 수 있어야 한다.
|
||||
payload["skipped_structures"] = skipped
|
||||
return payload
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/* =============================================================================
|
||||
* B08_Quantity_UI_StructureFigure.ts
|
||||
* 구조물도 한 장의 **상단 그림** — 서버가 낸 모양 목록(m, 위가 +y)을 SVG 로 그림 (PLAN 12장).
|
||||
*
|
||||
* ⚠ 읽기 전용 — 치수를 셈하지 않음(서버 `B08_Quantity_Engine_StructureFigure` 한 벌). CAD 편집은 뒤.
|
||||
* ⚠ 못 그리는 장은 빈칸 대신 **까닭**을 같은 자리에 띄움.
|
||||
* ⚠ 한 장 = 그림 6 : 표 4(실무 비율) — 그림 칸 높이를 화면 높이의 60% 로 둠.
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "./B08_Quantity_UI_StructureSheet_Formula";
|
||||
|
||||
export type FigureShape =
|
||||
| { kind: "path"; points: [number, number][]; role: "wall" | "guide"; dash: boolean }
|
||||
| {
|
||||
kind: "text";
|
||||
text: string;
|
||||
at: [number, number];
|
||||
align: "left" | "center" | "right";
|
||||
size: number;
|
||||
};
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const ANCHOR = { left: "start", center: "middle", right: "end" } as const;
|
||||
|
||||
export const FIGURE_CSS = `
|
||||
.b08-sheet__figure { height: 60vh; min-height: 16rem; border: 1px solid var(--color-border);
|
||||
background: var(--color-surface); }
|
||||
.b08-sheet__figure svg { display: block; width: 100%; height: 100%; }
|
||||
.b08-sheet__figure--none { height: auto; min-height: 0; padding: 6px 10px; }
|
||||
.b08-fig__wall, .b08-fig__guide { fill: none; vector-effect: non-scaling-stroke; }
|
||||
.b08-fig__wall { stroke: var(--color-text); stroke-width: 2; }
|
||||
.b08-fig__guide { stroke: var(--color-text-secondary, #888); stroke-width: 1; }
|
||||
.b08-fig__guide.is-dash { stroke-dasharray: 6 4; }
|
||||
.b08-sheet__figure text { fill: var(--color-text); }
|
||||
`;
|
||||
|
||||
/** 그림 칸 — 모양이 없으면 까닭 한 줄. */
|
||||
export function figureSection(
|
||||
figure: FigureShape[] | null | undefined,
|
||||
reason: string | null | undefined,
|
||||
): HTMLElement {
|
||||
const box = el("div", "b08-sheet__figure");
|
||||
if (!figure?.length) {
|
||||
box.classList.add("b08-sheet__figure--none");
|
||||
box.append(el("p", "b08-quantity__message", (reason ?? "그림 없음").replace(/\*\*/g, "")));
|
||||
return box;
|
||||
}
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
const group = document.createElementNS(SVG_NS, "g");
|
||||
for (const shape of figure) {
|
||||
if (shape.kind === "text") {
|
||||
const text = document.createElementNS(SVG_NS, "text");
|
||||
text.setAttribute("x", String(shape.at[0]));
|
||||
text.setAttribute("y", String(-shape.at[1]));
|
||||
text.setAttribute("font-size", String(shape.size));
|
||||
text.setAttribute("text-anchor", ANCHOR[shape.align]);
|
||||
text.setAttribute("dominant-baseline", "middle");
|
||||
text.textContent = shape.text;
|
||||
group.append(text);
|
||||
continue;
|
||||
}
|
||||
const line = document.createElementNS(SVG_NS, "polyline");
|
||||
line.setAttribute("points", shape.points.map(([x, y]) => `${x},${-y}`).join(" "));
|
||||
line.setAttribute("class", `b08-fig__${shape.role}${shape.dash ? " is-dash" : ""}`);
|
||||
group.append(line);
|
||||
}
|
||||
svg.append(group);
|
||||
box.append(svg);
|
||||
// 보이는 틀은 **그려진 뒤 실제 글자 폭**으로 — 글자 폭을 서버가 짐작하지 않게.
|
||||
requestAnimationFrame(() => {
|
||||
if (!svg.isConnected) return;
|
||||
const b = group.getBBox();
|
||||
const pad = Math.max(b.width, b.height) * 0.04;
|
||||
svg.setAttribute(
|
||||
"viewBox",
|
||||
`${b.x - pad} ${b.y - pad} ${b.width + pad * 2} ${b.height + pad * 2}`,
|
||||
);
|
||||
});
|
||||
return box;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* ⛔ 탭 등록은 이 파일이 하지 않음 — `B08_Quantity_UI_Page.ts` 탭 배선은 브레인 몫(PLAN 0장 충돌 막이).
|
||||
* 부르는 법: `{ label: "구조물도", build: () => renderStructureSheets(projectId) }`
|
||||
* ⚠ 값을 셈하지 않음 — 서버(`/quantity/structure-sheets`)가 낸 단위당 값을 표기 자리수로만 접음.
|
||||
* ⚠ 상단 그림·하단 일위대가는 뒤 일감 — 지금은 가운데 원단위 수량표만.
|
||||
* ⚠ 한 장 = 상단 그림(`_StructureFigure`, 읽기 전용 SVG) + 원단위 수량표 + 하단 일위대가.
|
||||
* ⚠ 제원 저장은 칸 옆 [제원 저장] 한 번에 정본(`structures.json`)으로 감 — 옛 B07 폼 규약 그대로.
|
||||
* ⚠ 식 고치기(PLAN 3장 ⑤) — 칸에서 고치면 **이 화면이 같은 풀이기로 즉시** 다시 풂(왕복 없음).
|
||||
* [식 저장] 때는 식만 보내고 **값은 서버가 Node 로 다시 냄**(판정 Ⓐ). 저장은 양식 + 프로젝트 단위.
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "./B08_Quantity_UI_StructureSheet_Spec";
|
||||
import { buildLibraryPanel, libraryLabel } from "./B08_Quantity_UI_StructureSheet_Library";
|
||||
import { unitPriceSection } from "./B08_Quantity_UI_StructureSheet_UnitPrice";
|
||||
import { FIGURE_CSS, figureSection, type FigureShape } from "./B08_Quantity_UI_StructureFigure";
|
||||
|
||||
export interface StructureSheetRow {
|
||||
no: number;
|
||||
@@ -75,6 +76,9 @@ export interface StructureSheet extends StandardSheetSpec {
|
||||
}[];
|
||||
notes: string[];
|
||||
unpriced_rows: string[];
|
||||
/** 상단 그림 모양 목록 — 못 그리는 장은 `null` + `figure_reason`(PLAN 12장). */
|
||||
figure?: FigureShape[] | null;
|
||||
figure_reason?: string | null;
|
||||
/** 양식으로 선 장이면 그 양식 — 없으면 지금 전개 줄(고정형 모양). */
|
||||
/** `imported_from` 없음 = 프로그램 기본을 읽는 중(가져오기 전). */
|
||||
library_item?: {
|
||||
@@ -144,7 +148,7 @@ function injectStyles(): void {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = CSS;
|
||||
style.textContent = CSS + FIGURE_CSS;
|
||||
document.head.append(style);
|
||||
}
|
||||
|
||||
@@ -256,7 +260,8 @@ function sheetBody(
|
||||
// 실무 시트 머리의 「m당」·「개소당」 — 종류마다 다름(통일하지 않음).
|
||||
el("span", "b08-grid__caption", sheet.unit_label),
|
||||
);
|
||||
main.append(head);
|
||||
// 한 장 = 상단 그림 + 하단 표(PLAN 3장 한 장의 짜임) — 못 그리는 장은 까닭.
|
||||
main.append(head, figureSection(sheet.figure, sheet.figure_reason));
|
||||
if (!sheet.rows.length) {
|
||||
main.append(
|
||||
el("p", "b08-quantity__message", "이 제원은 원단위 줄이 서지 않음 — 아래 사유 참고"),
|
||||
|
||||
@@ -19,15 +19,17 @@ 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 B08_Quantity.B08_Quantity_Engine_StructureSheet import sheet_title # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureFigure import ( # noqa: E402
|
||||
FIGURE_TYPE_IDS,
|
||||
figure_reason,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureSheet import ( # noqa: E402
|
||||
SLOPE_TYPE_IDS,
|
||||
sheet_title,
|
||||
)
|
||||
|
||||
|
||||
def _sheet(type_id: str, **options) -> dict:
|
||||
|
||||
@@ -21,7 +21,7 @@ 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
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureFigure import ( # noqa: E402
|
||||
build_figure,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureSheet_Edit import ( # noqa: E402
|
||||
@@ -83,10 +83,7 @@ def _figure_labels(**options) -> list[str]:
|
||||
"height_m": 2.0,
|
||||
"options": {"height_m": 2.0, "back_len_cm": 45, **options},
|
||||
}
|
||||
entities, _height = build_figure(
|
||||
"d1", sheet, "layer", (0.0, 0.0), "#000000", "#111111", "#222222"
|
||||
)
|
||||
return [str((item.get("shapeData") or {}).get("label") or "") for item in entities]
|
||||
return [shape["text"] for shape in build_figure(sheet) or [] if shape["kind"] == "text"]
|
||||
|
||||
|
||||
def test_물빼기_주석이_구조물_값을_쓴다() -> None:
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""구조물도 상단 그림 — 그림 치수가 **수량표와 같은 한 벌**인가 (PLAN 12장).
|
||||
|
||||
⚠ 옛 B07 그림은 두께 식을 따로 적어, 상부·하부 두께를 넣어도 **그림만 옛 두께**였음.
|
||||
"""
|
||||
|
||||
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 B08_Quantity.B08_Quantity_Engine_StructureFigure import ( # noqa: E402
|
||||
attach_figures,
|
||||
build_figure,
|
||||
)
|
||||
|
||||
|
||||
def _sheet(**options) -> dict:
|
||||
return {
|
||||
"title": "돌쌓기(메)",
|
||||
"type_id": "masonry_dry",
|
||||
"height_m": 2.0,
|
||||
"options": {"height_m": 2.0, **options},
|
||||
}
|
||||
|
||||
|
||||
def _paths(shapes: list[dict]) -> list[dict]:
|
||||
return [shape for shape in shapes if shape["kind"] == "path"]
|
||||
|
||||
|
||||
def _area(points: list[list[float]]) -> float:
|
||||
return abs(sum(x0 * y1 - x1 * y0 for (x0, y0), (x1, y1) in zip(points, points[1:]))) / 2
|
||||
|
||||
|
||||
def test_벽_두께는_사용자가_넣은_값을_따른다() -> None:
|
||||
wall = _paths(build_figure(_sheet(back_len_cm=35, face_slope_ratio=0.3)))[0]["points"]
|
||||
# 식: 상부 0.35+0.30=0.65 · 하부 0.65+0.30×(2−1)=0.95 · 기울기 0.3 → 앞면 위 끝 0.6
|
||||
assert wall[:4] == [[0.0, 0.0], [0.6, 2.0], [1.25, 2.0], [0.95, 0.0]]
|
||||
given = _paths(
|
||||
build_figure(
|
||||
_sheet(
|
||||
back_len_cm=35, face_slope_ratio=0.3, thickness_top_m=0.5, thickness_bottom_m=1.2
|
||||
)
|
||||
)
|
||||
)[0]["points"]
|
||||
assert given[2] == [1.1, 2.0] and given[3] == [1.2, 0.0]
|
||||
|
||||
|
||||
def test_터파기_넓이는_수량식과_같다() -> None:
|
||||
dig = _paths(build_figure(_sheet(back_len_cm=35, face_slope_ratio=0.3)))[1]
|
||||
assert dig["dash"]
|
||||
assert abs(_area(dig["points"]) - 2.0 * ((0.65 + 0.95) / 2 + 0.2)) < 1e-9
|
||||
|
||||
|
||||
def test_못_그리는_장은_그림_대신_까닭이_실린다() -> None:
|
||||
payload = {
|
||||
"sheets": [
|
||||
_sheet(),
|
||||
{**_sheet(stone_cm="60~80"), "type_id": "boulder_masonry"},
|
||||
_sheet(back_len_cm=45),
|
||||
]
|
||||
}
|
||||
attach_figures(payload)
|
||||
no_back, boulder, drawn = payload["sheets"]
|
||||
assert no_back["figure"] is None and "뒷길이" in no_back["figure_reason"]
|
||||
assert boulder["figure"] is None and "직경" in boulder["figure_reason"]
|
||||
assert drawn["figure_reason"] is None and drawn["figure"]
|
||||
Reference in New Issue
Block a user