- 반폭 재생성 = 옛 횡단 행 data · 상태를 측점 열쇠(0.01 m)로 새 행에 · 옛 종단 data 사용자 키(표준 횡단면 · 유토 · 계획 입력 · 상태) — 같은 트랜잭션 - 계획고 편집 = 옛 계획선 통째(_carry_profile 같은 노선 갈래) · 실패면 종 · 횡단 파일 되돌림 - 넓힌 지반 위 [저장]과 같은 서버 재계산 - 화면 = 다시 만들기 전 확인창 · 취소면 재생성 없음 · 오버레이 셈 짝 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
151 lines
7.2 KiB
Python
151 lines
7.2 KiB
Python
"""B06 카드 폭 넘김 다시 만들기(446) — 지반 표본을 넓혀 종 · 횡단을 다시 만들되 사용자 값은 그대로.
|
|
|
|
`regenerate_route_sections` 는 종 · 횡단 행을 지우고 새로 만든다. 같은 노선 · 같은 측점이므로
|
|
측점 열쇠(누가거리 0.01 m 안)로 옛 횡단 행의 설계(지반 · 메모 · CAD · 상세 표 · 조절) · 상태를,
|
|
옛 종단 행의 사용자 키(표준 횡단면 · 유토 풍선 · 계획 입력 · 상태)를 같은 트랜잭션에서 새 행에
|
|
옮겨 담고, 계획고 편집(종단 정본 `profile_alignment`)을 옛 계획선 통째로 되살린 뒤 [저장]과 같은
|
|
서버 재계산으로 넓힌 지반 위에 다시 셈. 뒤 단계가 실패하면 종 · 횡단 파일을 옛것으로 되돌림.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
import aiomysql
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
|
from B06_Section.B06_Section_Repository import get_longitudinal_section
|
|
from B06_Section.B06_Section_Schema import SectionDetailResponse
|
|
from common_util.common_util_storage import resolve_stored_project_path
|
|
from config.config_db import get_db_pool
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_MATCH_M = 0.01 # 측점 열쇠 — 저장 길(`update_cross_section_design`)과 같은 근사
|
|
|
|
|
|
def carried_longitudinal_data(old: dict[str, Any], new: dict[str, Any]) -> dict[str, Any]:
|
|
"""새 종단 data — 생성 요약 · 옵션은 새것 · 나머지(표준 횡단면 · 유토 · 계획 입력)는 옛것."""
|
|
merged = {**old, **new, "options": {**(old.get("options") or {}), **(new.get("options") or {})}}
|
|
if "grade_overrides" in old: # 사용자 계획 입력 — 다시 만들기는 늘 빈 값으로 냄
|
|
merged["grade_overrides"] = old["grade_overrides"]
|
|
return merged
|
|
|
|
|
|
def carried_cross_rows(
|
|
old_rows: list[dict[str, Any]], new_rows: list[dict[str, Any]]
|
|
) -> tuple[list[tuple[str, str, int]], int]:
|
|
"""(새 행 UPDATE 값 [(data, status, id)], 못 옮긴 옛 행 수) — 새 요약 위에 옛 data · 상태."""
|
|
updates: list[tuple[str, str, int]] = []
|
|
used: set[int] = set()
|
|
for row in new_rows:
|
|
at = float(row["chainage_m"])
|
|
near = [
|
|
index
|
|
for index, old in enumerate(old_rows)
|
|
if abs(float(old["chainage_m"]) - at) < _MATCH_M
|
|
]
|
|
if not near:
|
|
continue
|
|
index = max(near, key=lambda i: int(old_rows[i]["id"])) # 같은 측점 둘이면 최신 행
|
|
used.update(near)
|
|
old = old_rows[index]
|
|
data = {**_as_dict(old.get("data")), **_as_dict(row.get("data"))}
|
|
updates.append((json.dumps(data, ensure_ascii=False), str(old["status"]), int(row["id"])))
|
|
return updates, len(old_rows) - len(used)
|
|
|
|
|
|
def _as_dict(value: Any) -> dict[str, Any]:
|
|
if isinstance(value, (str, bytes)):
|
|
value = json.loads(value)
|
|
return value if isinstance(value, dict) else {}
|
|
|
|
|
|
async def _cross_rows(connection: aiomysql.Connection, route_id: int) -> list[dict[str, Any]]:
|
|
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
|
await cursor.execute(
|
|
"SELECT id, chainage_m, data, status FROM cross_sections"
|
|
" WHERE route_id = %s ORDER BY id",
|
|
(route_id,),
|
|
)
|
|
return list(await cursor.fetchall())
|
|
|
|
|
|
async def widen_route_sections(
|
|
project_id: UUID, route_id: int, cross_half_width_m: float
|
|
) -> SectionDetailResponse | JSONResponse:
|
|
"""표본 폭 넘김 다시 만들기 — 사용자 값을 옮겨 담은 뒤 새 상세를 돌려줌."""
|
|
from B05_Profile.B05_Profile_Engine_Sections import _STAGE_SUBDIR
|
|
from B05_Profile.B05_Profile_Route_Save import _Backup, _carry_profile, _read_json
|
|
from B06_Section.B06_Section_Router import get_section_detail, regenerate_route_sections
|
|
from B06_Section.B06_Section_Router_Confirm import _recompute_stored_designs
|
|
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection:
|
|
old_long = await get_longitudinal_section(connection, project_id, route_id)
|
|
old_rows = await _cross_rows(connection, route_id)
|
|
stored_path = await get_project_storage_relative_path(connection, project_id)
|
|
project_root = Path(resolve_stored_project_path(stored_path))
|
|
relative = str((old_long or {}).get("longitudinal_file_path") or "")
|
|
old_alignment = (_read_json(project_root / relative) if relative else {}).get(
|
|
"profile_alignment"
|
|
) or {}
|
|
stage_dir = project_root / _STAGE_SUBDIR
|
|
backup = _Backup(project_root, [], [stage_dir / "longitudinal", stage_dir / "cross_sections"])
|
|
dropped = 0
|
|
|
|
async def carry(connection: aiomysql.Connection) -> None:
|
|
nonlocal dropped
|
|
updates, dropped = carried_cross_rows(old_rows, await _cross_rows(connection, route_id))
|
|
async with connection.cursor() as cursor:
|
|
if updates:
|
|
await cursor.executemany(
|
|
"UPDATE cross_sections SET data = %s, status = %s WHERE id = %s", updates
|
|
)
|
|
if old_long:
|
|
await cursor.execute(
|
|
"SELECT id, data FROM longitudinal_sections WHERE route_id = %s"
|
|
" ORDER BY id DESC LIMIT 1",
|
|
(route_id,),
|
|
)
|
|
row = await cursor.fetchone()
|
|
data = carried_longitudinal_data(old_long.get("data") or {}, _as_dict(row[1]))
|
|
await cursor.execute(
|
|
"UPDATE longitudinal_sections SET data = %s, status = %s WHERE id = %s",
|
|
(json.dumps(data, ensure_ascii=False), old_long["status"], int(row[0])),
|
|
)
|
|
|
|
try:
|
|
made = await regenerate_route_sections(
|
|
project_id, route_id, cross_half_width_m, carry=carry
|
|
)
|
|
if isinstance(made, JSONResponse):
|
|
await asyncio.to_thread(backup.restore)
|
|
return made
|
|
try: # 같은 노선 — `_carry_profile` 의 옛 계획선 통째 갈래(빈 정점열 = 바뀐 구간 없음)
|
|
async with pool.acquire() as connection:
|
|
new_long = await get_longitudinal_section(connection, project_id, route_id)
|
|
new_relative = str((new_long or {}).get("longitudinal_file_path") or "")
|
|
await _carry_profile(project_root, new_relative, route_id, old_alignment, [], [])
|
|
except Exception as exc: # noqa: BLE001 — 계획고 편집을 잃느니 파일을 옛것으로
|
|
logger.exception("B06 표본 넓힘: 계획고 옮김 실패 project_id=%s", project_id)
|
|
await asyncio.to_thread(backup.restore)
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"status": "error", "message": f"계획고 편집 옮김 실패: {exc}"},
|
|
)
|
|
finally:
|
|
backup.drop()
|
|
if dropped:
|
|
logger.warning(
|
|
"B06 표본 넓힘: 새 측점에 없는 옛 횡단 행 %d건 project_id=%s", dropped, project_id
|
|
)
|
|
await _recompute_stored_designs(project_id, route_id) # [저장]과 같은 서버 재계산(실패 = 로그)
|
|
return await get_section_detail(project_id, route_id)
|