fix(472 덧 관 지움 ↶): 서버 상세 읽기 관 세트 = 지금 노선에 얹은 관 자리 — 측점 계획 · 관 지점 GET · 화면과 한 벌

- 관 지점 정본 지문이 지금 노선과 어긋난 저장분(검증_sub4 · 파일 394 · 좌표 351) — 측점 계획(_load_pipe_points) · GET 은 351 에 얹고 서버 상세 읽기(attach_saved_shapes)만 파일 누가거리로 찾아 351 관 세트 빠짐 → 새로고침 = 관 세트 없음 · 관 지움 ↶ = 화면이 다시 얹어 관 세트 있음(누가토량 갈림)
- saved_pipes(노선 경로) — 상세 GET · 재생성 응답 · 설계 미리보기가 노선 경로를 넘김 · 노선 못 읽으면 저장분 그대로
- 시험 test_472_d_pipe_route_attach(옛 코드 실패) · golden 헬퍼도 같은 읽기

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 11:22:37 +09:00
co-authored by Claude Opus 5.5
parent 0e77191421
commit 789bd9ce09
5 changed files with 126 additions and 9 deletions
+26 -3
View File
@@ -141,9 +141,32 @@ def attach_shapes(
return misses
def attach_saved_shapes(project_root: Path, sections: list[dict[str, Any]]) -> None:
"""저장분 현황(관 지점 · 구조물 정본)으로 얹음 — 구조물 정본을 못 읽으면 관만(횡단 조회는 이어 감)."""
pipes = read_pipe_points_file(pipe_points_file(project_root))
def saved_pipes(project_root: Path, route_data_path: str | None = None) -> list[Any]:
"""관 지점 정본 — 노선 경로를 주면 그 노선에 얹어 읽음(472 덧) · 없으면 파일 그대로.
측점 계획(`_load_pipe_points`) · 관 지점 GET · 화면과 같은 이월 — 지문이 어긋난 옛 저장분도
측점이 선 자리에 관 세트가 얹힘."""
if route_data_path:
from B05_Profile.B05_Profile_Engine_Sections import (
_load_pipe_points,
_load_route_polyline,
)
try:
return _load_pipe_points(
project_root, _load_route_polyline(project_root, route_data_path)
)
except (FileNotFoundError, ValueError):
logger.warning("B06 관 지점: 노선을 못 읽어 저장분 그대로 얹음 (%s)", route_data_path)
return read_pipe_points_file(pipe_points_file(project_root))
def attach_saved_shapes(
project_root: Path, sections: list[dict[str, Any]], route_data_path: str | None = None
) -> None:
"""저장분 현황(관 지점 · 구조물 정본)으로 얹음 — 관 = 그 노선에 얹은 것(`saved_pipes`) ·
구조물 정본을 못 읽으면 관만(횡단 조회는 이어 감)."""
pipes = saved_pipes(project_root, route_data_path)
try:
structures = load_structures(str(project_root))[1]
except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 간다
+12 -4
View File
@@ -232,7 +232,9 @@ async def get_sections(project_id: UUID, route_id: int) -> SectionSummaryRespons
)
def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dict:
def _read_section_detail(
project_root: Path, longitudinal_file_path: str, route_data_path: str | None = None
) -> dict:
"""검증된 프로젝트 루트 안의 종단 및 횡단 JSON을 읽는다."""
root = project_root.resolve()
longitudinal_path = (root / longitudinal_file_path).resolve()
@@ -258,7 +260,7 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic
):
raise ValueError("종횡단 상세 파일 형식이 올바르지 않습니다.")
# 측점 그림 자료(관 세트 · 벽 · 포장 · 맹암거 · 빈칸 경고) = 형상 얹기 한 벌(323-4 · 화면 짝 attachShapes)
attach_saved_shapes(root, cross_sections)
attach_saved_shapes(root, cross_sections, route_data_path)
relabel_pipe_stations(root, cross_sections) # 저장으로 종류만 바꾼 관 — 카드 제목(54-4)
return {"longitudinal": longitudinal, "cross_sections": cross_sections}
@@ -314,10 +316,11 @@ async def get_section_detail(
try:
# 서로 기다릴 이유가 없는 읽기 셋 — 원격 DB 라 순차로 내면 왕복이 그대로 더해진다
# (질의 하나 약 12ms, 2026-09-06 실측). 같이 보내 가장 느린 하나의 시간만 쓴다.
longitudinal, stored_path, designs = await asyncio.gather(
longitudinal, stored_path, designs, source = await asyncio.gather(
run_with_connection(get_longitudinal_section, project_id, route_id),
run_with_connection(get_project_storage_relative_path, project_id),
run_with_connection(get_cross_section_designs, route_id),
run_with_connection(get_route_generation_source, project_id, route_id),
)
if not longitudinal:
return JSONResponse(
@@ -329,6 +332,9 @@ async def get_section_detail(
_read_section_detail,
project_root,
str(longitudinal["longitudinal_file_path"]),
(source or {}).get(
"route_data_path"
), # 관 = 이 노선에 얹은 것(측점 계획 · 화면과 같음)
)
# DB에만 있는 잠정 설계 지정을 chainage 근사로 각 횡단에 얹어 화면 복원을 돕는다.
for record in designs:
@@ -461,7 +467,7 @@ async def regenerate_route_sections(
raise
result = sections["result"]
# 재생성 응답도 상세 조회와 같은 형상 얹기 한 벌(323-4) — 화면이 어긋나지 않게.
attach_saved_shapes(project_root, result["cross_sections"])
attach_saved_shapes(project_root, result["cross_sections"], source["route_data_path"])
return SectionDetailResponse(
longitudinal=result["longitudinal"], cross_sections=result["cross_sections"]
)
@@ -517,12 +523,14 @@ async def preview_cross_designs(
)
stored_path = await get_project_storage_relative_path(connection, project_id)
designs = await get_cross_section_designs(connection, route_id)
source = await get_route_generation_source(connection, project_id, route_id)
designs = _with_design_choices(designs, request.design_choices)
project_root = Path(resolve_stored_project_path(stored_path))
detail = await asyncio.to_thread(
_read_section_detail,
project_root,
str(longitudinal_row["longitudinal_file_path"]),
(source or {}).get("route_data_path"),
)
def rebuild() -> None:
@@ -44,7 +44,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
| F1 | 시설 따라감(노선 바뀜) | `common_util_route_follow.ts:291` · `logic/B05_Profile_Pipeline_Follow.ts` · `logic/B05_Profile_Structure_Follow.ts` | `common_util_route_follow.py:120` | — | 짝 · `test_410_follow_xy` · `test_344_2_route_pairs` |
| F2 | 추가 측점 따라감 | `B05_Profile_Api_PlusStations.ts` followPlusDraft | `B05_Profile_Plus_Stations.py:82` | — | 짝 · `test_459_route_seat_mirror` |
| F3 | 시설 측점 계획 | `common_util_structure_station.ts` | `common_util_structure_station.py` | — | 짝 · `test_322_3_plan_mirror` |
| F4 | 형상 얹기(관 세트 · 벽 · 포장 그림 자료) | `common_util_structure_attach.ts:130` | `B06_Section_Engine_Attach.py:114` | — | 짝 · `test_323_4_attach_mirror` |
| F4 | 형상 얹기(관 세트 · 벽 · 포장 그림 자료) — 관 = 지금 노선에 얹은 저장분(서버 `saved_pipes` = 측점 계획 · 관 지점 GET 과 같은 이월) | `common_util_structure_attach.ts:130` | `B06_Section_Engine_Attach.py:114` | — | 짝 · `test_323_4_attach_mirror` · `test_472_d_pipe_route_attach` |
| F5 | 관 세트(관경 · 날개벽 · 집수정) | `common_util_culvert_sets.ts` · `logic/B06_Section_UI_Cross_Culvert_Const.ts` | `B06_Section_Engine_Culvert.py` | Const = sc · co · ex | 짝 · `test_b06_culvert_sets_mirror` · `test_b06_pipe_wall_height_mirror` |
| F6 | 벽 · 포장 | `common_util_structure_walls.ts` · `common_util_structure_pavement.ts` | `B06_Section_Engine_Structures_Wall.py` · `common_util_structure_pavement.py` | — | 짝 · `test_b06_structure_walls_mirror` · `test_b06_structure_pavement_mirror` |
| F7 | 관 길이 · 구조물 면적 · 다단 벽 | `B06_Section_Structure_Layouts.ts:95 · 287` · `design_structure_pipe_calc.ts` · `common_util_cross_structure_areas.ts` | — | sc | ② · `test_b06_pipe_length` · `test_299_ford_span_pipe` |
+3 -1
View File
@@ -234,7 +234,9 @@ def server_route(root: Path) -> dict[str, Any]:
entries = compute_default_designs(root, LONG, chainages, None, inputs)
by_m = {round(at, 3): {**design, **adjusts.get(round(at, 3), {})} for at, design in entries}
# [확정] 상세 읽기(`get_section_detail`) — 행 설계(0.01 m) 얹기 · 없는 측점 기본값
detail = _read_section_detail(root, LONG)
detail = _read_section_detail(
root, LONG, ROUTE
) # 관 = 이 노선에 얹은 것(get_section_detail 과 같음)
for section in detail["cross_sections"]:
hit = next(
(d for at, d in by_m.items() if abs(at - float(section["chainage_m"])) < 0.01), None
@@ -0,0 +1,84 @@
"""PLAN 472 덧 (요구 272) — 서버 상세 읽기 관 세트 = 측점 계획 · 화면과 같은 관 자리.
관 지점 정본의 노선 지문이 지금 노선과 어긋남(옛 노선 누가거리 394 · 좌표는 지금 노선 351) —
측점 계획(`_load_pipe_points`) · 관 지점 GET · 화면은 좌표로 351 에 얹는데 서버 상세 읽기
(`attach_saved_shapes`)만 파일 누가거리 394 로 찾아 351 측점에 관 세트를 못 붙였음(새로고침 =
관 세트 없음 · 관 지우기 ↶ = 화면이 다시 얹어 관 세트 있음 → 누가토량 갈림).
./venv/Scripts/python.exe -m pytest resources/tester/test_472_d_pipe_route_attach.py -q
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B05_Profile.B05_Profile_Engine_Sections import ( # noqa: E402
_load_pipe_points,
_load_route_polyline,
)
from B06_Section.B06_Section_Engine_Attach import attach_saved_shapes # noqa: E402
ROUTE = "B05_Profile/route/route_main.geojson"
def _project(root: Path) -> None:
route = root / ROUTE
route.parent.mkdir(parents=True)
route.write_text(
json.dumps(
{
"type": "Feature",
"geometry": {"type": "LineString", "coordinates": [[0, 0, 0], [1000, 0, 0]]},
}
),
encoding="utf-8",
)
edits = root / "B04_PreProcess" / "drainage" / "edits"
edits.mkdir(parents=True)
pipe = {
"id": "p35100",
"chainage_m": 394.0, # 옛 노선 누가거리
"x": 351.0,
"y": 0.3,
"options": {"pipe_kind": "파형강관", "pipe_diameter_mm": "1000"},
}
(edits / "pipe_points.json").write_text(
json.dumps({"route_signature": "옛-노선", "points": [pipe]}), encoding="utf-8"
)
def _sections() -> list[dict]:
return [
{"chainage_m": at, "center_x": at, "center_y": 0.0, "structure": None}
for at in (340.0, 351.0, 360.0, 380.0, 400.0)
]
def test_상세_읽기_관_세트_자리_측점_계획과_같음(tmp_path: Path) -> None:
_project(tmp_path)
planned = _load_pipe_points(tmp_path, _load_route_polyline(tmp_path, ROUTE))
assert [round(p.chainage_m, 2) for p in planned] == [351.0] # 측점 계획 = 351 측점
sections = _sections()
attach_saved_shapes(tmp_path, sections, ROUTE)
owners = {s["chainage_m"]: s["culvert"]["chainage_m"] for s in sections if "culvert" in s}
assert owners == {351.0: 351.0}
def test_노선_모름_저장분_그대로(tmp_path: Path) -> None:
_project(tmp_path)
sections = _sections()
attach_saved_shapes(tmp_path, sections) # 노선 경로 없음 = 파일 누가거리(394 · 제 측점 없음)
assert not any("culvert" in s for s in sections)
def test_노선_파일_없음_저장분_그대로(tmp_path: Path) -> None:
_project(tmp_path)
sections = _sections()
attach_saved_shapes(tmp_path, sections, "B05_Profile/route/없음.geojson")
assert not any("culvert" in s for s in sections)