"""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 B05_Profile.B05_Profile_Repository import get_latest_route from common_util.common_util_auth import verify_session from common_util.common_util_json import atomic_write_json 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 E02_StabGround import E02_StabGround_Model as model router = APIRouter(prefix="/api/projects", tags=["E02 Stab Ground"]) class GroundRequest(BaseModel): ground: dict[str, Any] async def _b06_stations(connection: aiomysql.Connection, project_ids: list[str]) -> list[dict]: """측점 · 지반 라벨 · 토층 두께(암 측점 = 암 경계 깊이 · 토사 = 없음). 자기 복사본(532-1)이 먼저 · 없으면 원본 임도.""" for project_id in project_ids: route = await get_latest_route(connection, UUID(project_id)) if not route: continue async with connection.cursor() as cursor: await cursor.execute( "SELECT chainage_m, data FROM cross_sections WHERE route_id = %s" " ORDER BY chainage_m", (route["id"],), ) rows = await cursor.fetchall() stations = [] for chainage, data in rows: data = json.loads(data) if isinstance(data, str) else 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(chainage), 3), "ground_type": ground_type, "soil_thickness_m": round(abs(float(offset)), 3) if rock else None, } ) if stations: return stations return [] 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, source_project_id 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 async def ensure_initial(project_root: Path, project_id: str, source_id: str | None) -> dict: """초기값이 없을 때만 한 번 — 기본값 + B06 측점(E 생성 직후 · 옛 E 프로젝트는 첫 GET).""" path = model.initial_path(project_root) if path.is_file(): return json.loads(path.read_text(encoding="utf-8")) async with get_db_pool().acquire() as connection: ids = [project_id] + ([source_id] if source_id else []) stations = await _b06_stations(connection, ids) ground = model.build_initial(model.load_defaults(), stations) atomic_write_json(path, ground) return ground async def seed_initial(project_id: str) -> None: """E01 생성 직후 부름 — 초기값 한 벌.""" root, project = await _project(UUID(project_id)) await ensure_initial(root, project_id, project["source_project_id"]) 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 = await ensure_initial(root, str(project_id), project["source_project_id"]) 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, project = 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 await ensure_initial( root, str(project_id), project["source_project_id"] ) 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, project = await _project(project_id) initial = await ensure_initial(root, str(project_id), project["source_project_id"]) atomic_write_json(model.ground_path(root), initial) return {"ground": initial}