"""계산 창구 `/calc` 의 서버 맥락(PLAN 511-B B-5) — follow 「서버」(`stage.follow` · until follow)에 붙이는 저장 정본을 프로젝트 id 로 서버가 모음. 브라우저가 보낸 정본 칸은 버림(경로 막기와 같은 원칙). plan · types · definition · saved_pipes · saved_structures · saved_plus · interval_m · longitudinal = 화면이 받는 값과 같은 길(GET 노선 계획 · 구조물 종류 · 정의 · 관 지점 · 구조물 · 추가 측점 · 측점 간격 기본 · 최신 노선 저장 종단) — 정본 쓰기 없음(관 지점 = 화면 GET 과 같은 파생 저장본만). 브라우저 몫 = `BROWSER_KEYS`(묶음 입력 · 초안 전 시설 · 관 세션 장부 · 세션 계획고 초안). """ from __future__ import annotations import asyncio import json from pathlib import Path from typing import Any from uuid import UUID BROWSER_KEYS = frozenset({"input", "before_draft", "pipe_ids", "saved_edits", "until"}) PLAN_KEYS = ("planned", "curves", "min_radius_m", "station_snap", "station_snap_step") async def follow_context(project_id: UUID) -> dict[str, Any] | str: """follow 두 단계 맥락 — 못 모으면 까닭 글.""" from B04_PreProcess.B04_PreProcess_Router_Basins import get_pipe_points from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions from B05_Profile.B05_Profile_Engine_Sections_Node import structure_table from B05_Profile.B05_Profile_Plus_Stations import load_plus_stations from B05_Profile.B05_Profile_Router_Replan import _project_paths, read_route_plan from B05_Profile.B05_Profile_Structures_Router import read_structures paths = await _project_paths(project_id) if paths is None: return "프로젝트 저장 경로 없음" root = paths[0] plan, pipes, structures, (types, definition) = await asyncio.gather( read_route_plan(project_id), get_pipe_points(project_id), read_structures(project_id), asyncio.to_thread(structure_table), ) for name, got in (("노선", plan), ("관", pipes)): if not isinstance(got, dict): return f"{name} 정본을 못 읽음" if not hasattr(structures, "model_dump"): return "구조물 정본을 못 읽음" return { "plan": {key: plan.get(key) for key in PLAN_KEYS}, "types": types, "definition": definition, "saved_pipes": [ {k: v for k, v in pipe.items() if k != "lonlat"} for pipe in pipes["pipe_points"] ], "saved_structures": structures.model_dump(mode="json")["structures"], "saved_plus": await asyncio.to_thread(load_plus_stations, root), "interval_m": SectionGenerationOptions().station_interval_m, "longitudinal": await _saved_longitudinal(project_id, root), } async def _saved_longitudinal(project_id: UUID, root: Path) -> dict[str, Any] | None: """최신 노선의 저장 종단(화면 상세 `longitudinal`) — 없거나 못 읽으면 None.""" from B05_Profile.B05_Profile_Repository import get_latest_route from B06_Section.B06_Section_Repository import get_longitudinal_section from config.config_db import get_db_pool async with get_db_pool().acquire() as connection: route = await get_latest_route(connection, project_id) row = route and await get_longitudinal_section(connection, project_id, int(route["id"])) rel = (row or {}).get("longitudinal_file_path") if not rel: return None path = (root / str(rel)).resolve() if not path.is_relative_to(root.resolve()) or not path.is_file(): return None data = json.loads(await asyncio.to_thread(path.read_text, encoding="utf-8")) return data if isinstance(data, dict) else None async def attach_follow_context(project_id: UUID, inputs: dict[str, Any]) -> str | None: """`inputs.stages`(until follow) — 브라우저 몫만 남기고 서버 맥락을 붙임 · 못 붙이면 까닭 글.""" stages = inputs.get("stages") if not isinstance(stages, dict) or stages.get("until") != "follow": return None context = await follow_context(project_id) if isinstance(context, str): return context inputs["stages"] = { **{k: v for k, v in stages.items() if k in BROWSER_KEYS}, **context, } return None