feat(B04·B05 배수): 369-1 관 자리 기본 = 세류선 교차만 · auto 칸(all · add) · 노선 저장 오목 보충 끔
- build_detail auto — 없음 = 세류선 교차만 · all = 세류선 + 100 m 보충으로 다시 · add = 지금 목록 + 빈 구간 보충 · 후보는 늘 셈 - 배수유역 라우터 — 요청 auto 칸(틀리면 400) · 같은 자리(0.05 m) 관만 속성 · id · 사유 이어받음 · 나머지 새 관 = 등록부 기본값 · 저장본 열쇠에 auto - 노선 [저장] · 추천 [적용] — 오목 지점 보충 지움(응답 concave 칸 · 화면 한 줄 같이) - 시험 — test_369_drainage_auto 세 모드 · test_346_3 · 312 · 342_3 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
격자 해석은 하지 않는다. 배수유역 분석이 저장해 둔 `03_road_routing` 산출물을 읽어
|
||||
|
||||
· 기본 관(도로 × 상류 세류선 교차점) + 최대 간격 자동 보충으로 관 목록을 만들고
|
||||
· 기본 관(도로 × 상류 세류선 교차점)으로 관 목록을 만들고([자동 제안] = 최대 간격 보충 · 369)
|
||||
· 계획노선 종단 Z로 노면 물이 어느 관으로 가는지 정해 세부유역을 나눈다
|
||||
|
||||
계산 알고리즘은 B05 사용자 화면과 공용이다(`common_util_drainage_detail`) — 같은 이름의
|
||||
@@ -67,6 +67,8 @@ router = APIRouter(prefix="/api/projects", tags=["B04 Surface Drainage Basins"])
|
||||
|
||||
_NO_ANALYSIS = "배수유역 분석 결과가 없습니다. [유역 분석]을 먼저 실행하세요."
|
||||
# 노선 지문이 분석 때와 다름(옛 저장분은 지문 없음) — 옛 격자로 나누면 틀린 유역이 나온다(342-3).
|
||||
# [자동 제안] 방식(369) — None = 지금 목록 그대로(없으면 세류선 교차만) · all · add
|
||||
AUTO_MODES = (None, "all", "add")
|
||||
STALE_MESSAGE = (
|
||||
"배수유역 낡음 — 노선이 분석 때와 다릅니다. 유역을 다시 분석해야 세부 유역을 나눕니다."
|
||||
)
|
||||
@@ -78,20 +80,22 @@ def _build(
|
||||
points: list[PipePoint] | None,
|
||||
routing: Any = None,
|
||||
upstream_lonlat: list | None = None,
|
||||
auto: str | None = None,
|
||||
) -> tuple[DrainageDetail | None, list[PipePoint]]:
|
||||
"""세부유역을 계산하고, 그 결과에 쓰인 관 목록을 함께 돌려준다.
|
||||
|
||||
`points`가 None 이면 B04 기본 관에 최대 간격 규칙으로 자동 보충한 목록이 만들어진다.
|
||||
있으면 그 자리를 그대로 쓰되, 관이 왜 거기 있는지(기본/자동/수동)는 넘겨받은 목록의
|
||||
표시를 유지한다 — 계산기는 확정 좌표만 보므로 그대로 두면 전부 "수동"이 된다.
|
||||
`points`가 None 이면 B04 기본 관(세류선 교차)만. 있으면 그 자리를 그대로 쓰되, 관이 왜 거기
|
||||
있는지(기본/자동/수동)는 넘겨받은 목록의 표시를 유지한다 — 계산기는 확정 좌표만 보므로 그대로
|
||||
두면 전부 "수동"이 된다. `auto`([자동 제안] · 369) = "all" 자리 새로 · "add" 빈 구간 보충 —
|
||||
같은 자리(0.05 m) 관만 속성 · id 를 이어받고 나머지는 새 관(등록부 기본값).
|
||||
"""
|
||||
directory = drainage_dir(stored_path)
|
||||
chainages = [point.chainage_m for point in points] if points is not None else None
|
||||
detail = build_detail(directory, context.vertices, chainages, routing, upstream_lonlat)
|
||||
detail = build_detail(directory, context.vertices, chainages, routing, upstream_lonlat, auto)
|
||||
if detail is None:
|
||||
return None, []
|
||||
if points is not None:
|
||||
_retag(detail.pipes, points)
|
||||
_retag(detail.pipes, points, keep_new=auto is not None)
|
||||
rebuilt = _rebuilt(detail)
|
||||
# 원본에 없던 자리 = 이번에 새로 놓인 관(자동 배치 · 간격 보충) — 등록부 기본값을 받을 관
|
||||
fresh = [
|
||||
@@ -132,11 +136,14 @@ _DETAIL_CACHE_SIZE = 4
|
||||
DETAIL_CACHE_FILENAME = "04_detail_cache.pkl"
|
||||
|
||||
|
||||
def _detail_key(context: DrainageContext, points: list[PipePoint] | None) -> str:
|
||||
def _detail_key(
|
||||
context: DrainageContext, points: list[PipePoint] | None, auto: str | None = None
|
||||
) -> str:
|
||||
directory = drainage_dir(context.stored_path)
|
||||
digest = hashlib.sha1(usedforsecurity=False)
|
||||
listed = None if points is None else [point.as_dict() for point in points]
|
||||
digest.update(json.dumps(listed, sort_keys=True, ensure_ascii=False).encode())
|
||||
# auto 를 열쇠에 — 같은 관 목록이라도 [자동 제안] 결과와 섞이지 않게(369 · 옛 저장본도 비껴감)
|
||||
digest.update(json.dumps([listed, auto], sort_keys=True, ensure_ascii=False).encode())
|
||||
for vertex in context.vertices:
|
||||
digest.update(f"{vertex.x:.3f},{vertex.y:.3f},{vertex.z:.3f};".encode())
|
||||
routing = STAGES["road_routing"]
|
||||
@@ -167,10 +174,10 @@ def _remember(key: str, value: tuple[DrainageDetail, list[PipePoint]]) -> None:
|
||||
|
||||
|
||||
def _build_cached(
|
||||
context: DrainageContext, points: list[PipePoint] | None
|
||||
context: DrainageContext, points: list[PipePoint] | None, auto: str | None = None
|
||||
) -> tuple[DrainageDetail | None, list[PipePoint]]:
|
||||
"""`_build` + 저장본 — 관 목록은 사본으로 돌려준다(저장이 좌표를 채워도 저장본은 그대로)."""
|
||||
key = _detail_key(context, points)
|
||||
key = _detail_key(context, points, auto)
|
||||
hit = _DETAIL_CACHE.get(key)
|
||||
if hit is None:
|
||||
try:
|
||||
@@ -180,7 +187,7 @@ def _build_cached(
|
||||
except (OSError, pickle.PickleError, EOFError, AttributeError, KeyError, TypeError):
|
||||
hit = None
|
||||
if hit is None:
|
||||
detail, carried = _build(context.stored_path, context, points)
|
||||
detail, carried = _build(context.stored_path, context, points, auto=auto)
|
||||
if detail is None:
|
||||
return None, []
|
||||
hit = (detail, carried)
|
||||
@@ -276,8 +283,20 @@ def ford_height_m(flow_m3s: float | None, width_m: float) -> float | None:
|
||||
return math.ceil(depth * 100 - 1e-9) / 100
|
||||
|
||||
|
||||
def _retag(pipes: list[StructureCandidate], points: list[PipePoint]) -> None:
|
||||
"""확정 좌표로 되돌아온 관에 원래의 생성 사유를 다시 붙인다."""
|
||||
def _retag(
|
||||
pipes: list[StructureCandidate], points: list[PipePoint], keep_new: bool = False
|
||||
) -> None:
|
||||
"""확정 좌표로 되돌아온 관에 원래의 생성 사유를 다시 붙인다.
|
||||
|
||||
`keep_new`([자동 제안]) = 같은 자리(0.05 m) 관만 · 새로 놓인 관은 계산기 사유
|
||||
(stream · spacing) 그대로.
|
||||
"""
|
||||
if keep_new:
|
||||
picks = carry_indices([pipe.chainage_m for pipe in pipes], [p.chainage_m for p in points])
|
||||
for pipe, pick in zip(pipes, picks):
|
||||
if pick is not None:
|
||||
pipe.reason = points[pick].source
|
||||
return
|
||||
by_chainage = {round(point.chainage_m, 2): point.source for point in points}
|
||||
for pipe in pipes:
|
||||
source = by_chainage.get(round(pipe.chainage_m, 2))
|
||||
@@ -452,11 +471,16 @@ async def _resolve(
|
||||
돌려준다 — 관 목록도 없으면(자동 생성 자리) 409(342-3). 마지막 값 = 낡음.
|
||||
|
||||
빈 목록(`[]`)은 「관 0개」로 그대로 쓴다 — 자동 생성은 요청에 칸이 없고 저장분도 없을(None) 때만(312).
|
||||
요청 `auto`([자동 제안] · 369) = "all" · "add" — `_build` 참고 · 없으면 None.
|
||||
"""
|
||||
context, reason = await load_drainage_context(project_id)
|
||||
if context is None:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": reason})
|
||||
|
||||
auto = (payload or {}).get("auto")
|
||||
if auto not in AUTO_MODES:
|
||||
message = f"auto 는 {', '.join(m for m in AUTO_MODES if m)} 중 하나입니다."
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": message})
|
||||
try:
|
||||
raw = (payload or {}).get("points")
|
||||
requested = None if raw is None else parse_pipe_points(raw)
|
||||
@@ -479,9 +503,11 @@ async def _resolve(
|
||||
|
||||
# [저장]은 정본으로 다시 셈 — 저장본은 GET · POST 만
|
||||
if fresh:
|
||||
result = await asyncio.to_thread(_build, context.stored_path, context, points)
|
||||
result = await asyncio.to_thread(
|
||||
lambda: _build(context.stored_path, context, points, auto=auto)
|
||||
)
|
||||
else:
|
||||
result = await asyncio.to_thread(_build_cached, context, points)
|
||||
result = await asyncio.to_thread(_build_cached, context, points, auto)
|
||||
detail, resolved = result
|
||||
if detail is None:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": _NO_ANALYSIS})
|
||||
@@ -511,8 +537,9 @@ async def post_detail_basins(
|
||||
) -> dict[str, Any] | JSONResponse:
|
||||
"""화면에서 편집 중인 관 목록으로 세부유역을 다시 나눈다(저장하지 않는다).
|
||||
|
||||
`points` 칸 없이 부르면 저장분을 무시하고 기본 관 + 자동 보충으로 되돌린다 —
|
||||
노선이 바뀌어 전체 분석을 다시 돌린 직후의 경로다. 빈 목록은 관 0개(312).
|
||||
`points` 칸 없이 부르면 저장분을 무시하고 기본 관(세류선 교차)만으로 되돌린다 —
|
||||
[배관 배치 초기화] · 노선이 바뀌어 전체 분석을 다시 돌린 직후의 경로다. 빈 목록은 관 0개(312).
|
||||
`auto` = [자동 제안](369) — "all" 전체 자동 · "add" 지금 관 + 빈 구간 보충.
|
||||
"""
|
||||
resolved = await _resolve(project_id, payload, use_stored=False)
|
||||
if isinstance(resolved, JSONResponse):
|
||||
@@ -523,7 +550,7 @@ async def post_detail_basins(
|
||||
|
||||
@router.delete("/{project_id}/drainage/pipe-points", response_model=None)
|
||||
async def delete_pipe_points(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
"""저장된 관 지점을 버리고 기본 관 + 자동 보충 배치로 되돌린다("초기화").
|
||||
"""저장된 관 지점을 버리고 기본 관(세류선 교차) 배치로 되돌린다("초기화").
|
||||
|
||||
화면만 되돌리면 다시 들어왔을 때 옛 관이 살아난다 — 저장분과 파생 산출물까지 지운다
|
||||
(2026-08-02 사용자 보고).
|
||||
|
||||
Reference in New Issue
Block a user