Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_7' and 'origin/sub_laptop_8' into sub_laptop_4
This commit is contained in:
@@ -4,6 +4,7 @@ import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
@@ -401,8 +402,10 @@ async def get_section_detail(
|
||||
async def regenerate_sections(
|
||||
project_id: UUID, route_id: int, request: SectionRegenerateRequest
|
||||
) -> SectionDetailResponse | JSONResponse:
|
||||
"""표시 옵션의 횡단 반폭으로 종횡단을 재생성해 저장하고 상세를 반환한다."""
|
||||
return await regenerate_route_sections(project_id, route_id, request.cross_half_width_m)
|
||||
"""표시 옵션의 횡단 반폭으로 종횡단을 재생성해 저장 · 상세 반환(사용자 값 유지 · 446)."""
|
||||
from B06_Section.B06_Section_Router_Widen import widen_route_sections
|
||||
|
||||
return await widen_route_sections(project_id, route_id, request.cross_half_width_m)
|
||||
|
||||
|
||||
async def regenerate_route_sections(
|
||||
@@ -411,6 +414,7 @@ async def regenerate_route_sections(
|
||||
cross_half_width_m: float,
|
||||
*,
|
||||
stored_options: dict[str, Any] | None = None,
|
||||
carry: Callable[[aiomysql.Connection], Awaitable[None]] | None = None,
|
||||
) -> SectionDetailResponse | JSONResponse:
|
||||
"""`regenerate_sections` 본체 — `stored_options` 를 주면 DB 최신 종단 옵션 대신 그것을 쓴다.
|
||||
|
||||
@@ -472,6 +476,8 @@ async def regenerate_route_sections(
|
||||
route_id=route_id,
|
||||
sections=sections["cross_sections"],
|
||||
)
|
||||
if carry: # 옛 사용자 값을 새 행에 — 같은 트랜잭션(446)
|
||||
await carry(connection)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""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)
|
||||
@@ -3,7 +3,8 @@
|
||||
* 횡단 카드 고르기 차례(295-1) — 고를 자리가 좁은 것이 위(모달 `pickAt` 과 같은 잣대).
|
||||
*
|
||||
* 판정면(구조물 · 띠 · 형상)을 다 꽂은 뒤 넓이 큰 것부터 아래로 다시 세움(작은 것 위) ·
|
||||
* 보라 선 판정(넓이 0 · 요구 185 사면 손잡이)은 맨 위 · 면적 띠(절 · 성토)는 맨 아래 그대로.
|
||||
* 보라 선 판정(넓이 0 · 요구 185 사면 손잡이)은 맨 위 · 면적 띠(절 · 성토)는 맨 아래 그대로 ·
|
||||
* 암 경계선 판정(굵은 투명 선)은 판정면 아래 — 구조물 아랫부분을 덮지 않음(440).
|
||||
* 끌고 뗀 누름(3 px 넘게 움직임)은 고름 아님.
|
||||
* 맹암거(306-2) = 안쪽 판정면은 맨 아래(겹친 띠 · 구조물이 위) · 테두리 판정(투명 굵은 선)이 면 판정 맨 위.
|
||||
* ========================================================================== */
|
||||
@@ -15,6 +16,8 @@ import { CENTER_HIT_CLASS } from "./B06_Section_UI_Cross_Center";
|
||||
const SURFACE_HIT_CLASSES = ["b06-chart__culvert-revet-hit", "b06-chart__shape-hit"];
|
||||
/** 선 판정 — 보라 설계선 조각(`_Cross_Design_Pick`). */
|
||||
const LINE_HIT_CLASS = "b06-chart__design-hit";
|
||||
/** 암 경계선 판정(`_Cross_Design_Pick` `ROCK_HIT_CLASS`) — 판정면 아래(440). */
|
||||
const ROCK_HIT_CLASS = "b06-chart__rock-hit";
|
||||
/** 맹암거 몸(그림) · 테두리 판정 클래스(306-2). */
|
||||
const UNDERDRAIN_GROUP = "b06-chart__underdrain";
|
||||
const UNDERDRAIN_BODY = "b06-chart__underdrain-body";
|
||||
@@ -69,9 +72,12 @@ export function stackPickTargets(layer: SVGElement): void {
|
||||
.map((el) => ({ el, area: pointsArea(el.getAttribute("points")) }))
|
||||
.sort((a, b) => b.area - a.area)
|
||||
.map(({ el }) => el);
|
||||
const lines = kids.filter((el) => el.classList.contains(LINE_HIT_CLASS));
|
||||
const rocks = kids.filter((el) => el.classList.contains(ROCK_HIT_CLASS));
|
||||
const lines = kids.filter(
|
||||
(el) => el.classList.contains(LINE_HIT_CLASS) && !el.classList.contains(ROCK_HIT_CLASS),
|
||||
);
|
||||
const centers = kids.filter((el) => el.classList.contains(CENTER_HIT_CLASS)); // 중심점 = 맨 위(356-3)
|
||||
layer.append(...surfaces, ...edges, ...lines, ...centers);
|
||||
layer.append(...rocks, ...surfaces, ...edges, ...lines, ...centers);
|
||||
let down: { x: number; y: number } | null = null;
|
||||
layer.addEventListener(
|
||||
"pointerdown",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { regenerateSections, type SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { loadSectionDetail } from "./B06_Section_Section_Store";
|
||||
|
||||
@@ -21,7 +26,8 @@ export interface SampleWidenerDeps {
|
||||
* 요청 반폭까지 지반 샘플을 넓히는 함수를 만든다 — 보유 샘플로 충분하면 즉시 true
|
||||
* (재계산 없음). 모자라면 그 폭으로 B05부터 재생성(영구 저장)하고 상세를 다시 받는다.
|
||||
* 느려도 허용(2026-08-06 사용자 확정). 전역 [전체 측점 반영]과 카드 줌 버튼의 반폭
|
||||
* 확대가 같은 경로를 쓴다(2026-08-23 사용자 지시).
|
||||
* 확대가 같은 경로를 쓴다(2026-08-23 사용자 지시). 다시 만들기 전 확인창 · 서버가 측점 설계 ·
|
||||
* 계획고 편집 · 유토 풍선을 옮겨 담음(446).
|
||||
*/
|
||||
export function createSampleWidener(deps: SampleWidenerDeps): (widthM: number) => Promise<boolean> {
|
||||
let running = false;
|
||||
@@ -31,8 +37,14 @@ export function createSampleWidener(deps: SampleWidenerDeps): (widthM: number) =
|
||||
if (widthM <= deps.sampledHalfWidth() + 1e-6) return true;
|
||||
if (running) return false; // 재생성 중 중복 요청은 무시(버튼 연타).
|
||||
running = true;
|
||||
showLoadingOverlay();
|
||||
let shown = false; // 오버레이는 셈 수 — 취소면 안 내림(남의 오버레이까지 내려감)
|
||||
try {
|
||||
const ask = L("B06_Profile_Regenerate_Confirm")
|
||||
.replace("{width}", String(widthM))
|
||||
.replace("{sampled}", String(deps.sampledHalfWidth()));
|
||||
if (!(await showConfirmDialog(ask, L("B06_Profile_Regenerate_Go")))) return false;
|
||||
showLoadingOverlay();
|
||||
shown = true;
|
||||
await regenerateSections(target.projectId, target.routeId, widthM);
|
||||
// 재생성 응답에는 설계가 없다 — 상세를 다시 받아 기본 설계 프리뷰까지 얹는다 · 공유 캐시도
|
||||
// 새 상세로(B05 가 옛 값을 못 보게) · 초안 측점 · 그림 자료도 같은 길(323-4)
|
||||
@@ -46,7 +58,7 @@ export function createSampleWidener(deps: SampleWidenerDeps): (widthM: number) =
|
||||
return false;
|
||||
} finally {
|
||||
running = false;
|
||||
hideLoadingOverlay();
|
||||
if (shown) hideLoadingOverlay();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user