feat(D03): 꺾임점 고침 [저장] 정본 — 서버가 마디로 선 · 지표 다시 셈 · 이은 누가 고친 길이 · GET 은 분석 지문 같은 고침만 · 산림청만 · 시험 (PLAN 543-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,14 @@
|
||||
- `POST …/plan/analyze` — 연도 구간마다 노선 추천 갈래 넷(뒤에서 · 파생 저장본)
|
||||
- `GET …/plan/progress` — 두 일의 진행률
|
||||
- `GET …/plan/analysis` — 저장본 + 고른 갈래 · 이은 누가 · 이은 중심선
|
||||
- `PUT …/plan/analysis` — [저장] 연도별 고른 갈래 → `plan_route_years.route_json` · `fingerprint`
|
||||
- `PUT …/plan/analysis` — [저장] 연도별 고른 갈래(+ 꺾임점 고침) → `plan_route_years.route_json` ·
|
||||
`fingerprint`
|
||||
|
||||
지점 정본은 `plan_route_years` 시점 · 종점 칸(529-3 몫). D03 화면에서 끈 지점(캐시 · PLAN 530-3)은
|
||||
`points`(작업 좌표계 m)로 받아 그 지점으로 받기 · 분석 · 보기 · [저장] 때만 D02 규칙으로 정본에 씀.
|
||||
계산 자리 = ③ 서버 단독 — [저장]은 노선을 받지 않고 갈래 이름만 받아 서버 저장본으로 다시 셈.
|
||||
꺾임점 고침(543)은 마디 · 켬끔 · R 만 받아 서버가 선 · 지표를 다시 셈(`D03_PlanAnalysis_Edit`) ·
|
||||
GET 은 분석 지문이 같은 저장 고침만 돌려줌.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -31,6 +34,7 @@ from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
from D02_PlanBase import D02_PlanBase_Router as base
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Analyze as analyze
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Edit as plan_edit
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Terrain as terrain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -54,8 +58,19 @@ class PointsBody(BaseModel):
|
||||
points: list[PlanPoint] | None = None
|
||||
|
||||
|
||||
class PlanEditBody(BaseModel):
|
||||
"""연도 하나의 꺾임점 고침 — 고친 갈래 · 그 분석 지문 · 마디(작업 좌표계 m) · 곡선 켬끔 · R."""
|
||||
|
||||
base_mode: str
|
||||
base_fingerprint: str
|
||||
nodes: list[tuple[float, float]]
|
||||
curve_flags: list[bool]
|
||||
radii: list[float | None]
|
||||
|
||||
|
||||
class ChoiceRequest(PointsBody):
|
||||
choices: dict[int, str] # 연도 → 갈래
|
||||
edits: dict[int, PlanEditBody] | None = None # 연도 → 꺾임점 고침(없는 연도 = 엔진 노선)
|
||||
|
||||
|
||||
def _override(segments: list[dict[str, Any]], points: list[PlanPoint]) -> list[dict[str, Any]]:
|
||||
@@ -220,14 +235,46 @@ def _saved_mode(route_json: Any) -> str | None:
|
||||
|
||||
|
||||
def _assemble(
|
||||
root: Path, project: dict[str, Any], segments: list[dict[str, Any]], choices: dict[int, Any]
|
||||
root: Path,
|
||||
project: dict[str, Any],
|
||||
segments: list[dict[str, Any]],
|
||||
choices: dict[int, Any],
|
||||
edits: dict[int, dict[str, Any]] | None = None,
|
||||
) -> tuple[list[dict[str, Any] | None], list[dict[str, Any]] | None, list[list[float]]]:
|
||||
"""(연도 저장본 · 이은 누가 · 이은 중심선) — 저장본이 하나라도 없으면 None · 빈 선."""
|
||||
"""(연도 저장본 · 이은 누가 · 이은 중심선) — 저장본이 하나라도 없으면 None · 빈 선.
|
||||
|
||||
`edits` = 연도 → 고친 노선(`{mode, planned, metrics}`) — 누가 · 선만 고친 길이로(저장본은 엔진
|
||||
그대로).
|
||||
"""
|
||||
results = [analyze.read_segment(root, seg) for seg in segments]
|
||||
if any(result is None for result in results):
|
||||
return results, None, []
|
||||
rows = analyze.chain(results, choices, float(project["route_start_m"] or 0.0))
|
||||
return results, rows, analyze.joined_line(results, rows)
|
||||
view = analyze.with_edits(results, edits or {}) # type: ignore[arg-type]
|
||||
rows = analyze.chain(view, choices, float(project["route_start_m"] or 0.0))
|
||||
return results, rows, analyze.joined_line(view, rows)
|
||||
|
||||
|
||||
def _saved_edits(
|
||||
root: Path, segments: list[dict[str, Any]], choices: dict[int, Any]
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
"""저장된 꺾임점 고침 — 저장 갈래 그대로 · 분석 지문이 지금과 같은 것만."""
|
||||
out = {}
|
||||
for seg in segments:
|
||||
saved = plan_edit.saved_edit(seg["route_json"])
|
||||
result = analyze.read_segment(root, seg)
|
||||
if (
|
||||
saved
|
||||
and result
|
||||
and saved["base_mode"] == choices.get(seg["year"])
|
||||
and saved["base_fingerprint"] == result.get("fingerprint")
|
||||
and saved.get("planned")
|
||||
):
|
||||
out[seg["year"]] = saved
|
||||
return out
|
||||
|
||||
|
||||
def _edit_view(saved: dict[str, Any]) -> dict[str, Any]:
|
||||
return {key: saved[key] for key in (*plan_edit.INPUT_KEYS, "metrics")}
|
||||
|
||||
|
||||
def _query_points(points: str | None) -> list[PlanPoint] | None:
|
||||
@@ -247,11 +294,22 @@ async def plan_analysis(project_id: UUID, points: str | None = None) -> dict[str
|
||||
"""
|
||||
root, project, segments = await _plan_inputs(project_id, _query_points(points))
|
||||
choices = {seg["year"]: _saved_mode(seg["route_json"]) for seg in segments}
|
||||
results, rows, line = await asyncio.to_thread(_assemble, root, project, segments, choices)
|
||||
saved = await asyncio.to_thread(_saved_edits, root, segments, choices)
|
||||
edits = {year: {"mode": e["base_mode"], **e} for year, e in saved.items()}
|
||||
results, rows, line = await asyncio.to_thread(
|
||||
_assemble, root, project, segments, choices, edits
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
"years": [
|
||||
{"year": seg["year"], "start": seg["start"], "end": seg["end"], "analysis": result}
|
||||
{
|
||||
"year": seg["year"],
|
||||
"start": seg["start"],
|
||||
"end": seg["end"],
|
||||
"analysis": result,
|
||||
# 저장된 꺾임점 고침(입력 + 그때 지표) — 분석 지문이 같을 때만
|
||||
"edit": _edit_view(saved[seg["year"]]) if seg["year"] in saved else None,
|
||||
}
|
||||
for seg, result in zip(segments, results)
|
||||
],
|
||||
"chain": rows,
|
||||
@@ -297,7 +355,7 @@ async def save_plan_analysis(
|
||||
session: dict[str, Any] = Depends(require_company),
|
||||
) -> dict[str, Any]:
|
||||
"""[저장] — 끈 지점(있으면 정본으로) · 연도별 고른 갈래를 서버 저장본으로 다시 이어 씀."""
|
||||
if request.points and not is_forest_service(session):
|
||||
if (request.points or request.edits) and not is_forest_service(session):
|
||||
raise HTTPException(status_code=403, detail="예정노선 지점은 산림청만 고칠 수 있습니다.")
|
||||
root, project, segments = await _plan_inputs(project_id, request.points)
|
||||
bad = [m for m in request.choices.values() if m not in analyze.PLAN_MODES]
|
||||
@@ -313,6 +371,17 @@ async def save_plan_analysis(
|
||||
lost = [r["year"] for r in rows if choices.get(r["year"]) not in (None, r["mode"])]
|
||||
if lost:
|
||||
raise HTTPException(status_code=409, detail=f"고른 갈래에 노선이 없습니다: {lost}년")
|
||||
edited: dict[int, dict[str, Any]] = {}
|
||||
if request.edits:
|
||||
saved = {seg["year"]: plan_edit.saved_edit(seg["route_json"]) for seg in segments}
|
||||
asked = {year: body.model_dump() for year, body in request.edits.items()}
|
||||
try:
|
||||
edited = await asyncio.to_thread(
|
||||
plan_edit.build_edits, root, results, rows, asked, saved
|
||||
)
|
||||
except plan_edit.EditError as error:
|
||||
raise HTTPException(status_code=409, detail=str(error)) from error
|
||||
_, rows, _ = await asyncio.to_thread(_assemble, root, project, segments, choices, edited)
|
||||
if request.points:
|
||||
await _store_points(project_id, segments)
|
||||
async with get_db_pool().acquire() as connection:
|
||||
@@ -320,8 +389,10 @@ async def save_plan_analysis(
|
||||
try:
|
||||
async with connection.cursor() as cursor:
|
||||
for result, row in zip(results, rows):
|
||||
route = result["routes"][row["mode"]]
|
||||
route = edited.get(row["year"]) or result["routes"][row["mode"]]
|
||||
body = {**row, **{k: route[k] for k in ("planned", "nodes", "curves")}}
|
||||
if row["year"] in edited: # 꺾임점 고침 — 입력 · 지표 함께(GET 이 돌려줌)
|
||||
body.update(metrics=route["metrics"], edit=route["edit"])
|
||||
await cursor.execute(
|
||||
"UPDATE plan_route_years SET route_json = %s, fingerprint = %s "
|
||||
"WHERE project_id = %s AND year = %s",
|
||||
|
||||
Reference in New Issue
Block a user