fix(B06/B08): 스냅된 관이 측점에 안 붙어 금액에서 빠지던 것 — 배수관 넷 복구

⚠⚠ **진단이 뒤집힌 자리임.** 계획서 3-14 는 「관을 나중에 놓으면 측점이 안 생긴다」로
적혀 있었으나 실측하니 **측점은 이미 있었음** — 구조물 이름표까지 달고.

진짜 원인 — **관 자리와 측점 자리는 최대 0.5m 어긋나고 그것이 설계임.**
측점을 만들 때 정수 미터가 같은 격자 측점이 있으면 그리로 스냅함
(`B05_Profile_Engine_Sections_Core` — 횡단 파일명이 정수 미터라 두 측점이 한 파일을
덮어쓰는 것을 막는 가드). 관 440.241 은 **측점 440.0** 위에 섬.
그런데 붙이는 쪽이 **0.02m** 로만 봐서 그런 관은 어느 측점에도 안 붙었음
⇒ 횡단도에 안 서고 길이도 안 실려 B08 이 「연장 없음」으로 막음.

- `attach_culvert_sets` / `attachCulvertSets`(짝) — **가장 가까운 측점 하나**를 그 관의
  자리로 봄. 거리로 자르지 않아 스냅 폭이 바뀌어도 따라가고, 하나만 고르므로 두 번 안 셈
- `pipeOwnerChainage` 신설 — 길이를 싣는 주인도 같은 규칙
- `SECTION_MATCH_TOLERANCE_M` 0.05 → 0.5 — 좁게 보면 「횡단 자체가 없습니다」라는
  **거짓 사유**가 뜸(반대 방향의 거짓). 옛 판단 근거를 주석에 남기고 뒤집은 까닭도 적음

실측 — 관 9개 중 **5개만** 길이가 있던 것이 **9개 전부**로 (440·620·720·900 복구).

곁들여
- 「측점 없는 구조물 N개」 알림 + [측점 만들기] 단추(3-14 ㉯) — **진짜로 측점이 없는**
  경우를 위해 남김. 판정은 스냅을 셈에 넣어 0.5m.
  샘플링 조건은 확정 때 남긴 `sampling.json` → 없으면 1단계 저장값. 둘 다 없으면 막고 사유
- 등록부 `retaining_wall.form` 에 「식생옹벽블럭」 추가(다른 창 요청)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 12:02:27 +09:00
co-authored by Claude Opus 5
parent 513aff3ec0
commit e1fed5f68b
11 changed files with 506 additions and 12 deletions
+45
View File
@@ -199,6 +199,50 @@ def _merge_irregular_into_longitudinal(
atomic_write_json(path, data)
#: 지표 샘플링 조건을 적어 두는 파일 — **나중에 측점을 더 만들 때 같은 조건으로** 뜨기 위해.
#: ⚠ 조건이 다르면 그 측점만 다른 지표에서 뽑혀 옆 측점과 지반고가 어긋난다.
SAMPLING_SNAPSHOT_NAME = "sampling.json"
def sampling_snapshot_path(project_root: Path) -> Path:
return project_root / "B06_Section" / SAMPLING_SNAPSHOT_NAME
def save_sampling_snapshot(project_root: Path, request: RouteConfirmRequest) -> None:
"""확정 때 쓴 지표 샘플링 조건을 남긴다(2026-09-09).
왜 — 관을 나중에 놓으면 그 측점이 안 생기는데(계획서 3-14), 나중에 만들려면 **그때와 같은
조건**으로 떠야 한다. 조건을 안 남기면 되짚을 길이 없어 **지어내야 하는 자리**가 된다.
"""
if not request.filter_key or not request.method:
return
path = sampling_snapshot_path(project_root)
path.parent.mkdir(parents=True, exist_ok=True)
atomic_write_json(
path,
{
"filter_key": request.filter_key,
"method": request.method,
"smooth": bool(request.smooth),
"surface_model_id": request.surface_model_id,
},
)
def load_sampling_snapshot(project_root: Path) -> dict[str, Any] | None:
"""남겨 둔 샘플링 조건. 없으면 `None` — **지어내지 않는다**(노선 확정을 한 번 더 받는다)."""
path = sampling_snapshot_path(project_root)
if not path.is_file():
return None
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
if not data.get("filter_key") or not data.get("method"):
return None
return data
async def _append_irregular_cross_sections(
connection: aiomysql.Connection,
project_id: UUID,
@@ -208,6 +252,7 @@ async def _append_irregular_cross_sections(
"""확정 시 비정규 측점의 횡단을 생성해 종단 파일에 병합한다(파일 기반, 비치명적 호출용)."""
stored_path = await get_project_storage_relative_path(connection, project_id)
project_root = Path(resolve_stored_project_path(stored_path))
save_sampling_snapshot(project_root, request)
stored_options = await get_latest_section_options(connection, project_id)
crs_epsg = await get_surface_crs_epsg(connection, project_id, request.surface_model_id)
irregular_stations = await asyncio.to_thread(
+1 -1
View File
@@ -709,7 +709,7 @@
"key": "form",
"label": "형식",
"input": "select",
"choices": ["중력식", "반중력식", "캔틸레버식", "부벽식"],
"choices": ["중력식", "반중력식", "캔틸레버식", "부벽식", "식생옹벽블럭"],
"default": null,
"required": true,
"phase": "detail"