Files
Aislo/E01_StabRegister/E01_StabRegister_Source.py
T
eomsangdonandClaude Opus 5.5 f2c3aefae2 fix(532-1): E01 복사본 읽기 — 횡단 경로 null · 파일 없음 · 옛 꼴도 B06 이름 규칙으로 찾음 · 없으면 그 측점만 건너뜀 · 생성 복사 경고(요구 351)
- Source.stations · cross_section: 경로 칸 null · 파일 없음 · 절대 경로 · 옛 정수 m 이름이면 find_cross_file 로 찾음 · 그래도 없으면 「횡단 없음」(stations 에서 빠짐 · cross_section KeyError) · missing_stations 추가
- copy_stability: 같은 검사 → manifest missing_cross_chainages · warnings · E01 생성 응답 warnings
- 실측: 검증 프로젝트 14 복사 모두 stations 성공 · 검증_브레인232 null 1 행(359 m) 살림

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:05:14 +09:00

157 lines
5.9 KiB
Python

"""E 안정성평가 복사본 읽기 — storage 를 아는 유일한 자리 (요구 345 · 532-1).
복사본 = E 프로젝트 저장소 `E01_StabRegister/source/`(`common_util_project_copy.copy_stability`
이 생성 때 한 번 만듦 · 그 뒤 불변). E 엔진 · 화면은 이 함수들로만 받음 — 원본 임도 프로젝트를
읽지 않음.
"""
from __future__ import annotations
import json
from functools import lru_cache
from pathlib import Path
from typing import Any
import aiomysql
from common_util.common_util_project_copy import (
MANIFEST_NAME,
STABILITY_DB_NAME,
STABILITY_SOURCE_DIR,
resolve_cross_file,
)
from common_util.common_util_storage import resolve_stored_project_path
from config.config_db import get_db_pool
def source_dir(project_root: Path | str) -> Path:
"""E 프로젝트 루트 → 복사본 폴더."""
return Path(project_root) / STABILITY_SOURCE_DIR
async def project_source_dir(project_id: str) -> Path:
"""E project_id → 복사본 폴더(없으면 LookupError)."""
async with (
get_db_pool().acquire() as connection,
connection.cursor(aiomysql.DictCursor) as cursor,
):
await cursor.execute(
"SELECT storage_path FROM projects"
" WHERE id = %s AND project_kind = 'STABILITY' AND deleted_at IS NULL",
(project_id,),
)
row = await cursor.fetchone()
if not row:
raise LookupError(f"안정성평가 프로젝트가 아님: {project_id}")
root = source_dir(resolve_stored_project_path(row["storage_path"]))
if not (root / MANIFEST_NAME).is_file():
raise LookupError(f"복사본이 없음: {project_id}")
return root
@lru_cache(maxsize=8)
def _read_json(path: str, mtime_ns: int) -> Any:
return json.loads(Path(path).read_text(encoding="utf-8"))
def _json(path: Path) -> Any:
return _read_json(str(path), path.stat().st_mtime_ns)
def manifest(root: Path) -> dict[str, Any]:
"""복사 목록 — 원본 id · 복사 시각 · 지문 · 파일."""
return _json(root / MANIFEST_NAME)
def db_snapshot(root: Path) -> dict[str, Any]:
"""동결 DB 행 — project · project_workflow_stages · surface_params · route ·
route_points · route_statistics · longitudinal_sections · cross_sections."""
return _json(root / STABILITY_DB_NAME)
def _cross_rows(root: Path) -> list[dict[str, Any]]:
rows = [row for row in db_snapshot(root)["cross_sections"] if row["chainage_m"] is not None]
return sorted(rows, key=lambda row: (float(row["chainage_m"]), row["sequence_num"] or 0))
def _row_data(row: dict[str, Any]) -> dict[str, Any]:
data = row.get("data")
return (json.loads(data) if isinstance(data, str) else data) or {}
def stations(root: Path) -> list[dict[str, Any]]:
"""측점 목록(누가 차례) — chainage_m · label · 중심 좌표 · 방위각 · 오르막 쪽 · 지반 유형.
횡단 파일을 못 찾은 측점(`missing_stations`)은 「횡단 없음」으로 빠짐 — 전체는 안 깨짐."""
out = []
for row in _cross_rows(root):
path = resolve_cross_file(root, row)
if path is None:
continue
cross = _json(path)
out.append(
{
"chainage_m": float(row["chainage_m"]),
"label": cross.get("label"),
"kind": cross.get("kind"),
"center_x": cross.get("center_x"),
"center_y": cross.get("center_y"),
"center_z": cross.get("center_z"),
"azimuth_deg": cross.get("azimuth_deg"),
"uphill_side": cross.get("uphill_side"),
"ground_type": (_row_data(row).get("design") or {}).get("ground_type"),
}
)
return out
def missing_stations(root: Path) -> list[float]:
"""횡단 없음으로 빠진 측점 누가거리 — 화면 알림용(개수 = len)."""
return [
float(row["chainage_m"])
for row in _cross_rows(root)
if resolve_cross_file(root, row) is None
]
def cross_section(root: Path, chainage_m: float) -> dict[str, Any]:
"""한 측점 횡단 — 지반선(samples: offset_m · x · y · z · valid) · 좌표틀 · B06 설계값
(`design`: design_line · road_edges · section_mode · 절성토 경사 · 소단 · ground_type ·
rock_boundary_offset_m(암 경계 = 지반선을 그만큼 내린 선 · 암 측점만) …).
측점이 없거나 횡단 없음이면 KeyError."""
for row in _cross_rows(root):
if abs(float(row["chainage_m"]) - float(chainage_m)) < 1e-6:
path = resolve_cross_file(root, row)
if path is None:
raise KeyError(f"횡단 없음: {chainage_m}")
cross = _json(path)
return {
"chainage_m": float(row["chainage_m"]),
"label": cross.get("label"),
"frame": cross.get("frame"),
"uphill_side": cross.get("uphill_side"),
"samples": cross.get("samples") or [],
"design": _row_data(row).get("design") or {},
}
raise KeyError(f"측점 없음: {chainage_m}")
def terrain_grid_path(root: Path) -> Path:
"""복사한 지형 격자(DTM npz — 원본 설계가 쓴 그 하나)."""
found = sorted((root / "B04_PreProcess" / "models").glob("dtm_*.npz"))
if len(found) != 1:
raise FileNotFoundError(f"복사한 지형 격자가 하나가 아님: {[p.name for p in found]}")
return found[0]
def corridor_path(root: Path) -> Path | None:
"""최신 노선 3D 노선면(없으면 None)."""
found = sorted((root / "B05_Profile" / "corridor").glob("corridor_*.json"))
return found[0] if found else None
def structures_path(root: Path) -> Path | None:
"""노선 구조물 정본(벽 등 · 없으면 None)."""
path = root / "B05_Profile" / "route" / "structures.json"
return path if path.is_file() else None