Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
|
||||
길이 · 측점 · 높이는 저장하지 않음(늘 다시 셈 · 높이는 지형에서). 측점 = 선마다 No.0 부터.
|
||||
조각 = 선의 마크 사이 구간(구역 × 갈래) · 업체 지정은 조각 단위(`plan_route_assignees.piece_id`).
|
||||
전체 시점 · 종점(`base`) = D02 정본(`plan_base.load_base`) — 비어 옛 연도 행을 접어 보이던 것은
|
||||
[저장] 전에 칸에 굳힘(연도 행이 바뀌어도 그대로).
|
||||
계산 자리 = ① 파이썬·TS 짝(`D03_PlanAnalysis_Draw`) — 화면은 그리는 즉시 · [저장]은 여기서 다시 셈.
|
||||
"""
|
||||
|
||||
@@ -27,6 +29,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from common_util.common_util_auth import is_forest_service, require_company
|
||||
from config.config_db import get_db_pool
|
||||
from D02_PlanBase import D02_PlanBase_Router as plan_base
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Draw as draw
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["D03 Plan Network"])
|
||||
@@ -266,9 +269,10 @@ async def _load(connection: Any, project_id: UUID) -> tuple[dict[str, Any] | Non
|
||||
return legacy, legacy is not None
|
||||
|
||||
|
||||
def _view(network: dict[str, Any] | None, legacy: bool) -> dict[str, Any]:
|
||||
def _view(network: dict[str, Any] | None, legacy: bool, base: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "success",
|
||||
"base": base,
|
||||
"network": network,
|
||||
"derived": derive(network) if network else None,
|
||||
"legacy": legacy,
|
||||
@@ -277,10 +281,11 @@ def _view(network: dict[str, Any] | None, legacy: bool) -> dict[str, Any]:
|
||||
|
||||
@router.get("/{project_id}/plan/network", response_model=None)
|
||||
async def get_plan_network(project_id: UUID) -> dict[str, Any]:
|
||||
base = await plan_base.load_base(project_id)
|
||||
async with get_db_pool().acquire() as connection:
|
||||
network, legacy = await _load(connection, project_id)
|
||||
try:
|
||||
return _view(network, legacy)
|
||||
return _view(network, legacy, base)
|
||||
except NetworkError as error:
|
||||
raise HTTPException(status_code=409, detail=f"저장된 노선이 틀립니다 — {error}") from error
|
||||
|
||||
@@ -374,6 +379,19 @@ async def _store(
|
||||
)
|
||||
|
||||
|
||||
async def _freeze_base(connection: Any, project_id: UUID, base: dict[str, Any]) -> None:
|
||||
"""옛 연도 행을 접은 시점 · 종점 → D02 정본 칸(비었을 때만)."""
|
||||
stored = {
|
||||
"epsg": base["epsg"],
|
||||
**{key: {"x": base[key]["x"], "y": base[key]["y"]} for key in ("start", "end")},
|
||||
}
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"UPDATE projects SET plan_base_json = %s WHERE id = %s AND plan_base_json IS NULL",
|
||||
(json.dumps(stored), str(project_id)),
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{project_id}/plan/network", response_model=None)
|
||||
async def put_plan_network(
|
||||
project_id: UUID,
|
||||
@@ -384,23 +402,27 @@ async def put_plan_network(
|
||||
if not is_forest_service(session):
|
||||
raise HTTPException(status_code=403, detail="예정노선은 산림청만 고칠 수 있습니다.")
|
||||
network = payload.model_dump()
|
||||
base = await plan_base.load_base(project_id) # 예정노선 프로젝트가 아니면 404 · 400
|
||||
if base["epsg"] is not None and int(base["epsg"]) != network["epsg"]:
|
||||
raise HTTPException(status_code=400, detail="좌표계가 기준 지정과 다릅니다.")
|
||||
async with get_db_pool().acquire() as connection:
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"SELECT project_year FROM projects WHERE id = %s", (str(project_id),)
|
||||
"SELECT project_year, plan_base_json FROM projects WHERE id = %s",
|
||||
(str(project_id),),
|
||||
)
|
||||
project = await cursor.fetchone()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
|
||||
try:
|
||||
derived = derive(network, project["project_year"])
|
||||
except NetworkError as error:
|
||||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||||
await connection.begin()
|
||||
try:
|
||||
if project["plan_base_json"] is None and base["start"] and base["end"]:
|
||||
await _freeze_base(connection, project_id, base)
|
||||
await _store(connection, project_id, network, derived)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
raise
|
||||
return {"status": "success", "network": network, "derived": derived, "legacy": False}
|
||||
return {**_view(network, False, base), "derived": derived}
|
||||
|
||||
@@ -174,7 +174,7 @@ class _Connection:
|
||||
class _Db:
|
||||
def __init__(self):
|
||||
self.log: list = []
|
||||
self.answers: dict = {"FROM projects": [{"project_year": 2026}]}
|
||||
self.answers: dict = {"FROM projects": [{"project_year": 2026, "plan_base_json": "{}"}]}
|
||||
|
||||
def acquire(self):
|
||||
return _Connection(self)
|
||||
@@ -184,6 +184,12 @@ class _Db:
|
||||
def client(monkeypatch):
|
||||
db = _Db()
|
||||
monkeypatch.setattr(net, "get_db_pool", lambda: db)
|
||||
base = {"epsg": 5186, "start": {"x": 0.0, "y": 0.0}, "end": {"x": 90.0, "y": 180.0}}
|
||||
|
||||
async def load_base(project_id):
|
||||
return base
|
||||
|
||||
monkeypatch.setattr(net.plan_base, "load_base", load_base)
|
||||
app = FastAPI()
|
||||
app.include_router(net.router)
|
||||
session = {"role": "USER", "email": "a@forest.go.kr"}
|
||||
@@ -191,6 +197,7 @@ def client(monkeypatch):
|
||||
test = TestClient(app)
|
||||
test.db = db
|
||||
test.session = session
|
||||
test.base = base
|
||||
return test
|
||||
|
||||
|
||||
@@ -207,6 +214,7 @@ def test_get_saved_legacy_or_empty(client) -> None:
|
||||
client.db.answers["FROM plan_networks"] = [{"network_json": json.dumps(_network())}]
|
||||
got = client.get(URL).json()
|
||||
assert got["legacy"] is False and len(got["derived"]["pieces"]) == 3
|
||||
assert got["base"]["end"] == {"x": 90.0, "y": 180.0}
|
||||
|
||||
|
||||
def test_put_stores_network_pieces_years(client) -> None:
|
||||
@@ -226,6 +234,16 @@ def test_put_stores_network_pieces_years(client) -> None:
|
||||
assert ("UPDATE projects SET project_year_end = %s WHERE id = %s", (2027, PID)) in log
|
||||
stale = [a for s, a in log if "year NOT IN" in s]
|
||||
assert stale == [(PID, 2026, 2027)]
|
||||
assert not [s for s, _ in log if "plan_base_json = %s" in s] # 정본 칸 있음 = 안 굳힘
|
||||
|
||||
|
||||
def test_put_freezes_folded_base_first(client) -> None:
|
||||
client.db.answers["FROM projects"] = [{"project_year": 2026, "plan_base_json": None}]
|
||||
assert client.put(URL, json=_network()).status_code == 200
|
||||
sqls = [(s, a) for s, a in client.db.log if s.startswith("UPDATE projects SET plan_base_json")]
|
||||
assert len(sqls) == 1 and json.loads(sqls[0][1][0]) == client.base
|
||||
first_year = next(i for i, (s, _) in enumerate(client.db.log) if "plan_route_years" in s)
|
||||
assert client.db.log.index(sqls[0]) < first_year
|
||||
|
||||
|
||||
def test_put_reset_clears_year_routes(client) -> None:
|
||||
@@ -254,5 +272,6 @@ def test_put_rejects(client) -> None:
|
||||
client.db.answers["piece_id IS NULL"] = [(2027,)]
|
||||
assert client.put(URL, json=one).status_code == 409
|
||||
assert not [s for s, _ in client.db.log if s.startswith("INSERT")]
|
||||
assert client.put(URL, json=_network(epsg=5187)).status_code == 400 # 기준 지정과 다른 좌표계
|
||||
client.session["email"] = "a@gmail.com"
|
||||
assert client.put(URL, json=_network()).status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user