feat(562-2): D03 노선 정본에 전체 시점 · 종점(base) — D02 load_base 그대로 응답 · 좌표계 다르면 거절 · 옛 접음은 [저장] 전에 칸에 굳힘

- GET · PUT /plan/network 응답에 base({epsg, start, end})
- PUT network.epsg ≠ base.epsg = 400
- plan_base_json 이 비어 옛 연도 행을 접어 보이던 프로젝트 = 연도 행을 바꾸기 전에 접은 두 점을 칸에 씀(D02 시점 · 종점이 안 바뀜)
- 시험 test_530_2_plan_analyze 18 · 실제 호출 한 바퀴(검증_sub3_D)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 20:10:58 +09:00
co-authored by Claude Opus 5.5
parent 84b72ad704
commit bbe2079cee
2 changed files with 48 additions and 7 deletions
+28 -6
View File
@@ -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}