feat(B04/B05): 재확정 재계산 체인(12) + 편집 버튼·유토곡선 UI 정리(13~15)
- 12: run_redesign_chain — B04 재확정 시 사용자 입력(stage 2 params) 유지한 채 새 지표면 기준 B05 재계산·확정, 옛 측점별 설계·표준단면 설정을 chainage 매칭 이월 후 B06 확정. 경로 없으면 신규 자동 체인 폴백. B04 confirm에 백그라운드 결선 - 13: 구간 이동 글리프 ⇧⇩ → 속 찬 ⬆︎⬇︎(21×17·굵게), 규칙·비정규 측점 버튼 최소 간격 20px 캐스케이드 배치로 근접 측점 ▲▼ 겹침 해소 - 14: 유토곡선 손잡이를 표준 삼각형 손잡이(64×24, 바닥 중앙)로 통일, 캡션 제거, '유토곡선 펼치기/접기' 툴팁 - 15: 유토곡선 스크롤러 세로 휠 → 가로 이동(종단 그래프와 동기) Playwright 검증: 버튼 27개 최소 간격 20px, 글리프 ⬆︎ 21×17, 표준 손잡이 64×24, 휠 스크롤 0→400 종단 동기, 콘솔 오류 0. typecheck·ruff 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,3 +146,180 @@ async def run_auto_design_chain(project_id: UUID, surface_model_id: int | None =
|
||||
except Exception:
|
||||
# 체인은 업로드·WF1 흐름의 부가 작업이다 — 어떤 예외도 밖으로 던지지 않는다.
|
||||
logger.exception("자동 설계 체인 실패: project_id=%s", project_id)
|
||||
|
||||
|
||||
async def run_redesign_chain(
|
||||
project_id: UUID,
|
||||
surface_model_id: int,
|
||||
selection: dict[str, Any],
|
||||
) -> None:
|
||||
"""B04 재확정 후 — **사용자 입력을 유지한 채** 새 지표면 기준으로 B05·B06 재계산·저장.
|
||||
|
||||
관리자가 B04에서 다른 지표면 모델로 재확정하면(2026-08-04 사용자 확정) 그 값을
|
||||
기준으로 다음 페이지들도 함께 갱신돼야 한다. 이때 일반 사용자가 이미 쓰던 설정은
|
||||
버리지 않는다:
|
||||
- B05: 저장된 stage 2 params(제어점 BP/EP/CP·회피/금지원·경사 옵션·측점 간격 등)를
|
||||
그대로 쓰고 **지표면(filter/method/smooth/model id)만** 새 확정값으로 바꾼다.
|
||||
- B06: 옛 경로의 측점별 설계(지반유형·단면유형·측구·암 경계)를 chainage 매칭으로
|
||||
새 경로에 이월하고, 표준단면 설정(data.options)도 함께 넘긴다. 나머지 미지정
|
||||
측점은 확정 시 기본값으로 채워진다.
|
||||
|
||||
경로가 아예 없으면 신규 자동 체인(계획노선 CSV 기본값)으로 되돌아간다.
|
||||
"""
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_wf2_Route.B05_wf2_Route_Repository import get_latest_route
|
||||
from B05_wf2_Route.B05_wf2_Route_Router import confirm_latest_route, solve_route
|
||||
from B05_wf2_Route.B05_wf2_Route_Schema import RouteSolveRequest
|
||||
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import (
|
||||
get_cross_section_designs,
|
||||
get_longitudinal_section,
|
||||
update_cross_section_design,
|
||||
)
|
||||
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Router_Confirm import confirm_sections
|
||||
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Schema import SectionConfirmRequest
|
||||
from common_util.common_util_workflow_state import get_workflow_state
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
_ = get_project_storage_relative_path # 시그니처 정렬용 (사용 안 함)
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
latest = await get_latest_route(connection, project_id)
|
||||
if not latest:
|
||||
logger.info(
|
||||
"재확정 체인 → 기존 경로 없음, 신규 자동 체인으로: project_id=%s", project_id
|
||||
)
|
||||
await run_auto_design_chain(project_id, surface_model_id=surface_model_id)
|
||||
return
|
||||
|
||||
# 1) 사용자 입력 회수 — 마지막 경로 계산의 stage 2 params가 정본이다.
|
||||
old_route_id = int(latest["id"])
|
||||
async with pool.acquire() as connection:
|
||||
import aiomysql
|
||||
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
state = await get_workflow_state(cursor, str(project_id))
|
||||
old_longitudinal = await get_longitudinal_section(connection, project_id, old_route_id)
|
||||
old_designs = await get_cross_section_designs(connection, old_route_id)
|
||||
stage2 = next(
|
||||
(s for s in (state or {}).get("stages", []) if int(s.get("stage_no", -1)) == 2), None
|
||||
)
|
||||
params = (stage2 or {}).get("params") or {}
|
||||
points = params.get("points") or {}
|
||||
options = params.get("options") or {}
|
||||
if not points.get("bp") or not points.get("ep"):
|
||||
logger.warning(
|
||||
"재확정 체인 중단(stage 2 params에 제어점 없음): project_id=%s", project_id
|
||||
)
|
||||
return
|
||||
|
||||
# 2) B05 재계산 — 지표면 관련 값만 새 확정 선택으로 교체, 나머지는 사용자 저장분.
|
||||
request = RouteSolveRequest(
|
||||
filter_key=str(selection.get("source_filter") or params.get("filter_key")),
|
||||
method=str(selection.get("method") or params.get("method") or "dtm"),
|
||||
smooth=bool(selection.get("smooth", params.get("smooth", False))),
|
||||
surface_model_id=surface_model_id,
|
||||
algorithm=str(params.get("algorithm") or "dijkstra"),
|
||||
bp=points["bp"],
|
||||
ep=points["ep"],
|
||||
cp=points.get("cp") or [],
|
||||
ap=points.get("ap") or [],
|
||||
fp=points.get("fp") or [],
|
||||
station_interval_m=params.get("station_interval_m"),
|
||||
cross_half_width_m=params.get("cross_half_width_m"),
|
||||
cross_sample_interval_m=params.get("cross_sample_interval_m"),
|
||||
long_sample_interval_m=params.get("long_sample_interval_m"),
|
||||
**{key: options.get(key) for key in ("grade_class",) if options.get(key)},
|
||||
paved=bool(options.get("paved", False)),
|
||||
terrain_type=str(options.get("terrain_type") or "normal"),
|
||||
main_direction=str(options.get("main_direction") or "auto"),
|
||||
min_curve_radius_m=options.get("min_curve_radius_m"),
|
||||
max_uphill_grade=options.get("max_uphill_grade"),
|
||||
max_downhill_grade=options.get("max_downhill_grade"),
|
||||
min_uphill_grade=options.get("min_uphill_grade"),
|
||||
min_downhill_grade=options.get("min_downhill_grade"),
|
||||
weights=options.get("weights"),
|
||||
allow_avoid_pass_through=bool(options.get("allow_avoid_pass_through", False)),
|
||||
max_grade_pct=params.get("max_grade_pct"),
|
||||
min_vertical_radius_m=params.get("min_vertical_radius_m"),
|
||||
min_tangent_length_m=params.get("min_tangent_length_m"),
|
||||
balance_segment_length_m=params.get("balance_segment_length_m"),
|
||||
start_elevation_offset_m=params.get("start_elevation_offset_m"),
|
||||
end_elevation_offset_m=params.get("end_elevation_offset_m"),
|
||||
)
|
||||
solve_result: Any = await solve_route(project_id, request)
|
||||
if isinstance(solve_result, JSONResponse):
|
||||
logger.error(
|
||||
"재확정 체인 중단(B05 재계산 실패): project_id=%s status=%s",
|
||||
project_id,
|
||||
solve_result.status_code,
|
||||
)
|
||||
return
|
||||
new_route_id = int(solve_result.route_id)
|
||||
logger.info(
|
||||
"재확정 체인 B05 재계산 완료: project_id=%s %s→%s",
|
||||
project_id,
|
||||
old_route_id,
|
||||
new_route_id,
|
||||
)
|
||||
|
||||
# 3) 옛 측점별 사용자 설계를 chainage 매칭으로 새 경로에 이월(비치명적).
|
||||
carried = 0
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
await connection.begin()
|
||||
try:
|
||||
for record in old_designs:
|
||||
design = record.get("design")
|
||||
if not isinstance(design, dict):
|
||||
continue
|
||||
await update_cross_section_design(
|
||||
connection,
|
||||
route_id=new_route_id,
|
||||
chainage_m=float(record["chainage_m"]),
|
||||
design=design,
|
||||
project_id=project_id,
|
||||
)
|
||||
carried += 1
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"재확정 체인 — 옛 설계 이월 실패(계속 진행): project_id=%s", project_id
|
||||
)
|
||||
logger.info("재확정 체인 설계 이월: project_id=%s %d건", project_id, carried)
|
||||
|
||||
# 4) B05 확정 → B06 확정(옛 표준단면 설정 이월, 미지정 측점 기본값 채움).
|
||||
confirm_result = await confirm_latest_route(project_id, None)
|
||||
if isinstance(confirm_result, JSONResponse):
|
||||
logger.error(
|
||||
"재확정 체인 중단(B05 확정 실패): project_id=%s status=%s",
|
||||
project_id,
|
||||
confirm_result.status_code,
|
||||
)
|
||||
return
|
||||
old_options = ((old_longitudinal or {}).get("data") or {}).get("options") or {}
|
||||
section_request = (
|
||||
SectionConfirmRequest(standard_cross_section=old_options["standard_cross_section"])
|
||||
if old_options.get("standard_cross_section")
|
||||
else None
|
||||
)
|
||||
sections_result = await confirm_sections(project_id, new_route_id, section_request)
|
||||
if isinstance(sections_result, JSONResponse):
|
||||
logger.error(
|
||||
"재확정 체인 중단(B06 확정 실패): project_id=%s status=%s",
|
||||
project_id,
|
||||
sections_result.status_code,
|
||||
)
|
||||
return
|
||||
logger.info(
|
||||
"재확정 체인 완료: project_id=%s route %s→%s (설계 %d건 이월)",
|
||||
project_id,
|
||||
old_route_id,
|
||||
new_route_id,
|
||||
carried,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("재확정 체인 실패: project_id=%s", project_id)
|
||||
|
||||
Reference in New Issue
Block a user