feat(509-5 · 509-2 B05 추천 지형): 분석 지형 고르기 = 추천 노선 창 안 · 지형마다 추천 미리 셈 · [적용] = 창 지형이 설계 지형 · 평면은 지형 이름만 · 설계 지형 격자 하나(수치지형도 적용이면 그 격자)

- 전처리 미리 셈이 고를 수 있는 지형마다(설계 지형 먼저 · 수치지형도는 DB 모델 있을 때만) 다섯 갈래 + 초기 종단
- 창 지형 바꿈 = 그 지형 저장본만 · 없으면 409 안내(+[재계산]) · 기본값 판정에서 지형 뺌
- POST /route/initial terrain = 창 지형 · [적용] draft.terrain = 창 지형 · 손 [저장]은 지형 안 바꿈(서버도 as_initial 일 때만)
- 평면 분석 지형 고르기 · 화면 취향(routeedit-terrain) 걷음 · 지반고 · 묶음 초안 = 설계 지형(null) · 적용 뒤 받아 둔 격자 버림
- 시험 test_509_5 · test_509_2 · 걷은 고르기를 보던 옛 시험 새 동작으로(352_3 건너뜀)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:41:20 +09:00
co-authored by Claude Opus 5.5
parent 23fd2f6f8d
commit 4eec3a4b37
30 changed files with 441 additions and 169 deletions
+47 -21
View File
@@ -6,6 +6,7 @@
import asyncio
import functools
import itertools
import json
import logging
import time
@@ -26,7 +27,7 @@ logger = logging.getLogger(__name__)
# 미리 셈 진행(364-4) — 프로젝트마다 하나만 돎 · 도는 동안 또 오면 끝난 뒤 한 번 더(입력이 바뀜)
_RUNNING: dict[str, dict[str, Any]] = {}
# 미리 셈 갈래인데 저장본이 없을 때(505-1) — 프로젝트 접근은 셈하지 않음
NOT_PREPARED = "추천 저장본이 없습니다 — 전처리를 다시 하면 생깁니다."
NOT_PREPARED = "추천 저장본이 없습니다 — 전처리를 다시 하면 생깁니다 · [재계산]이면 지금 셉니다."
@replan.router.post("/{project_id}/route/initial", response_model=None)
@@ -249,7 +250,8 @@ async def project_standard(project_id: UUID) -> dict[str, Any] | None:
async def precompute_defaults(project_id: UUID) -> None:
"""체인 끝 예약(PLAN 327-2 · 351-1) — 기본 다섯 알약 + 저장된 제약(initial_choice)의 다섯.
"""체인 끝 예약(PLAN 327-2 · 351-1) — 기본 다섯 알약 + 저장된 제약(initial_choice)의 다섯 ×
고를 수 있는 지형(509-5 · 설계 지형 먼저 · 수치지형도는 DB 모델 있을 때만).
모달은 저장된 제약 · 비중으로 열리므로 그 값으로도 미리 셈한다. 있으면 꺼내기만.
다섯 갈래 모두 종단 켬(420) · 첫머리에 30일 지난 다른 판 · 소스 지문 저장본을 지운다(420-2).
@@ -260,11 +262,13 @@ async def precompute_defaults(project_id: UUID) -> None:
started = time.perf_counter()
variants: list[tuple[Any, Any]] = [(None, None)]
modes = list(DEFAULT_MODES)
terrains: list[Any] = [None]
try:
paths = await replan._project_paths(project_id)
except LookupError:
paths = None
if paths is not None:
terrains = await precompute_terrains(project_id, paths[0])
removed = await asyncio.to_thread(purge_outdated, paths[0])
if removed:
logger.info("추천 옛 판 저장본 지움: project_id=%s %d", project_id, removed)
@@ -276,29 +280,51 @@ async def precompute_defaults(project_id: UUID) -> None:
modes.sort(key=lambda mode: mode != saved["logic"] or mode == "follow")
progress = _RUNNING.get(str(project_id))
if progress is not None:
progress.update(total=len(variants) * len(modes))
for criteria, weights in variants:
for mode in modes:
if progress is not None:
progress["mode"] = mode
# 화면과 같은 몸통 — 설계 지형 · 비중은 비중 갈래만 · 「종단 반영」 기본 켬(420)
request = replan.RouteInitialRequest(
mode=mode,
with_profile=True,
criteria=criteria,
weights=weights if mode == "weighted" else None,
progress.update(total=len(terrains) * len(variants) * len(modes))
for terrain, (criteria, weights), mode in itertools.product(terrains, variants, modes):
if progress is not None:
progress["mode"] = mode
# 화면과 같은 몸통 — 창 지형 · 비중은 비중 갈래만 · 「종단 반영」 기본 켬(420)
request = replan.RouteInitialRequest(
mode=mode,
with_profile=True,
criteria=criteria,
weights=weights if mode == "weighted" else None,
terrain=terrain,
)
outcome = await compute_initial(project_id, request, precompute=True)
if isinstance(outcome, JSONResponse):
logger.warning(
"추천 미리 셈 멈춤(%s %s %s): project_id=%s",
terrain,
mode,
outcome.status_code,
project_id,
)
outcome = await compute_initial(project_id, request, precompute=True)
if isinstance(outcome, JSONResponse):
logger.warning(
"추천 미리 셈 멈춤(%s %s): project_id=%s", mode, outcome.status_code, project_id
)
return
if progress is not None:
progress["done"] += 1
return
if progress is not None:
progress["done"] += 1
logger.info("추천 미리 셈 끝: project_id=%s %.1fs", project_id, time.perf_counter() - started)
async def precompute_terrains(project_id: UUID, project_root: Path) -> list[Any]:
"""미리 셈 지형(509-5) — 설계 지형 먼저 · 추천 창이 고를 수 있는 다른 지형."""
from B05_Profile.B05_Profile_Router_Terrain import _MODELS_SUBDIR, choosable_terrains
from common_util.common_util_surface_confirmation import (
design_terrain,
get_surface_confirmation_params,
)
pool = get_db_pool()
async with pool.acquire() as connection:
params = await get_surface_confirmation_params(connection, str(project_id))
sources = await choosable_terrains(
connection, project_id, params, Path(project_root) / _MODELS_SUBDIR
)
design = design_terrain(params)
return [design, *[source for source in sources if source != design]]
async def run_precompute(project_id: UUID) -> None:
"""미리 셈 한 줄(364-4) — 이미 돌면 「한 번 더」만 적고 돌아감(겹쳐 셈 막음)."""
key = str(project_id)