Files
Aislo/resources/tester/test_b07_plan_kinds.py
T
eomsangdonandClaude Opus 5.5 6df477a522 feat(B07): 평면도 관리 모달 종류 드롭다운 상세 · 장 한 블록 · 끌어 놓기 종류 순서(140-2)
- 맨 위 [종류 ▼] 상세 폼 · × 대신 폼 안 [지우기]
- 평면도 장 = 종류마다 한 줄 [≡] 이름 [1][2] · 장 단추 누르면 미리보기 + 드롭다운 따라감
- 줄 끌어 놓기 = 종류 순서 · 놓는 즉시 PUT plan-kinds-order(reorder_kinds) · 번호 바뀐 장 확정 풂

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
2026-10-09 14:51:11 +09:00

471 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""B07 계획평면도 종류 · 정보 조각 (PLAN 60장).
① 측점 둘에서 성토사면 외곽선 폭 = 횡단 설계 값(노견 끝 · 설계선이 지반에 닿는 자리)
② 종류 더하기 · 지우기(기본 넷 포함) · 다시 읽어 같음
③ 대피소 · 사토장 자리 — 한 자리에 둘 다
④ 고른 조각만 도면 층으로 얹힘
⑤ 조각 잘게(67장) — 노견선 · 성토 외곽선 · 절토 외곽선 따로 · 기본 넷 처음 체크 = 옛 그림 ·
옛 종류 파일의 slope_outline 펼침 · 종류 업데이트(기본 넷 포함) · 확정본 풀림 · 배수시설 기호
⑥ IP 표(94장) — 예시 검산 · 도-분-초 올림 · 장마다 그 장 IP 한 번 · 표 밑 선 자름 · 층
⑦ 용지도 종류(93장) — 기본 종류 · 옛 종류 폴더에 한 번 더함(지우면 안 살아남) · 용지 조각 층
⑧ 배수유역도 종류(109장) — 양식 watershed · 한 장 목록 · 지우기 · 다시 만들기 · IP 표 두 줄 차례 ·
용지도 상자 안 선 0
⑨ 종류 차례(140-2) — 끌어 놓은 차례 그대로 · 다시 읽어 같음 · 앞선 종류가 바뀐 종류만 확정 풂 · 목록 어긋나면 거절
"""
import json
import math
from pathlib import Path
import ezdxf
import pytest
from B06_Section.B06_Section_Engine_Design import compute_cross_design
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import (
_place,
curve_values,
dms,
ip_blocks,
ip_table_piece,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
DEFAULT_KINDS,
build_plan_kind_drawing,
layer_id,
slope_pieces,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import drawing_to_dxf
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_manifest, _write_manifest
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
_kinds_dir,
_refuge_items,
add_kind,
delete_kind,
list_kinds,
plan_kind_items,
read_marks,
reorder_kinds,
slope_records,
update_kind,
write_marks,
)
def _samples(slope: float) -> list[dict[str, float]]:
"""좌(+)가 높은 비탈 지반 — 오프셋 -20 ~ 20 m, 0.5 m 간격."""
return [
{"offset_m": i * 0.5, "elevation_m": 100.0 + slope * i * 0.5, "valid": True}
for i in range(-40, 41)
]
def _project(tmp_path: Path) -> tuple[Path, dict, dict]:
"""+y 로 곧게 가는 노선 · 측점 0 · 20 · 40 · 횡단 파일 · B06 저장 설계."""
root = tmp_path / "project"
cross_dir = root / "B06_Section" / "cross_sections"
cross_dir.mkdir(parents=True)
longitudinal_path = root / "B06_Section" / "longitudinal" / "longitudinal.json"
longitudinal_path.parent.mkdir(parents=True)
stations, designs = [], {}
for chainage, slope in ((0, 0.3), (20, 0.5), (40, 0.4)):
stations.append(
{"chainage_m": float(chainage), "center_x": 0.0, "center_y": float(chainage)}
)
source = {
"chainage_m": float(chainage),
"samples": _samples(slope),
# +y 진행의 좌측 = -x
"frame": {"origin": {"x": 0.0, "y": float(chainage)}, "left_xy": [-1.0, 0.0]},
}
(cross_dir / f"cross_{chainage:05d}m.json").write_text(json.dumps(source), "utf-8")
designs[chainage] = compute_cross_design(
source["samples"], 100.0, ground_type="soil", section_mode="left_cut"
)
longitudinal = {"stations": stations}
longitudinal_path.write_text(json.dumps(longitudinal), "utf-8")
return longitudinal_path, longitudinal, designs
def _meets_ground(design: dict, side: str) -> float:
"""설계선이 바깥으로 가며 지반과 다시 같아지는 첫 오프셋 — 시험 쪽 독립 계산."""
edge = design["road_edges"][side]["offset_m"]
line = sorted((p["offset_m"], p["elevation_m"]) for p in design["design_line"])
outward = (
[p for p in line if p[0] > edge]
if side == "left"
else [p for p in line[::-1] if p[0] < edge]
)
slope = SLOPES[design["_c"]]
for offset, elevation in outward:
# 지반은 직선 비탈이라 어느 오프셋에서나 식으로 바로 잰다(설계선 꺾임점 포함).
if abs(100.0 + slope * offset - elevation) < 1e-3:
return offset
raise AssertionError("설계선이 지반에 닿지 않음")
SLOPES = {0: 0.3, 20: 0.5, 40: 0.4}
def test_outline_width_equals_cross_design_at_two_stations(tmp_path: Path) -> None:
longitudinal_path, longitudinal, designs = _project(tmp_path)
records = slope_records(longitudinal_path, longitudinal, designs, 0.0, 40.0)
route = [(0.0, 0.0, 0.0), (40.0, 0.0, 40.0)]
edge, fill, cut, ticks = slope_pieces(route, records, 0.0, 40.0)
left_edge, right_edge = edge["lines"]
# 좌(+)가 높은 지반 — 좌측 비탈은 절토 · 우측은 성토, 각각 한 줄로 이어짐
assert all(r["left_cut"] and not r["right_cut"] for r in records)
(left_end,), (right_end,) = cut["lines"], fill["lines"]
for chainage in (20, 40): # 견본 측점 둘
design = {**designs[chainage], "_c": chainage}
index = next(i for i, p in enumerate(left_edge) if abs(p[1] - chainage) < 1e-6)
expected = {
"left_edge": design["road_edges"]["left"]["offset_m"],
"left_end": _meets_ground(design, "left"),
"right_edge": design["road_edges"]["right"]["offset_m"],
"right_end": _meets_ground(design, "right"),
}
# 좌측은 -x 쪽 — 중심(0, c)에서 잰 거리가 횡단 설계 오프셋이다.
assert -left_edge[index][0] == pytest.approx(expected["left_edge"], abs=1e-3)
assert -left_end[index][0] == pytest.approx(expected["left_end"], abs=1e-3)
assert -right_edge[index][0] == pytest.approx(expected["right_edge"], abs=1e-3)
assert -right_end[index][0] == pytest.approx(expected["right_end"], abs=1e-3)
# 성토 · 절토 비탈이 실제로 노견 밖에 있다
assert abs(expected["left_end"]) > abs(expected["left_edge"])
assert abs(expected["right_end"]) > abs(expected["right_edge"])
# 비탈 눈금은 노견 끝에서 바깥으로 · 긴 것과 짧은 것이 섞임
lengths = {round(math.dist(*tick), 2) for tick in ticks["ticks"]}
assert len(ticks["ticks"]) > 20 and len(lengths) > 2
def test_kinds_add_delete_persist(tmp_path: Path) -> None:
root = tmp_path / "project"
assert [k["name"] for k in list_kinds(root)] == [
"지형",
"노선배치도",
"대피소",
"라이다",
"용지도",
"배수유역도",
]
added = add_kind(root, "내 평면도", ["centerline", "station", "nope"])
assert added["pieces"] == ["centerline", "station"]
with pytest.raises(ValueError):
add_kind(root, "내 평면도", ["centerline"]) # 같은 이름
with pytest.raises(ValueError):
add_kind(root, "빈 것", [])
delete_kind(root, "terrain") # 기본 넷도 지움
again = list_kinds(root) # 다시 읽어 같음
assert [k["name"] for k in again] == [
"노선배치도",
"대피소",
"라이다",
"용지도",
"배수유역도",
"내 평면도",
]
assert again[-1]["pieces"] == ["centerline", "station"]
with pytest.raises(FileNotFoundError):
delete_kind(root, "terrain")
for kind in list(again):
delete_kind(root, kind["id"])
assert list_kinds(root) == [] # 다 지우면 비어 있음(기본 넷이 되살아나지 않음)
assert {k["id"] for k in DEFAULT_KINDS} == {
"terrain",
"route",
"layout",
"lidar",
"landuse",
"watershed",
}
def test_refuge_marks_same_spot(tmp_path: Path) -> None:
root = tmp_path / "project"
write_marks(root, [{"chainage_m": 20, "refuge": True, "spoil": True}, {"chainage_m": 5}])
assert read_marks(root) == [{"chainage_m": 20.0, "refuge": True, "spoil": True}]
route = [(0.0, 0.0, 0.0), (40.0, 0.0, 40.0)]
items = _refuge_items(root, route, 0.0, 40.0)["items"]
assert [item["label"] for item in items] == ["대피소 · 사토장"]
assert items[0]["y"] == pytest.approx(20.0)
def test_drawing_has_only_chosen_pieces() -> None:
pieces = {
"centerline": {"lines": [[[0.0, 0.0], [0.0, 40.0]]]},
"radius": {"items": [{"label": "R=15", "at": [1.0, 20.0]}]},
}
drawing = build_plan_kind_drawing(
"plan_k1", "계획평면도(시험)", (-50.0, -10.0, 50.0, 50.0), pieces
)
layers = {entity.get("layerId") for entity in drawing["entities"]}
assert layer_id("centerline") in layers and layer_id("radius") in layers
assert layer_id("contour") not in layers and layer_id("station") not in layers
labels = [e["shapeData"]["label"] for e in drawing["entities"] if e["type"] == "Text"]
assert "R=15" in labels and "계획평면도(시험)" in labels
def test_split_runs_switch_cut_fill_and_stay_joined() -> None:
route = [(0.0, 0.0, 0.0), (40.0, 0.0, 40.0)]
base = {"left_edge_m": 2.0, "left_end_m": 5.0, "right_edge_m": -2.0, "right_end_m": -5.0}
records = [
{"chainage_m": 0.0, **base, "left_cut": True, "right_cut": False},
{"chainage_m": 40.0, **base, "left_cut": False, "right_cut": False},
]
_edge, fill, cut, _ticks = slope_pieces(route, records, 0.0, 40.0)
(cut_left,) = cut["lines"]
fill_left = next(line for line in fill["lines"] if line[0][0] < 0)
assert cut_left[-1] == fill_left[0] # 바뀌는 자리에서 두 선이 이어짐
assert cut_left[0][1] == 0.0 and fill_left[-1][1] == 40.0
def test_default_kinds_keep_old_picture_and_legacy_file(tmp_path: Path) -> None:
root = tmp_path / "project"
outline = {"road_edge", "fill_outline", "cut_outline"}
# 옛 slope_outline 자리를 나눈 셋이 그대로 채움 · 배수시설은 옛 그림에 없음(용지도는 따로)
for kind in DEFAULT_KINDS:
if kind["id"] not in ("landuse", "watershed"):
assert outline <= set(kind["pieces"]) and "drainage" not in kind["pieces"]
folder = _kinds_dir(root)
folder.mkdir(parents=True)
(folder / "old.json").write_text(
json.dumps({"name": "옛것", "pieces": ["slope_outline", "station"], "order": 0}), "utf-8"
)
kind = next(k for k in list_kinds(root) if k["id"] == "old") # 용지도는 이관으로 더해짐
assert set(kind["pieces"]) == outline | {"station"}
def test_update_kind_default_and_release_confirmed(tmp_path: Path) -> None:
root = tmp_path / "project"
manifest = _read_manifest(root)
manifest["drawings"] = {
"plan_terrain": {"confirmed": True},
"plan_route": {"confirmed": True},
}
_write_manifest(root, manifest)
update_kind(root, "terrain", "지형2", ["contour", "centerline"]) # 기본 종류 고침
again = {k["id"]: k for k in list_kinds(root)} # 다시 읽어 같음
assert again["terrain"] == {
"id": "terrain",
"name": "지형2",
"pieces": ["contour", "centerline"],
"layout": "plan",
"scale": 1200,
}
assert [k["id"] for k in list_kinds(root)][0] == "terrain" # 차례 그대로
drawings = _read_manifest(root)["drawings"]
assert drawings["plan_terrain"]["confirmed"] is False
assert drawings["plan_route"]["confirmed"] is True
with pytest.raises(ValueError):
update_kind(root, "terrain", "노선배치도", ["contour"]) # 다른 종류와 같은 이름
with pytest.raises(ValueError):
update_kind(root, "terrain", "지형", [])
with pytest.raises(FileNotFoundError):
update_kind(root, "nokind", "없음", ["contour"])
def test_drainage_symbol_piece() -> None:
pieces = {"drainage": {"items": [{"x": 0.0, "y": 20.0, "label": "D800"}]}}
drawing = build_plan_kind_drawing("plan_k2", "시험", (-50.0, -10.0, 50.0, 50.0), pieces)
mine = [e for e in drawing["entities"] if e.get("layerId") == layer_id("drainage")]
assert {e["type"] for e in mine} >= {"Text"} and len(mine) >= 2
assert any(e["type"] == "Text" and e["shapeData"]["label"] == "D800" for e in mine)
def test_ip_table_example_values_and_dms() -> None:
ia = 94 + 59 / 60 + 58.38 / 3600 # 예시 그림 IP1
values = curve_values(15.0, ia)
assert dms(ia) == "94-59-58.38"
assert [f"{values[k]:.5f}" for k in ("tl", "cl", "sl")] == ["16.36950", "24.87082", "7.20271"]
assert dms(1 + 59 / 60 + 59.9999 / 3600) == "2-00-00.00" # 초 60.00 올림
assert dms(5 + 3 / 60 + 7.2 / 3600) == "5-03-07.20"
# ㄱ자 노선 — (0,0)→(100,0)→(100,100) · 모서리 R 15 곡선(가운데 누가거리 100) · 앞에 노선 밖 옛 곡선
_ROUTE = [(0.0, 0.0, 0.0), (85.0, 85.0, 0.0), (115.0, 100.0, 15.0), (200.0, 100.0, 100.0)]
_CURVES = [
{"apex": [500.0, 500.0], "start": [490.0, 500.0], "end": [500.0, 510.0], "radius_m": 10.0},
{"apex": [100.0, 0.0], "start": [85.0, 0.0], "end": [100.0, 15.0], "radius_m": 15.0},
]
def test_ip_blocks_one_sheet_each_and_ends() -> None:
first = ip_blocks(_CURVES, _ROUTE, 1, 2, 0.0, 100.0)
second = ip_blocks(_CURVES, _ROUTE, 2, 2, 100.0, 200.0)
assert [b["name"] for b in first] == ["BP"] # 경계 IP 는 뒤 장에만
assert [b["name"] for b in second] == ["2", "EP"] # 번호 = 곡선표 차례(옛 곡선은 빠짐)
ip = second[0]
assert dms(ip["ia"]) == "90-00-00.00" and ip["tl"] == pytest.approx(15.0)
assert [b["name"] for b in ip_blocks(_CURVES, _ROUTE, 1, 1, 0.0, 200.0)] == ["BP", "2", "EP"]
def test_ip_table_drawing_clips_lines_under_table(tmp_path: Path) -> None:
box = (-20.0, -20.0, 300.0, 200.0)
contour = [[-20.0, 0.0], [300.0, 0.0]] # 표(왼쪽 아래 · 높이 48 m) 밑을 지나는 등고선
pieces = {
"contour": {"lines": [contour]},
"centerline": {"lines": [[[0.0, 100.0], [9.0, 100.0]]]},
}
pieces["ip_table"] = ip_table_piece(
pieces, _CURVES, _ROUTE, box, {"number": 1, "start_m": 0.0, "end_m": 200.0}, 1
)
# IP 표 칸(x 29.2 ~ 72.4 · y -14 ~ 34)만 빠짐 · BP · EP 칸(3행)은 y 0 위라 안 자름
assert pieces["contour"]["lines"] == [[[-20.0, 0.0], [29.2, 0.0]], [[72.4, 0.0], [300.0, 0.0]]]
assert pieces["centerline"]["lines"] == [[[0.0, 100.0], [9.0, 100.0]]] # 표 밖은 그대로
drawing = build_plan_kind_drawing("plan_ip", "시험", box, pieces)
mine = [e for e in drawing["entities"] if e.get("layerId") == layer_id("ip_table")]
assert [e["type"] for e in mine] == ["Table"] * 3
bp, ip, _ep = (e["shapeData"] for e in mine)
assert len(bp["rowHeights"]) == 3 and len(ip["rowHeights"]) == 8
assert bp["cells"][1][2]["text"] == "0.00000" and ip["cells"][1][2]["text"] == "0.00000"
assert ip["cells"][2][2]["text"] == "100.00000" # Y = 동(x)
assert ip["cells"][3][1]["text"] == "90-00-00.00"
assert {layer["id"] for layer in drawing["layers"]} >= {layer_id("ip_table")}
assert "ip_table" in next(k for k in DEFAULT_KINDS if k["id"] == "route")["pieces"]
# DXF — 표 → 선 + 글(IP 묶음: 바깥 4 · 세로 10 · 가로 20 · 글 17)
raw, skipped = drawing_to_dxf({"entities": [mine[1]], "layers": drawing["layers"]})
(tmp_path / "ip.dxf").write_bytes(raw)
space = ezdxf.readfile(tmp_path / "ip.dxf").modelspace()
assert skipped == {} and len(space.query("LINE")) == 34 and len(space.query("TEXT")) == 17
assert {"90-00-00.00", "15.00000"} <= {t.dxf.text for t in space.query("TEXT")}
def test_landuse_kind_added_once_to_old_folder(tmp_path: Path) -> None:
root = tmp_path / "project"
folder = _kinds_dir(root)
folder.mkdir(parents=True)
for order, kind in enumerate(DEFAULT_KINDS[:4]): # 93장 앞에 내려 둔 기본 넷
(folder / f"{kind['id']}.json").write_text(
json.dumps({"name": kind["name"], "pieces": kind["pieces"], "order": order}), "utf-8"
)
kinds = list_kinds(root)
assert [k["id"] for k in kinds][-2:] == ["landuse", "watershed"] # 늘어난 기본 종류 둘
assert kinds[-2]["pieces"] == ["contour", "centerline", "parcel", "parcel_info", "emd", "sgg"]
assert len(list_kinds(root)) == 6 # 두 번 읽어도 한 번
delete_kind(root, "landuse")
assert "landuse" not in [k["id"] for k in list_kinds(root)] # 지운 것은 안 살아남
def test_landuse_pieces_become_layers() -> None:
pieces = {
"parcel": {"lines": [[[0.0, 0.0], [10.0, 0.0]]]},
"emd": {"lines": [[[0.0, 5.0], [10.0, 5.0]]]},
"sgg": {"lines": [[[0.0, 9.0], [10.0, 9.0]]]},
"parcel_info": {
"items": [
{"lines": ["1-1", "임야", "", "국"], "anchor": [5.0, 2.0], "center": [20.0, 20.0]}
]
},
}
drawing = build_plan_kind_drawing(
"plan_landuse", "평면도(용지도)", (-50.0, -10.0, 50.0, 50.0), pieces
)
names = {layer["id"] for layer in drawing["layers"]}
assert {layer_id(p) for p in ("parcel", "parcel_info", "emd", "sgg")} <= names
labels = [
e["shapeData"]["label"]
for e in drawing["entities"]
if e.get("layerId") == layer_id("parcel_info") and e["type"] == "Text"
]
assert labels == ["1-1", "임야", "국"]
def test_watershed_kind_layout_one_sheet(tmp_path: Path) -> None:
"""배수유역도 = 기본 종류 · 양식 watershed · 조각 없음 · 장이 여럿이어도 목록은 한 장."""
root = tmp_path / "project"
watershed = next(k for k in list_kinds(root) if k["id"] == "watershed")
assert watershed == {
"id": "watershed",
"name": "배수유역도",
"pieces": [],
"layout": "watershed",
"scale": 6000,
}
# 측점 0 ~ 2,000 m — 1/1,200 평면도는 여러 장 · 유역도는 한 장
stations = [
{"chainage_m": float(c), "center_x": 0.0, "center_y": float(c)} for c in range(0, 2001, 20)
]
items = plan_kind_items(root, {"stations": stations})
terrain = [item for item in items if item["plan_kind"] == "terrain"]
mine = [item for item in items if item["plan_kind"] == "watershed"]
assert len(terrain) > 1
assert mine == [
{
"id": "plan_watershed",
"label": "배수유역도",
"plan_kind": "watershed",
"plan_kind_name": "배수유역도",
}
]
def test_watershed_kind_delete_and_add_again(tmp_path: Path) -> None:
"""× 로 지우면 안 살아남 · 「새 종류」 양식 유역도로 다시 만듦 · 업데이트는 양식 그대로."""
root = tmp_path / "project"
delete_kind(root, "watershed")
assert "watershed" not in [k["id"] for k in list_kinds(root)]
assert "watershed" not in [k["id"] for k in list_kinds(root)] # 다시 읽어도 안 살아남
added = add_kind(root, "배수유역도", ["contour"], "watershed") # 조각은 버림
assert added["layout"] == "watershed" and added["pieces"] == []
again = next(k for k in list_kinds(root) if k["id"] == added["id"])
assert again == added
assert any(
item["id"] == f"plan_{added['id']}" for item in plan_kind_items(root, {"stations": []})
)
update_kind(root, added["id"], "유역도2", []) # 유역도 양식은 조각 없이 업데이트
assert next(k for k in list_kinds(root) if k["id"] == added["id"])["layout"] == "watershed"
with pytest.raises(ValueError):
add_kind(root, "이상한 양식", ["contour"], "nope")
def test_watershed_kind_added_once_to_folder_with_landuse(tmp_path: Path) -> None:
"""용지도 표시가 있는 옛 폴더 — 배수유역도만 한 번 더해짐 · 지우면 안 살아남."""
root = tmp_path / "project"
folder = _kinds_dir(root)
folder.mkdir(parents=True)
for order, kind in enumerate(DEFAULT_KINDS[:5]):
(folder / f"{kind['id']}.json").write_text(
json.dumps({"name": kind["name"], "pieces": kind["pieces"], "order": order}), "utf-8"
)
(folder / "_landuse_added").touch()
assert [k["id"] for k in list_kinds(root)][-1] == "watershed"
delete_kind(root, "watershed")
assert [k["id"] for k in list_kinds(root)][-1] == "landuse"
def test_ip_table_two_lines_left_to_right_top_to_bottom() -> None:
"""한 줄 셋까지 들어가는 폭 · 묶음 넷 — 첫 줄(1 · 2 · 3) 맨 위 왼 → 오 · 넷째는 아랫줄 왼쪽."""
blocks = [{"name": str(i), "ia": 1.0} for i in range(1, 5)]
block_w = 7.0 + 6.0 + 23.0
_place(blocks, 2 * 5.0 + 3 * block_w + 1.0)
origins = [b["origin"] for b in blocks]
tops = [o[1] for o in origins]
assert tops[0] == tops[1] == tops[2] > tops[3] # 첫 줄이 위
assert origins[0][0] < origins[1][0] < origins[2][0] # 왼 → 오
assert origins[3][0] == origins[0][0] # 다음 줄은 다시 왼쪽
assert tops[3] == 5.0 + 8 * 5.0 # 마지막 줄이 도곽 왼쪽 아래 기준(옛 한 줄 자리와 같음)
assert tops[0] - tops[3] == 8 * 5.0 + 3.0 # 줄 간격 그대로
def test_reorder_kinds_order_and_release_renumbered(tmp_path: Path) -> None:
root = tmp_path / "project"
manifest = _read_manifest(root)
manifest["drawings"] = {
"plan_terrain": {"confirmed": True},
"plan_route": {"confirmed": True},
"plan_layout": {"confirmed": True},
}
_write_manifest(root, manifest)
before = [k["id"] for k in list_kinds(root)]
assert before[:3] == ["terrain", "route", "layout"]
after = ["route", "terrain", *before[2:]] # 앞 둘만 바꿈
reorder_kinds(root, after)
assert [k["id"] for k in list_kinds(root)] == after # 다시 읽어 같음
drawings = _read_manifest(root)["drawings"]
assert drawings["plan_terrain"]["confirmed"] is False # 번호 바뀜
assert drawings["plan_route"]["confirmed"] is False
assert drawings["plan_layout"]["confirmed"] is True # 앞선 종류 묶음 그대로 = 번호 그대로
with pytest.raises(ValueError):
reorder_kinds(root, after[:-1]) # 빠진 종류
with pytest.raises(ValueError):
reorder_kinds(root, [*after[:-1], "terrain"]) # 겹친 종류