fix(B06): 옮긴 관 측점의 횡단 수정을 저장 때 버리지 않음 — 행 없으면 측점부터 세움
- 원인: B06 [저장]은 관 측점 비정규 측점을 종단에 싣지 않아(확정은 상단측만) 옮긴 관 자리에 DB 행이 없고, merge_cross_section_designs 가 행 없는 수정을 버림 - 고침(서버 한 곳): 저장 · 확정이 행 없는 측점의 수정을 받으면 ensure_structure_stations — [측점 만들기]와 같은 조건(샘플링 기록)으로 구조물 측점을 세움 → 기존 _rowless_station_chainages 가 기본 설계 행을 만들고 수정이 그 위에 얹힘 - 판정: 화면이 행을 만들려면 지표 샘플링 · 횡단 파일이 필요해 서버만 할 수 있음 — 서버 한 곳으로 충분 - B06_Section_Router_Stations: 측점 다시 뜨기를 _regenerate_structure_stations 로 떼어 단추 · 저장이 같이 씀 - 시험: test_b06_patch_station(부르는 조건) · sub8 왕복 시험 [B06] 통과(프로젝트 사본 · DB 읽기만) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrQ65VtGZbae2VKgYVMhmc
This commit is contained in:
@@ -88,6 +88,22 @@ def _rowless_station_chainages(
|
||||
return result
|
||||
|
||||
|
||||
async def _stand_patch_stations(
|
||||
project_id: UUID, request: SectionConfirmRequest | None, known: list[float]
|
||||
) -> None:
|
||||
"""행 없는 측점의 횡단 수정 — 버리지 않고 그 측점부터 세운다(B05·B06 한 페이지처럼).
|
||||
|
||||
옮긴 관 · 구조물 측점은 B05 확정 전이면 종단 측점 · 횡단 파일 · DB 행이 없다. 여기서
|
||||
[측점 만들기]와 같은 조건으로 세우면 아래 `_rowless_station_chainages` 가 행을 만들고
|
||||
수정이 그 행에 얹힌다.
|
||||
"""
|
||||
patches = (request.cross_patches or []) if request else []
|
||||
if any(all(abs(p.chainage_m - k) >= 0.01 for k in known) for p in patches):
|
||||
from B06_Section.B06_Section_Router_Stations import ensure_structure_stations
|
||||
|
||||
await ensure_structure_stations(project_id)
|
||||
|
||||
|
||||
async def _apply_section_edits(
|
||||
connection: aiomysql.Connection,
|
||||
route_id: int,
|
||||
@@ -196,6 +212,7 @@ async def save_sections(
|
||||
# ⚠ **값이 있는 행은 건드리지 않는다** — 사용자 조작값·이월분이 거기 있다.
|
||||
missing = await get_cross_sections_missing_design_chainages(connection, route_id)
|
||||
|
||||
await _stand_patch_stations(project_id, request, known)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
rowless = missing + await asyncio.to_thread(
|
||||
_rowless_station_chainages,
|
||||
@@ -268,6 +285,7 @@ async def confirm_sections(
|
||||
missing = await get_cross_sections_missing_design_chainages(connection, route_id)
|
||||
known = await get_cross_section_chainages(connection, route_id)
|
||||
|
||||
await _stand_patch_stations(project_id, request, known)
|
||||
marks.append(("조회(종단·경로·미지정 측점)", time.perf_counter()))
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
# 행 자체가 없는 측점(구조물 등 비정규)도 확정 대상에 넣는다 — 정본이 없으면
|
||||
|
||||
@@ -129,6 +129,83 @@ async def _project_paths(project_id: UUID) -> tuple[Path, dict[str, Any]] | None
|
||||
return Path(resolve_stored_project_path(stored)), route
|
||||
|
||||
|
||||
async def _regenerate_structure_stations(
|
||||
project_id: UUID, project_root: Path, route: dict[str, Any], snapshot: dict[str, Any]
|
||||
) -> int:
|
||||
"""구조물 측점 전체를 노선 확정 때와 같은 조건으로 다시 떠 종단 정본에 병합한다. 뜬 수.
|
||||
|
||||
⚠ 종단 병합이 비정규 측점을 **통째로 교체**하므로 빠진 것만이 아니라 전체를 뜬다.
|
||||
"""
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
_load_pipe_points,
|
||||
_load_route_polyline,
|
||||
generate_irregular_sections,
|
||||
resolve_extra_stations,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Router_Confirm import (
|
||||
_merge_irregular_into_longitudinal,
|
||||
_section_options_from_stored,
|
||||
)
|
||||
from B06_Section.B06_Section_Repository import (
|
||||
get_latest_section_options,
|
||||
get_longitudinal_section,
|
||||
)
|
||||
|
||||
async def _load(connection):
|
||||
options = await get_latest_section_options(connection, project_id)
|
||||
crs_epsg = await get_surface_crs_epsg(
|
||||
connection, project_id, snapshot.get("surface_model_id")
|
||||
)
|
||||
longitudinal = await get_longitudinal_section(connection, project_id, route["id"])
|
||||
return options, crs_epsg, longitudinal
|
||||
|
||||
stored_options, crs_epsg, longitudinal = await run_with_connection(_load)
|
||||
|
||||
def _regenerate() -> int:
|
||||
polyline = _load_route_polyline(project_root, str(route["route_data_path"]))
|
||||
pipes = _load_pipe_points(project_root, polyline)
|
||||
extras = resolve_extra_stations(project_root, pipes)
|
||||
stations = generate_irregular_sections(
|
||||
project_root,
|
||||
str(route["route_data_path"]),
|
||||
str(snapshot["filter_key"]),
|
||||
str(snapshot["method"]),
|
||||
bool(snapshot.get("smooth")),
|
||||
extra_stations=extras,
|
||||
options=_section_options_from_stored(stored_options),
|
||||
crs=f"EPSG:{crs_epsg}" if crs_epsg is not None else None,
|
||||
)
|
||||
if stations and longitudinal:
|
||||
_merge_irregular_into_longitudinal(
|
||||
project_root, str(longitudinal["longitudinal_file_path"]), stations
|
||||
)
|
||||
return len(stations)
|
||||
|
||||
return await asyncio.to_thread(_regenerate)
|
||||
|
||||
|
||||
async def ensure_structure_stations(project_id: UUID) -> int:
|
||||
"""빠진 구조물 측점을 세운다 — 횡단 저장이 **행 없는 측점의 수정**을 받을 때 부름.
|
||||
|
||||
옮긴 관 · 구조물 측점은 B05 확정 전이면 종단 측점 · 횡단 파일 · DB 행이 없어, 그 자리
|
||||
횡단 수정이 받을 곳 없이 버려진다. [측점 만들기] 단추와 같은 조건으로 세운다.
|
||||
만들 수 없으면(노선 · 샘플링 조건 없음 · 빠진 것 없음) 0 — 저장을 막지 않는다.
|
||||
"""
|
||||
paths = await _project_paths(project_id)
|
||||
if paths is None:
|
||||
return 0
|
||||
project_root, route = paths
|
||||
missing = await asyncio.to_thread(_missing_marks, project_root, str(route["route_data_path"]))
|
||||
if not missing:
|
||||
return 0
|
||||
snapshot = await _sampling_conditions(project_id, project_root)
|
||||
if snapshot is None:
|
||||
logger.warning("B06 저장: 빠진 구조물 측점 %d곳 — 샘플링 조건이 없어 못 세움", len(missing))
|
||||
return 0
|
||||
await _regenerate_structure_stations(project_id, project_root, route, snapshot)
|
||||
return len(missing)
|
||||
|
||||
|
||||
@router.get("/{project_id}/section/missing-stations")
|
||||
async def get_missing_stations(project_id: UUID) -> JSONResponse:
|
||||
"""측점이 없는 구조물 목록 — 화면이 「측점 없는 관 N개」를 띄우는 데 쓴다."""
|
||||
@@ -164,21 +241,6 @@ async def create_missing_stations(project_id: UUID) -> JSONResponse:
|
||||
⚠ 구조물 측점 전체를 다시 뜬다 — 종단 병합이 비정규 측점을 **통째로 교체**하므로
|
||||
빠진 것만 넘기면 이미 있던 구조물 측점이 지워진다.
|
||||
"""
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
_load_pipe_points,
|
||||
_load_route_polyline,
|
||||
generate_irregular_sections,
|
||||
resolve_extra_stations,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Router_Confirm import (
|
||||
_merge_irregular_into_longitudinal,
|
||||
_section_options_from_stored,
|
||||
)
|
||||
from B06_Section.B06_Section_Repository import (
|
||||
get_latest_section_options,
|
||||
get_longitudinal_section,
|
||||
)
|
||||
|
||||
try:
|
||||
paths = await _project_paths(project_id)
|
||||
if paths is None:
|
||||
@@ -199,37 +261,7 @@ async def create_missing_stations(project_id: UUID) -> JSONResponse:
|
||||
if not missing:
|
||||
return JSONResponse(content={"status": "success", "created": 0, "missing": []})
|
||||
|
||||
async def _load(connection):
|
||||
options = await get_latest_section_options(connection, project_id)
|
||||
crs_epsg = await get_surface_crs_epsg(
|
||||
connection, project_id, snapshot.get("surface_model_id")
|
||||
)
|
||||
longitudinal = await get_longitudinal_section(connection, project_id, route["id"])
|
||||
return options, crs_epsg, longitudinal
|
||||
|
||||
stored_options, crs_epsg, longitudinal = await run_with_connection(_load)
|
||||
|
||||
def _regenerate() -> int:
|
||||
polyline = _load_route_polyline(project_root, str(route["route_data_path"]))
|
||||
pipes = _load_pipe_points(project_root, polyline)
|
||||
extras = resolve_extra_stations(project_root, pipes)
|
||||
stations = generate_irregular_sections(
|
||||
project_root,
|
||||
str(route["route_data_path"]),
|
||||
str(snapshot["filter_key"]),
|
||||
str(snapshot["method"]),
|
||||
bool(snapshot.get("smooth")),
|
||||
extra_stations=extras,
|
||||
options=_section_options_from_stored(stored_options),
|
||||
crs=f"EPSG:{crs_epsg}" if crs_epsg is not None else None,
|
||||
)
|
||||
if stations and longitudinal:
|
||||
_merge_irregular_into_longitudinal(
|
||||
project_root, str(longitudinal["longitudinal_file_path"]), stations
|
||||
)
|
||||
return len(stations)
|
||||
|
||||
made = await asyncio.to_thread(_regenerate)
|
||||
made = await _regenerate_structure_stations(project_id, project_root, route, snapshot)
|
||||
except Exception:
|
||||
logger.exception("B06 측점 만들기 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""횡단 저장 — 행 없는 측점의 수정을 버리지 않고 그 측점부터 세우는지(PLAN 19장).
|
||||
|
||||
옮긴 관 · 구조물 측점은 B05 확정 전이면 DB 행이 없다. 저장 · 확정이 그 자리 수정을 받으면
|
||||
`ensure_structure_stations`([측점 만들기]와 같은 길)를 불러 측점을 세운 뒤 행을 만든다.
|
||||
DB · 파일 전체 왕복은 test_b05_b06_save_roundtrip [B06] 이 봄 — 여기서는 부르는 조건만.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import B06_Section.B06_Section_Router_Stations as stations
|
||||
from B06_Section.B06_Section_Router_Confirm import _stand_patch_stations
|
||||
from B06_Section.B06_Section_Schema import SectionConfirmRequest
|
||||
|
||||
PROJECT = "936be972-11bc-46c2-8bf3-b15d8de7df0d"
|
||||
|
||||
|
||||
def _calls(monkeypatch, patches: list[float], known: list[float]) -> list[str]:
|
||||
called: list[str] = []
|
||||
|
||||
async def fake(project_id):
|
||||
called.append(str(project_id))
|
||||
return 1
|
||||
|
||||
monkeypatch.setattr(stations, "ensure_structure_stations", fake)
|
||||
request = SectionConfirmRequest(cross_patches=[{"chainage_m": c} for c in patches])
|
||||
asyncio.run(_stand_patch_stations(PROJECT, request, known))
|
||||
return called
|
||||
|
||||
|
||||
def test_행_없는_측점_수정이면_측점을_세움(monkeypatch):
|
||||
assert _calls(monkeypatch, [120.0, 263.47], [120.0, 140.0]) == [PROJECT]
|
||||
|
||||
|
||||
def test_모든_수정에_행이_있으면_안_부름(monkeypatch):
|
||||
assert _calls(monkeypatch, [120.0, 140.004], [120.0, 140.0]) == []
|
||||
assert _calls(monkeypatch, [], [120.0]) == []
|
||||
Reference in New Issue
Block a user