Files
Aislo/B06_Section/B06_Section_Router_Widen.py
T
eomsangdonandClaude Opus 5.5 341e83114e fix(446 B06 표본 넓힘): 카드 폭 넘김 다시 만들기가 측점 설계 · 표준 횡단면 · 유토 풍선 · 계획고 편집 옮겨 담음 · 다시 만들기 전 확인창
- 반폭 재생성 = 옛 횡단 행 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
2026-10-18 18:42:21 +09:00

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)