feat(510-1 자동 관 제안 = B05 배수유역 후보 고름): [자동 제안] = 서버 후보(지금 노선 × 세류선 교차 5 m 솎기 · 정수 m · 라벨 + 분석이 지금 노선이면 간격 보충) · 상태 있음(라벨 짝 · 교차 5 m 안 관) · 지운 관(지움 표지 짝) · 새 · 배수유역 칸 목록 + 지도 점선 원 · 고른 것만 관 넣기 길(addPipe · 세류선 교차 = 라벨 실음) · 옛 두 방식 창(전체 자동 · 지정 포함 추가) 걷음

- 서버 POST drainage/pipe-proposals(아무것도 안 씀) · propose_pipes(common_util_stream_crossing) · claim_crossings = {교차점: 라벨} 짝
- 라벨 왕복 — 계산 carry 가 화면 라벨 실음 · 관 속성 보관소가 응답 · 초안 · 요청에 라벨 실어 나름 · 옵션 고침에도 남김
- 지운 관을 다시 넣으면 지움 표지 걷음(keep_labels)
- 계산 등록부 D2 runner = propose_pipes · 시험 test_510 · 349 낡은 두 방식 기대 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 12:36:48 +09:00
co-authored by Claude Opus 5.5
parent 349c266064
commit 1abac6377f
16 changed files with 601 additions and 107 deletions
@@ -525,6 +525,8 @@ export interface DetailPipePoint {
summary_item?: SummaryItem | null;
/** 옵션 그룹마다 고른 집계 항목 `{그룹 id: 참조}`(PLAN 46-3) — 없으면 그룹 미지정. */
group_items?: GroupItems | null;
/** 자동 세류선 관 라벨(507-2 · `{stream, order, x, y}`) — 서버 정본 · 화면은 실어 나르기만(510). */
stream_label?: Record<string, string | number> | null;
}
/** 편집·저장 요청에 싣는 관 1건 — 좌표(lonlat)는 서버가 다시 계산하므로 뺀다. */
+46 -1
View File
@@ -37,6 +37,7 @@ from common_util.common_util_drainage_detail import (
DrainageDetail,
build_detail,
pipes_from_chainages,
read_road_routing,
)
from common_util.common_util_drainage_pipes import (
PIPE_SOURCE_USER,
@@ -57,7 +58,7 @@ from common_util.common_util_drainage_pipes import (
)
from common_util.common_util_json import LONLAT_DIGITS, round_floats
from common_util.common_util_route_geometry import StructureCandidate
from common_util.common_util_stream_crossing import keep_labels
from common_util.common_util_stream_crossing import keep_labels, propose_pipes
from config.config_system import (
DRAINAGE_PIPE_MAX_SPACING_M,
DRAINAGE_PIPE_MIN_SPACING_M,
@@ -645,3 +646,47 @@ async def put_pipe_points(
result["snapshot_failed"] = failed # 못 만든 구조물 조합 복사본(142-2) — [저장] 알림
result["stations_failed"] = not stood # 측점 맞춤 실패(433-6) — [저장] 알림
return result
@router.post("/{project_id}/drainage/pipe-proposals", response_model=None)
async def post_pipe_proposals(
project_id: UUID, payload: dict[str, Any] | None = None
) -> dict[str, Any] | JSONResponse:
"""[자동 제안](510) — 지금 노선 × 세류선 교차 + 간격 보충 후보와 상태 · 아무것도 안 씀.
`points` = 화면 관 목록(없으면 저장분) · 화면에서 지운 자동 관도 지움 표지로 봄(`keep_labels`).
간격 보충은 분석이 지금 노선일 때만(`spacing` 거짓 = 교차만).
"""
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import _base_pipes
from B04_PreProcess.B04_PreProcess_Router_Watershed import _prepare
context, reason = await load_drainage_context(project_id)
if context is None:
return JSONResponse(status_code=404, content={"status": "error", "message": reason})
path = pipe_points_path(context.stored_path)
stored = read_pipe_points_file(path)
try:
raw = (payload or {}).get("points")
points = (
load_pipe_points(
context.stored_path, route_signature(context.vertices), context.vertices
)
or []
if raw is None
else parse_pipe_points(raw)
)
except ValueError as error:
return JSONResponse(status_code=400, content={"status": "error", "message": str(error)})
dismissed = read_dismissed(path)
keep_labels(points, stored, dismissed)
prepared = await _prepare(project_id)
streams = [] if isinstance(prepared, JSONResponse) else prepared["streams"]
fresh = analysis_route_signature(context.stored_path) == route_signature(context.vertices)
routing = read_road_routing(drainage_dir(context.stored_path)) if fresh else None
curve = routing.strength_curve if routing is not None else None
proposals = await asyncio.to_thread(
lambda: propose_pipes(
context.vertices, _base_pipes(context.vertices, streams), points, dismissed, curve
)
)
return {"status": "success", "proposals": proposals, "spacing": curve is not None}