Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
# Conflicts: # resources/tester/test_591_1_plan_site.py
This commit is contained in:
@@ -118,7 +118,7 @@ const STUBS = {
|
||||
"@config/config_frontend": {
|
||||
ROUTES,
|
||||
API_BASE_URL: "",
|
||||
PLAN_STEP_ROUTES: ["d02-plan-base", "d03-plan-analysis", "d04-plan-confirm"],
|
||||
PLAN_STEP_ROUTES: ["d03-plan-analysis", "d04-plan-confirm"],
|
||||
// 565-1 — 보고서(E05) 맨 뒤
|
||||
STAB_STEP_ROUTES: [
|
||||
"e02-stab-ground",
|
||||
@@ -206,7 +206,7 @@ const ALL = [
|
||||
"m02-master-template",
|
||||
// 529-1 D · E 묶음 — 묶음 안에서만 [<] [>] · 등록(D01 · E01) = [?] 만
|
||||
"d01-plan-register",
|
||||
"d02-plan-base",
|
||||
"d03-plan-analysis",
|
||||
"d04-plan-confirm",
|
||||
"e01-stab-register",
|
||||
"e02-stab-ground",
|
||||
|
||||
@@ -36,10 +36,10 @@ def test_529_1_D_E_묶음_안에서만_앞뒤(results):
|
||||
rows = results["rows"]
|
||||
for route in ("d01-plan-register", "e01-stab-register"):
|
||||
assert rows[route]["plain"]["labels"] == ["?", "저장"], route
|
||||
for route in ("d02-plan-base", "d04-plan-confirm", "e02-stab-ground", "e05-stab-report"):
|
||||
for route in ("d03-plan-analysis", "d04-plan-confirm", "e02-stab-ground", "e05-stab-report"):
|
||||
assert rows[route]["plain"]["labels"] == ["?", "<", ">", "저장"], route
|
||||
# 묶음 첫 단계 [<] · 끝 단계 [>] 흐림(B 묶음과 따로)
|
||||
assert rows["d02-plan-base"]["plain"]["disabled"][:3] == [False, True, False]
|
||||
assert rows["d03-plan-analysis"]["plain"]["disabled"][:3] == [False, True, False]
|
||||
assert rows["d04-plan-confirm"]["plain"]["disabled"][:3] == [False, False, True]
|
||||
assert rows["e02-stab-ground"]["plain"]["disabled"][:3] == [False, True, False]
|
||||
assert rows["e05-stab-report"]["plain"]["disabled"][:3] == [False, False, True]
|
||||
|
||||
@@ -86,7 +86,7 @@ def test_공용이_도우미를_씀():
|
||||
leave = (ROOT / "A00_Common/design/design_leave.ts").read_text(encoding="utf-8")
|
||||
assert "guardProjectLeave(root, {" in leave
|
||||
state = (ROOT / "A00_Common/b_page_state.ts").read_text(encoding="utf-8")
|
||||
for name in ("d02-points", "e02-ground", "e03-prob", "e04-settings", "e05-report"):
|
||||
for name in ("e02-ground", "e03-prob", "e04-settings", "e05-report"):
|
||||
assert f'"{name}": {{ bucket: "draft", scope: "project"' in state
|
||||
assert '"e06-rock"' in state and '"e07-settings"' in state
|
||||
|
||||
|
||||
@@ -273,13 +273,13 @@ def test_register_ignores_sent_company():
|
||||
assert "company_name" not in data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind,count", [("ROAD", 7), ("PLAN", 3), ("STABILITY", 6)])
|
||||
@pytest.mark.parametrize("kind,count", [("ROAD", 7), ("PLAN", 2), ("STABILITY", 6)])
|
||||
def test_stage_seed_by_kind(kind, count):
|
||||
log = []
|
||||
asyncio.run(initialize_project_stages(FakeCursor({}, log), "p1", kind))
|
||||
assert len(log) == count
|
||||
if kind == "PLAN":
|
||||
assert [a[2] for _, a in log] == ["PLAN_BASE", "PLAN_ANALYSIS", "PLAN_CONFIRM"]
|
||||
assert [a[2] for _, a in log] == ["PLAN_ANALYSIS", "PLAN_CONFIRM"]
|
||||
if kind == "STABILITY": # 565-1 — 보고서 맨 뒤
|
||||
assert [a[2] for _, a in log][3:] == ["STAB_ROCK", "STAB_AREA", "STAB_REPORT"]
|
||||
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
"""D02 기준 지정(PLAN 529-3 · 560-2 · 579-1 · 585-1 · 590-1) — 경위도 ↔ TM 변환 · 지점 목록 API ·
|
||||
지점 = 설계 내용 파일 `points` · 옛 DB 칸(시점 · 종점 꼴 · 한 점 꼴 · 연도 행 접기)은 열 때 옮김 ·
|
||||
저장된 좌표계 그대로.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_529_3_plan_points.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from common_util.common_util_auth import require_company
|
||||
from common_util.common_util_crs import lonlat_to_tm, tm_epsg_for_lon, tm_to_lonlat
|
||||
from D02_PlanBase import D02_PlanBase_Router as router_mod
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Store as store
|
||||
|
||||
PID = "12953f5a-5ba1-42ad-a427-8ac857a58bc2"
|
||||
URL = f"/api/projects/{PID}/plan/points"
|
||||
|
||||
|
||||
# --- 좌표 변환 ---
|
||||
|
||||
|
||||
@pytest.mark.parametrize("epsg, lon0", [(5185, 125.0), (5186, 127.0), (5187, 129.0)])
|
||||
def test_origin_is_false_origin(epsg, lon0) -> None:
|
||||
x, y = lonlat_to_tm(lon0, 38.0, epsg) # 원점 = (200 000, 600 000)
|
||||
assert abs(x - 200000.0) < 0.001 and abs(y - 600000.0) < 0.001
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lon, lat", [(127.3851, 36.3504), (125.7, 34.1), (129.36, 36.02)])
|
||||
def test_round_trip_within_1mm(lon, lat) -> None:
|
||||
epsg = tm_epsg_for_lon(lon)
|
||||
x, y = lonlat_to_tm(lon, lat, epsg)
|
||||
back = lonlat_to_tm(*tm_to_lonlat(x, y, epsg), epsg)
|
||||
assert abs(back[0] - x) < 0.001 and abs(back[1] - y) < 0.001
|
||||
|
||||
|
||||
def test_belt_by_longitude() -> None:
|
||||
assert [tm_epsg_for_lon(v) for v in (124.5, 125.999, 126.0, 127.999, 128.0, 131.8)] == [
|
||||
5185,
|
||||
5185,
|
||||
5186,
|
||||
5186,
|
||||
5187,
|
||||
5187,
|
||||
]
|
||||
|
||||
|
||||
# --- 지점 정본 API ---
|
||||
|
||||
|
||||
A, B, C = (
|
||||
{"lon": 127.10, "lat": 37.50},
|
||||
{"lon": 127.12, "lat": 37.51},
|
||||
{"lon": 127.15, "lat": 37.52},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api(monkeypatch, tmp_path):
|
||||
"""파일 = tmp_path 의 진짜 파일 · 없으면 옛 DB 칸(`base_json` · 연도 행)을 옮김."""
|
||||
state = {
|
||||
"base_json": None,
|
||||
"rows": [],
|
||||
"root": tmp_path,
|
||||
"session": {"role": "USER", "email": "kim@forest.go.kr", "company_id": 1},
|
||||
}
|
||||
|
||||
async def load(project_id):
|
||||
plan = store.read_plan(tmp_path)
|
||||
return tmp_path, plan or store.migrate(state["base_json"], None, state["rows"])
|
||||
|
||||
monkeypatch.setattr(store, "load_plan", load)
|
||||
app = FastAPI()
|
||||
app.include_router(router_mod.router)
|
||||
app.dependency_overrides[require_company] = lambda: state["session"]
|
||||
client = TestClient(app)
|
||||
client.state = state
|
||||
return client
|
||||
|
||||
|
||||
def test_put_lists(api) -> None:
|
||||
got = api.put(URL, json={"points": [A, B, C]})
|
||||
assert got.status_code == 200
|
||||
data = got.json()
|
||||
assert data["epsg"] == 5186 and len(data["points"]) == 3
|
||||
assert (data["points"][0]["x"], data["points"][0]["y"]) == pytest.approx(
|
||||
lonlat_to_tm(127.10, 37.50, 5186)
|
||||
)
|
||||
end = data["points"][2]
|
||||
assert abs(end["lon"] - C["lon"]) < 1e-8 and abs(end["lat"] - C["lat"]) < 1e-8
|
||||
stored = json.loads(store.plan_path(api.state["root"]).read_text("utf-8"))
|
||||
assert stored["version"] == 1 and stored["epsg"] == 5186 and stored["lines"] == []
|
||||
assert set(stored["points"][1]) == {"x", "y"}
|
||||
|
||||
|
||||
def test_put_partial_and_empty(api) -> None:
|
||||
data = api.put(URL, json={"points": [A]}).json()
|
||||
assert len(data["points"]) == 1 and data["epsg"] == 5186
|
||||
data = api.put(URL, json={}).json()
|
||||
assert data == {"status": "success", "epsg": None, "points": []}
|
||||
assert api.put(URL, json={"points": [A] * 40}).status_code == 200
|
||||
assert api.put(URL, json={"points": [A] * 41}).status_code == 422 # 상한 40(옛 20 + 20)
|
||||
|
||||
|
||||
def test_put_keeps_stored_epsg(api) -> None:
|
||||
api.put(URL, json={"points": [A, C]})
|
||||
far = {"lon": 128.9, "lat": 37.5} # 평균이 128° 를 넘어도 저장된 5186 그대로
|
||||
data = api.put(URL, json={"points": [A, far, far]}).json()
|
||||
assert data["epsg"] == 5186
|
||||
|
||||
|
||||
def test_get_joins_old_start_end_lists(api) -> None:
|
||||
a, b, c = (lonlat_to_tm(p["lon"], p["lat"], 5186) for p in (A, B, C))
|
||||
old = {"epsg": 5186, "starts": [{"x": a[0], "y": a[1]}, {"x": b[0], "y": b[1]}],
|
||||
"ends": [{"x": c[0], "y": c[1]}]} # fmt: skip
|
||||
api.state["base_json"] = json.dumps(old)
|
||||
data = api.get(URL).json()
|
||||
assert [p["x"] for p in data["points"]] == pytest.approx([a[0], b[0], c[0]]) # 시점 뒤 종점
|
||||
|
||||
|
||||
def test_get_wraps_old_single_points(api) -> None:
|
||||
a, c = (lonlat_to_tm(p["lon"], p["lat"], 5186) for p in (A, C))
|
||||
old = {"epsg": 5186, "start": {"x": a[0], "y": a[1]}, "end": {"x": c[0], "y": c[1]}}
|
||||
api.state["base_json"] = json.dumps(old)
|
||||
data = api.get(URL).json()
|
||||
assert [p["x"] for p in data["points"]] == pytest.approx([a[0], c[0]])
|
||||
old["end"] = None
|
||||
api.state["base_json"] = json.dumps(old)
|
||||
assert [p["x"] for p in api.get(URL).json()["points"]] == pytest.approx([a[0]])
|
||||
|
||||
|
||||
def test_put_forest_only(api) -> None:
|
||||
api.state["session"] = {"role": "USER", "email": "lee@company.com", "company_id": 1}
|
||||
assert api.put(URL, json={"points": [A, B]}).status_code == 403
|
||||
api.state["session"] = {"role": "SYSTEM_ADMIN", "email": "admin@x.com", "company_id": None}
|
||||
assert api.put(URL, json={"points": [A, B]}).status_code == 200
|
||||
|
||||
|
||||
def test_get_folds_old_year_rows(api) -> None:
|
||||
epsg = 5186
|
||||
a, b, c = (lonlat_to_tm(p["lon"], p["lat"], epsg) for p in (A, B, C))
|
||||
for year, s, e in ((2026, a, b), (2027, b, c)):
|
||||
api.state["rows"].append(
|
||||
{
|
||||
"year": year,
|
||||
**{"start_x": s[0], "start_y": s[1], "end_x": e[0], "end_y": e[1]},
|
||||
"epsg": epsg,
|
||||
}
|
||||
)
|
||||
data = api.get(URL).json()
|
||||
assert data["epsg"] == epsg
|
||||
assert [p["x"] for p in data["points"]] == pytest.approx([a[0], c[0]])
|
||||
|
||||
|
||||
def test_get_empty_new_project(api) -> None:
|
||||
assert api.get(URL).json() == {"status": "success", "epsg": None, "points": []}
|
||||
|
||||
|
||||
def test_put_keeps_line_epsg(api) -> None:
|
||||
"""선이 있으면 지점을 다 지워도 좌표계 그대로(파일 머리 한 곳 · 선 좌표가 그 좌표계)."""
|
||||
api.put(URL, json={"points": [A]})
|
||||
path = store.plan_path(api.state["root"])
|
||||
plan = json.loads(path.read_text("utf-8"))
|
||||
plan["lines"] = [{"id": "main", "vertices": [[0.0, 0.0], [1.0, 0.0]], "branch": None}]
|
||||
path.write_text(json.dumps(plan), "utf-8")
|
||||
assert api.put(URL, json={}).json()["points"] == []
|
||||
assert json.loads(path.read_text("utf-8"))["epsg"] == 5186
|
||||
@@ -177,7 +177,7 @@ class _Connection:
|
||||
class _Db:
|
||||
def __init__(self):
|
||||
self.log: list = []
|
||||
self.answers: dict = {"FROM projects": [(2026,)]}
|
||||
self.answers: dict = {"FROM projects": [(2026, None, None, None)]} # 연도 · 기준점(591-2)
|
||||
|
||||
def acquire(self):
|
||||
return _Connection(self)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""D 지형 API(PLAN 530-1 · 561-2) — 범위 = 전체 시점 · 종점(D02 정본) 상자 + 여유 · 받기 = 한 벌.
|
||||
"""D 지형 API(PLAN 530-1 · 561-2 · 591-2) — 범위 = 지점(설계 내용 파일) 상자 + 여유 · 지점 · 선이
|
||||
없으면 등록 기준점 둘레 ± 1 km · 받기 = 한 벌.
|
||||
|
||||
옛 분석 API 시험(GET · PUT analysis · 꺾임점 고침)은 옛 추천 엔진과 함께 걷음(PLAN 562-2) ·
|
||||
노선 정본 시험 = `test_530_2_plan_analyze.py` · 지형 셈 시험 = `test_530_1_plan_terrain.py`.
|
||||
@@ -31,26 +32,25 @@ class _Pool:
|
||||
def client(tmp_path, monkeypatch):
|
||||
plan = {
|
||||
"epsg": 5186,
|
||||
"points": [
|
||||
{"lon": 0, "lat": 0, "x": P0[0], "y": P0[1]},
|
||||
{"lon": 0, "lat": 0, "x": P2[0], "y": P2[1]},
|
||||
],
|
||||
"points": [{"x": P0[0], "y": P0[1]}, {"x": P2[0], "y": P2[1]}],
|
||||
"lines": [],
|
||||
}
|
||||
info = {"year_start": 2026, "site": None}
|
||||
|
||||
async def load_base(project_id):
|
||||
return plan
|
||||
async def load_plan(project_id):
|
||||
return tmp_path, plan
|
||||
|
||||
async def stored(connection, project_id):
|
||||
return "1/3/p"
|
||||
async def project_info(project_id):
|
||||
return info
|
||||
|
||||
monkeypatch.setattr(router_mod.base, "load_base", load_base)
|
||||
monkeypatch.setattr(router_mod.store, "load_plan", load_plan)
|
||||
monkeypatch.setattr(router_mod.network, "project_info", project_info)
|
||||
monkeypatch.setattr(router_mod, "get_db_pool", lambda: _Pool())
|
||||
monkeypatch.setattr(router_mod, "get_project_storage_relative_path", stored)
|
||||
monkeypatch.setattr(router_mod, "resolve_stored_project_path", lambda s: str(tmp_path))
|
||||
app = FastAPI()
|
||||
app.include_router(router_mod.router)
|
||||
test = TestClient(app)
|
||||
test.plan = plan
|
||||
test.info = info
|
||||
return test
|
||||
|
||||
|
||||
@@ -63,10 +63,28 @@ def test_extent_uses_whole_ends(client) -> None:
|
||||
extra = client.get(f"{URL}/extent", params={"extra": json.dumps([[2000, 0]])}).json()
|
||||
assert extra["box_m"][2] == 2000 + extra["margin_m"]
|
||||
assert client.get(f"{URL}/extent", params={"extra": "[1]"}).status_code == 400
|
||||
client.plan["points"] = [] # 지점 빔 — 1단계로
|
||||
client.plan["points"] = [] # 지점 · 기준점 빔
|
||||
assert client.get(f"{URL}/extent").status_code == 400
|
||||
|
||||
|
||||
def test_extent_site_first(client) -> None:
|
||||
"""591-2 — 지점 · 선 없음 = 기준점 둘레 ± 1 km(처음 여유 포함) ·
|
||||
좌표계 = 파일 머리 없으면 기준점 띠."""
|
||||
from common_util.common_util_crs import lonlat_to_tm
|
||||
|
||||
client.plan.update(epsg=None, points=[])
|
||||
client.info["site"] = {"lon": 127.8, "lat": 36.3, "address": None}
|
||||
got = client.get(f"{URL}/extent").json()
|
||||
x, y = lonlat_to_tm(127.8, 36.3, 5186)
|
||||
r = router_mod.terrain.SITE_RADIUS_M
|
||||
assert got["epsg"] == 5186
|
||||
assert got["box_m"] == pytest.approx([x - r, y - r, x + r, y + r])
|
||||
# 지점이 있으면 기준점은 안 넣음(받은 범위가 이미 덮음 · 옛 프로젝트 그대로)
|
||||
client.plan.update(epsg=5186, points=[{"x": P0[0], "y": P0[1]}])
|
||||
got = client.get(f"{URL}/extent?margin_m=400").json()
|
||||
assert got["box_m"] == [P0[0] - 400, P0[1] - 400, P0[0] + 400, P0[1] + 400]
|
||||
|
||||
|
||||
def test_post_runs_one_set(client, monkeypatch) -> None:
|
||||
calls: list = []
|
||||
saved: list = []
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_thumb_line_fits_unit_box_and_caps_points():
|
||||
|
||||
def test_stage_names_follow_step_bar():
|
||||
assert len(summary.stage_names("ROAD")) == 7
|
||||
assert summary.stage_names("PLAN")[0] == "기준 지정"
|
||||
assert summary.stage_names("PLAN")[0] == "노선 분석" # 591-2 기준 지정 걷음
|
||||
assert summary.stage_names("EROSION") == summary.stage_names("ROAD")
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ def test_summaries_in_batch(monkeypatch, tmp_path):
|
||||
assert a["stale_days"] == 20 and a["owner_names"] == ["엄", "박"]
|
||||
assert a["metrics"] == {"length_m": 15.0, "stations": 3, "structures": 2}
|
||||
assert len(a["thumb"]) == 3 and a["done"] is False
|
||||
assert b["thumb"] is None and b["metrics"] == {} and b["stage_name"] == "기준 지정"
|
||||
assert b["thumb"] is None and b["metrics"] == {} and b["stage_name"] == "노선 분석"
|
||||
assert db["queries"] == 5
|
||||
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ def test_board_column_road():
|
||||
|
||||
|
||||
def test_board_column_other_kinds():
|
||||
assert board_column("PLAN", stages(C, C, P)) == "confirm"
|
||||
assert board_column("PLAN", stages(C, C, C)) == "done"
|
||||
assert board_column("PLAN", stages(C, P)) == "confirm"
|
||||
assert board_column("PLAN", stages(C, C)) == "done"
|
||||
assert board_column("STABILITY", stages(C, P, N, N, N, N)) == "design"
|
||||
assert board_column("STABILITY", stages(N, N, N, N, N)) == "start"
|
||||
# 모르는 종류(사방) = 단계 없음 → 시작 전 · 설계 중
|
||||
@@ -47,7 +47,7 @@ def test_board_column_other_kinds():
|
||||
|
||||
|
||||
def test_outputs_only_when_ready():
|
||||
assert project_outputs("PLAN", stages(C, C, C)) == []
|
||||
assert project_outputs("PLAN", stages(C, C)) == []
|
||||
assert project_outputs("ROAD", stages(C, C, C, P, N, N, N)) == []
|
||||
keys = lambda st: [o["key"] for o in project_outputs("ROAD", st)] # noqa: E731
|
||||
assert keys(stages(C, C, C, C, P, N, N)) == ["drawings"]
|
||||
|
||||
@@ -21,7 +21,7 @@ def tmp_path(): # 기본 pytest 임시 폴더가 이 PC 에서 막힘 — 직
|
||||
|
||||
|
||||
def test_every_page_has_manual_first():
|
||||
for page in [f"B0{n}" for n in range(2, 10)] + [f"D0{n}" for n in range(1, 5)]:
|
||||
for page in [f"B0{n}" for n in range(2, 10)] + ["D01", "D03", "D04"]: # D02 = 591-2 걷음
|
||||
items = read_help(page)
|
||||
assert items and items[0]["key"] == "00_설명서", page
|
||||
for n in range(1, 8):
|
||||
|
||||
@@ -16,15 +16,13 @@ def _no_hex(path: str) -> None:
|
||||
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", css), f"{path}: 날 색"
|
||||
|
||||
|
||||
def test_d02_parts():
|
||||
ts = _src("D02_PlanBase/D02_PlanBase_UI_Page.ts")
|
||||
css = _src("D02_PlanBase/D02_PlanBase_UI_Style.css")
|
||||
assert 'el("input"' not in ts
|
||||
assert 'createInputField({ type: "number"' in ts
|
||||
assert 'classList.add("d02-info")' in ts # 촬영 장면이 `p.d02-info` 로 잡음
|
||||
for dead in (".d02-info", "is-invalid", "font-size: var(--text-caption);\n}\n\n.d02-info"):
|
||||
assert dead not in css
|
||||
assert "data-axis" in ts or "dataset.axis" in ts
|
||||
def test_d03_points_parts():
|
||||
"""591-2 — D02 걷음 · 지점 칸 = 목록 줄 · 아이콘 단추 · 우클릭 메뉴 부품."""
|
||||
assert not (ROOT / "D02_PlanBase" / "D02_PlanBase_UI_Page.ts").exists()
|
||||
ts = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Points.ts")
|
||||
assert "createListRow(" in ts and "createIconButton(" in ts and "createMapContextMenu(" in ts
|
||||
for raw in ('createElement("button")', 'createElement("input")', 'el("input"'):
|
||||
assert raw not in ts
|
||||
|
||||
|
||||
def test_d03_parts():
|
||||
|
||||
@@ -10,7 +10,6 @@ PAGES = (
|
||||
"B08_Quantity",
|
||||
"B09_Estimation",
|
||||
"D01_PlanRegister",
|
||||
"D02_PlanBase",
|
||||
"D03_PlanAnalysis",
|
||||
"D04_PlanConfirm",
|
||||
"E01_StabRegister",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""560-4 D 단계 이동 = 저장 뒤 이동 · 572-2 B 떠남 물음 지킴 시험 — 서버 · 화면 없이 소스만 본다.
|
||||
단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로가 한 함수(beforeLeave)를 거치고 · D02 첫 불러오기 실패에도 지도를 세움.
|
||||
단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로가 한 함수(beforeLeave)를 거치고 (D02 기준 지정은 591-2 에서 걷음 · 지점 = D03).
|
||||
572-3 — D · E 페이지는 공용 가드에 {dirty, save} 를 둠(말없이 저장 없음) · 캐시 = 등록표 · 프로젝트 = currentProjectId."""
|
||||
|
||||
import re
|
||||
@@ -19,15 +19,6 @@ def test_group_step_waits_before_leave():
|
||||
assert "leave().then((ok) => ok && navigateTo(" in ts # 단계 아이콘 · [<] [>]
|
||||
|
||||
|
||||
def test_d02_leave_saves_and_builds_map_on_fail():
|
||||
ts = _src("D02_PlanBase/D02_PlanBase_UI_Page.ts")
|
||||
assert "dirty:" in ts and "save:" in ts and "ui-step-nav" not in ts # 따로 듣개 없음
|
||||
tail = ts[ts.rindex(" try {") :]
|
||||
# 불러오기 catch 뒤에 paint() — 실패해도 지도 · 표를 세움
|
||||
assert re.search(r"\} catch \(error\) \{[^}]*\}\s*active = firstEmpty\(\);\s*paint\(\);", tail)
|
||||
assert "sameBase" in ts # 옛 캐시는 그때 서버 값이 지금과 같을 때만
|
||||
|
||||
|
||||
# 572-2 B 페이지 떠남 물음 — 공용 가드에 {dirty, save} · 지금 프로젝트 = 탭마다(currentProjectId)
|
||||
B_GUARDS = {
|
||||
"B04_PreProcess/B04_PreProcess_UI_Page.ts": (
|
||||
@@ -89,7 +80,6 @@ def test_b_pages_leave_other_project_cache_to_enter():
|
||||
|
||||
# 572-3 D · E — 고침 판별 · 저장 · [폐기] 때 지울 등록표 칸(E03 = 별 저장 진행 중만 · 지울 칸 없음)
|
||||
DE_GUARDED = {
|
||||
"D02_PlanBase/D02_PlanBase_UI_Page.ts": "d02-points",
|
||||
"D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts": "d03-plan",
|
||||
"D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts": "d04-assignees",
|
||||
"E02_StabGround/E02_StabGround_UI_Page.ts": "e02-ground",
|
||||
@@ -104,7 +94,6 @@ DE_PAGES = [
|
||||
"E07_StabArea/E07_StabArea_UI_Page.ts",
|
||||
]
|
||||
DE_STATES = [
|
||||
"d02-points",
|
||||
"e02-ground",
|
||||
"e03-prob",
|
||||
"e04-settings",
|
||||
@@ -124,7 +113,7 @@ def test_de_pages_guard_leave():
|
||||
if name:
|
||||
assert f'discard: () => clearState("{name}", projectId)' in call, path
|
||||
# D03 — 못 읽은 채(프로젝트 없음 · 불러오기 실패) 떠나면 고침 없음
|
||||
assert "let savedJson = JSON.stringify(network);" in _src(
|
||||
assert "let savedJson = JSON.stringify({ network, points });" in _src(
|
||||
"D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts"
|
||||
)
|
||||
e03 = _src("E03_StabSection/E03_StabSection_UI_Page.ts")
|
||||
|
||||
@@ -160,8 +160,8 @@ def test_two_saves_do_not_interleave(monkeypatch, tmp_path):
|
||||
async def load(_pid):
|
||||
return tmp_path, store.read_plan(tmp_path) or store.empty_plan()
|
||||
|
||||
async def year_start(_pid):
|
||||
return 2026
|
||||
async def project_info(_pid):
|
||||
return {"year_start": 2026, "site": None}
|
||||
|
||||
async def slow_status(connection, project_id, pieces, years):
|
||||
order.append(f"start {len(pieces)}")
|
||||
@@ -189,7 +189,7 @@ def test_two_saves_do_not_interleave(monkeypatch, tmp_path):
|
||||
return Conn()
|
||||
|
||||
monkeypatch.setattr(store, "load_plan", load)
|
||||
monkeypatch.setattr(net, "_year_start", year_start)
|
||||
monkeypatch.setattr(net, "project_info", project_info)
|
||||
monkeypatch.setattr(net, "_store_status", slow_status)
|
||||
monkeypatch.setattr(net, "get_db_pool", lambda: Pool())
|
||||
forest = {"role": "SYSTEM_ADMIN", "email": "a@b.kr"}
|
||||
|
||||
@@ -108,5 +108,6 @@ def test_화면_지도_출처():
|
||||
|
||||
|
||||
def test_D04_연결_설계속도_안_넘김():
|
||||
# 590-1 이 씨앗 줄을 걷음 — 연결이 설계속도를 안 넘김(B05 기본 20)
|
||||
link = (ROOT / "D04_PlanConfirm" / "D04_PlanConfirm_Link.py").read_text(encoding="utf-8")
|
||||
assert "design_speed" not in link # 예정노선 설계속도 씨앗 걷음(590-1) → B05 기본 20
|
||||
assert "design_speed" not in link
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
"""591-2 D02 기준 지정 걷음 · 지점 = D03 노선분석(요구 452 · 453) — 옛 529-3 시험 갈음.
|
||||
|
||||
- 지점 = 설계 내용 파일 `points` · D03 [저장](PUT plan/network) 몸에 실림 ·
|
||||
빈 값 = 저장된 지점 그대로 · [초기화](빈 선) 뒤에도 지점 남음 · 상한 40 · 산림청만.
|
||||
- GET = 지점(TM + 경위도) · 등록 기준점 · 좌표계(파일 머리 · 없으면 기준점 경도 TM 띠).
|
||||
- 옛 지점 칸(시점 · 종점 목록 · 한 점 꼴)은 열 때 옮겨 보임.
|
||||
- 핀 끌기 = 그 자리 노선 노드 같이(TS `moveNodesAt` · 붙은 갈래 첫 점 · 다른 점은 그대로).
|
||||
- D02 길 · 단계 · 폴더가 남지 않음.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_591_2_plan_points.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from common_util.common_util_auth import require_company
|
||||
from common_util.common_util_crs import lonlat_to_tm, tm_epsg_for_lon, tm_to_lonlat
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Network as net
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Store as store
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PID = "12953f5a-5ba1-42ad-a427-8ac857a58bc2"
|
||||
URL = f"/api/projects/{PID}/plan/network"
|
||||
SITE = {"lon": 127.11, "lat": 37.505, "address": "경기도 어딘가 산 1"}
|
||||
A, B = lonlat_to_tm(127.10, 37.50, 5186), lonlat_to_tm(127.12, 37.51, 5186)
|
||||
LINE = {"id": "main", "vertices": [list(A), list(B)], "branch": None}
|
||||
|
||||
|
||||
# --- 좌표 변환(옛 529-3 에서 옮김) ---
|
||||
|
||||
|
||||
@pytest.mark.parametrize("epsg, lon0", [(5185, 125.0), (5186, 127.0), (5187, 129.0)])
|
||||
def test_origin_is_false_origin(epsg, lon0) -> None:
|
||||
x, y = lonlat_to_tm(lon0, 38.0, epsg) # 원점 = (200 000, 600 000)
|
||||
assert abs(x - 200000.0) < 0.001 and abs(y - 600000.0) < 0.001
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lon, lat", [(127.3851, 36.3504), (125.7, 34.1), (129.36, 36.02)])
|
||||
def test_round_trip_within_1mm(lon, lat) -> None:
|
||||
epsg = tm_epsg_for_lon(lon)
|
||||
x, y = lonlat_to_tm(lon, lat, epsg)
|
||||
back = lonlat_to_tm(*tm_to_lonlat(x, y, epsg), epsg)
|
||||
assert abs(back[0] - x) < 0.001 and abs(back[1] - y) < 0.001
|
||||
|
||||
|
||||
# --- 지점 = D03 [저장] ---
|
||||
|
||||
|
||||
class _Conn:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
async def begin(self):
|
||||
pass
|
||||
|
||||
async def commit(self):
|
||||
pass
|
||||
|
||||
async def rollback(self):
|
||||
pass
|
||||
|
||||
|
||||
class _Pool:
|
||||
def acquire(self):
|
||||
return _Conn()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api(monkeypatch, tmp_path):
|
||||
"""파일 = tmp_path 의 진짜 파일 · 없으면 옛 DB 칸(`base_json`)을 옮김 · 연도 상태 줄은 안 씀."""
|
||||
state = {
|
||||
"base_json": None,
|
||||
"root": tmp_path,
|
||||
"site": SITE,
|
||||
"session": {"role": "USER", "email": "kim@forest.go.kr", "company_id": 1},
|
||||
}
|
||||
|
||||
async def load(project_id):
|
||||
plan = store.read_plan(tmp_path)
|
||||
return tmp_path, plan or store.migrate(state["base_json"], None, [])
|
||||
|
||||
async def project_info(project_id):
|
||||
return {"year_start": 2026, "site": state["site"]}
|
||||
|
||||
async def no_status(*args):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(store, "load_plan", load)
|
||||
monkeypatch.setattr(net, "project_info", project_info)
|
||||
monkeypatch.setattr(net, "_store_status", no_status)
|
||||
monkeypatch.setattr(net, "get_db_pool", lambda: _Pool())
|
||||
app = FastAPI()
|
||||
app.include_router(net.router)
|
||||
app.dependency_overrides[require_company] = lambda: state["session"]
|
||||
client = TestClient(app)
|
||||
client.state = state
|
||||
return client
|
||||
|
||||
|
||||
def _saved(api) -> dict:
|
||||
return json.loads(store.plan_path(api.state["root"]).read_text("utf-8"))
|
||||
|
||||
|
||||
def test_new_project_site_and_epsg(api) -> None:
|
||||
base = api.get(URL).json()["base"]
|
||||
assert base == {"epsg": 5186, "points": [], "site": SITE} # 기준점 경도 TM 띠
|
||||
api.state["site"] = None
|
||||
assert api.get(URL).json()["base"]["epsg"] is None # 기준점 · 지점 없음 = 화면 안내
|
||||
|
||||
|
||||
def test_save_points_with_lines(api) -> None:
|
||||
body = {"epsg": 5186, "points": [list(A), list(B)], "lines": [LINE]}
|
||||
got = api.put(URL, json=body)
|
||||
assert got.status_code == 200
|
||||
points = got.json()["base"]["points"]
|
||||
assert [(p["x"], p["y"]) for p in points] == pytest.approx([A, B])
|
||||
assert abs(points[0]["lon"] - 127.10) < 1e-8 and abs(points[1]["lat"] - 37.51) < 1e-8
|
||||
saved = _saved(api)
|
||||
assert saved["epsg"] == 5186 and set(saved["points"][0]) == {"x", "y"}
|
||||
assert saved["lines"][0]["id"] == "main"
|
||||
|
||||
|
||||
def test_points_kept_when_absent_and_after_reset(api) -> None:
|
||||
api.put(URL, json={"epsg": 5186, "points": [list(A)], "lines": [LINE]})
|
||||
api.put(URL, json={"epsg": 5186, "lines": [LINE]}) # 빈 값 = 그대로
|
||||
assert len(_saved(api)["points"]) == 1
|
||||
# [초기화] = 빈 선 · 지점 실어 보냄 → 선만 지워짐
|
||||
reset = api.put(URL, json={"epsg": 5186, "points": [list(A)], "lines": []}).json()
|
||||
assert reset["network"] is None and len(reset["base"]["points"]) == 1
|
||||
assert _saved(api)["lines"] == [] and len(_saved(api)["points"]) == 1
|
||||
api.put(URL, json={"epsg": 5186, "points": [], "lines": []})
|
||||
assert _saved(api)["points"] == []
|
||||
|
||||
|
||||
def test_points_limit_and_forest_only(api) -> None:
|
||||
many = [list(A)] * 40
|
||||
assert api.put(URL, json={"epsg": 5186, "points": many, "lines": []}).status_code == 200
|
||||
many.append(list(A))
|
||||
assert api.put(URL, json={"epsg": 5186, "points": many, "lines": []}).status_code == 422
|
||||
api.state["session"] = {"role": "USER", "email": "lee@company.com", "company_id": 1}
|
||||
assert api.put(URL, json={"epsg": 5186, "points": [], "lines": []}).status_code == 403
|
||||
|
||||
|
||||
def test_epsg_stays_once_saved(api) -> None:
|
||||
api.put(URL, json={"epsg": 5186, "points": [list(A)], "lines": []})
|
||||
got = api.put(URL, json={"epsg": 5187, "points": [list(A)], "lines": []})
|
||||
assert got.status_code == 400 # 파일 머리 좌표계 한 곳
|
||||
|
||||
|
||||
def test_old_start_end_points_visible(api) -> None:
|
||||
old = {"epsg": 5186, "starts": [{"x": A[0], "y": A[1]}], "ends": [{"x": B[0], "y": B[1]}]}
|
||||
api.state["base_json"] = json.dumps(old)
|
||||
points = api.get(URL).json()["base"]["points"]
|
||||
assert [p["x"] for p in points] == pytest.approx([A[0], B[0]]) # 시점 뒤 종점
|
||||
api.state["base_json"] = json.dumps({"epsg": 5186, "start": {"x": A[0], "y": A[1]}})
|
||||
assert [p["x"] for p in api.get(URL).json()["base"]["points"]] == pytest.approx([A[0]])
|
||||
|
||||
|
||||
# --- D02 걷음 ---
|
||||
|
||||
|
||||
def test_d02_removed() -> None:
|
||||
assert not (ROOT / "D02_PlanBase" / "D02_PlanBase_Router.py").exists()
|
||||
assert "D02_PlanBase" not in (ROOT / "main.py").read_text(encoding="utf-8")
|
||||
front = (ROOT / "config" / "config_frontend.ts").read_text(encoding="utf-8")
|
||||
assert "D02_PLAN_BASE" not in front and "d02-plan-base" not in front
|
||||
from common_util.common_util_project_kind import KIND_STAGE_DIRS
|
||||
from common_util.common_util_workflow_state import STAGE_KEYS_BY_KIND
|
||||
|
||||
assert STAGE_KEYS_BY_KIND["PLAN"] == ["PLAN_ANALYSIS", "PLAN_CONFIRM"]
|
||||
assert "D02_PlanBase" not in KIND_STAGE_DIRS["PLAN"]
|
||||
for path in (
|
||||
"D01_PlanRegister/D01_PlanRegister_UI_Page.ts",
|
||||
"B01_Dashboard/B01_Dashboard_UI_View_Assigned.ts",
|
||||
):
|
||||
assert "ROUTES.D03_PLAN_ANALYSIS" in (ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# --- 핀 끌기 = 노드 같이(TS) ---
|
||||
|
||||
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Plan.ts"
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
_RUNNER = """
|
||||
import { writeFileSync } from "node:fs";
|
||||
import * as plan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.js";
|
||||
const Y = 2026;
|
||||
// 본선 (0,0)→(100,0)→(100,100) · 갈래 (100,0)→(150,0) · 다른 본선이 (100,0) 에서 시작
|
||||
let n = plan.newLine(plan.emptyNetwork(), [0, 0]);
|
||||
n = plan.appendVertex(plan.appendVertex(n, 0, [100, 0]), 0, [100, 100]);
|
||||
n = plan.appendVertex(plan.addBranch(n, 0, 1), 1, [150, 0]);
|
||||
n = plan.appendVertex(plan.newLine(n, [100, 0]), 2, [100, -50]);
|
||||
n = plan.normalizePieces(n, Y);
|
||||
const moved = plan.moveNodesAt(n, [100, 0], [110, 5]);
|
||||
const none = plan.moveNodesAt(n, [55, 55], [0, 0]);
|
||||
writeFileSync(process.argv[2], JSON.stringify({
|
||||
lines: moved.lines.map((l) => l.vertices),
|
||||
same: none === n,
|
||||
pieces: plan.normalizePieces(moved, Y).pieces.map((p) => [p.line, p.to_m]),
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
def _run_ts(tmp_path: Path) -> dict:
|
||||
out = tmp_path / "js"
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(TSC), str(TS_FILE), "--outDir", str(out), "--rootDir", str(ROOT),
|
||||
"--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler",
|
||||
"--ignoreConfig"],
|
||||
cwd=str(ROOT), check=True, capture_output=True,
|
||||
) # fmt: skip
|
||||
for emitted in out.rglob("*.js"):
|
||||
emitted.write_text(
|
||||
re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
result = tmp_path / "out.json"
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(out / "runner.mjs"), str(result)],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return json.loads(result.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_pin_drag_moves_nodes(tmp_path: Path) -> None:
|
||||
got = _run_ts(tmp_path)
|
||||
main, branch, other = got["lines"]
|
||||
assert main == [[0, 0], [110, 5], [100, 100]] # 지점 자리 꺾임점
|
||||
assert branch[0] == [110, 5] and branch[1] == [150, 0] # 붙은 갈래 첫 점 같이
|
||||
assert other == [[110, 5], [100, -50]] # 같은 자리 다른 선 머리도
|
||||
assert got["same"] # 꺾임점 없는 자리 = 그대로
|
||||
lengths = dict(got["pieces"])
|
||||
# 조각 = 새 길이(1 mm 반올림)
|
||||
assert lengths["main"] == pytest.approx(math.hypot(110, 5) + math.hypot(10, 95), abs=1e-3)
|
||||
Reference in New Issue
Block a user