Files
Aislo/E02_StabGround/E02_StabGround_Router.py
T

145 lines
5.9 KiB
Python

"""E02 지반 조건 API (요구 347-2 · 3 · 4 · PLAN 532-3).
- `GET /api/projects/{id}/stab/ground` — 작업본(없으면 초기값) + 기본값 + 고칠 수 있는지
- `PUT …/stab/ground` — [저장] 검사 뒤 작업본 `E02_StabGround/ground.json`
· 측점(B06 암 경계)은 서버 값
- `POST …/stab/ground/reset` — [초기화] 초기값을 작업본에 덮어씀(재계산 아님)
초기값 = E 생성 직후 마스터 기본값 + 복사본 B06 측점(`ensure_initial`) · 그 밖에는 쓰지 않음.
공유 회사(보기만)는 `require_project_access` 가 PUT · POST 를 막음.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from uuid import UUID
import aiomysql
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from common_util.common_util_auth import verify_session
from common_util.common_util_json import atomic_write_json
from common_util.common_util_project_copy import MANIFEST_NAME
from common_util.common_util_storage import resolve_stored_project_path
from config.config_db import get_db_pool
from config.config_system_design import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
from E01_StabRegister.E01_StabRegister_Source import db_snapshot, source_dir
from E02_StabGround import E02_StabGround_Model as model
router = APIRouter(prefix="/api/projects", tags=["E02 Stab Ground"])
class GroundRequest(BaseModel):
ground: dict[str, Any]
def _b06_stations(project_root: Path) -> list[dict]:
"""측점 · 지반 라벨 · 토층 두께(암 측점 = 암 경계 깊이 · 토사 = 없음).
E01 동결 복사본(`E01_StabRegister/source/db.json`)만 읽음 — 복사본이 없으면 빈 목록.
"""
root = source_dir(project_root)
if not (root / MANIFEST_NAME).is_file():
return []
rows = sorted(db_snapshot(root)["cross_sections"], key=lambda r: float(r["chainage_m"] or 0))
stations = []
for row in rows:
data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"] or {}
design = data.get("design") or {}
ground_type = design.get("ground_type") or "soil"
offset = design.get("rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M)
rock = ground_type in model.ROCK_LABELS and offset is not None
stations.append(
{
"chainage_m": round(float(row["chainage_m"]), 3),
"ground_type": ground_type,
"soil_thickness_m": round(abs(float(offset)), 3) if rock else None,
}
)
return stations
async def _project(project_id: UUID) -> tuple[Path, dict[str, Any]]:
async with get_db_pool().acquire() as connection:
try:
stored = await get_project_storage_relative_path(connection, project_id)
except Exception as error:
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.") from error
async with connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"SELECT company_id, project_kind FROM projects WHERE id = %s",
(str(project_id),),
)
project = await cursor.fetchone()
if not project or project["project_kind"] != "STABILITY":
raise HTTPException(status_code=400, detail="안정성평가 프로젝트가 아닙니다.")
return Path(resolve_stored_project_path(stored)), project
def ensure_initial(project_root: Path) -> dict:
"""초기값이 없을 때만 한 번 — 기본값 + 복사본 B06 측점(E 생성 직후 · 옛 E 프로젝트는 첫 GET)."""
path = model.initial_path(project_root)
if path.is_file():
return json.loads(path.read_text(encoding="utf-8"))
ground = model.build_initial(model.load_defaults(), _b06_stations(project_root))
atomic_write_json(path, ground)
return ground
async def seed_initial(project_id: str) -> None:
"""E01 생성 직후 부름 — 초기값 한 벌."""
root, _ = await _project(UUID(project_id))
ensure_initial(root)
def _read_working(project_root: Path) -> dict[str, Any] | None:
path = model.ground_path(project_root)
return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None
def _editable(session: dict[str, Any], project: dict[str, Any]) -> bool:
return session["role"] == "SYSTEM_ADMIN" or session.get("company_id") == project["company_id"]
@router.get("/{project_id}/stab/ground")
async def get_ground(
project_id: UUID, session: dict[str, Any] = Depends(verify_session)
) -> dict[str, Any]:
root, project = await _project(project_id)
initial = ensure_initial(root)
working = _read_working(root)
return {
"ground": working or initial,
"saved": working is not None,
"defaults": model.load_defaults(),
"editable": _editable(session, project),
}
@router.put("/{project_id}/stab/ground")
async def put_ground(project_id: UUID, payload: GroundRequest) -> dict[str, Any]:
root, _ = await _project(project_id)
defaults = model.load_defaults()
errors = model.check_ground(payload.ground, defaults["limits"])
if errors:
raise HTTPException(status_code=422, detail={"errors": errors})
ground = model.clean(payload.ground)
# 측점(B06 암 경계)은 서버 값 — 브라우저 값을 받아 적지 않음
base = _read_working(root) or ensure_initial(root)
ground["stations"] = base.get("stations", [])
ground["version"] = defaults["version"]
atomic_write_json(model.ground_path(root), ground)
return {"ground": ground}
@router.post("/{project_id}/stab/ground/reset")
async def reset_ground(project_id: UUID) -> dict[str, Any]:
root, _ = await _project(project_id)
initial = ensure_initial(root)
atomic_write_json(model.ground_path(root), initial)
return {"ground": initial}