Files
Aislo/E02_StabGround/E02_StabGround_Router.py
T
eomsangdonandClaude Opus 5.5 f0c5232f80 feat(532-3): E02 지반 조건 — 기본값 정본 · 물성 표(지층 추가 · 지움) · 구간 덮어쓰기 · KDS 기준안전율 · 노면 하중 · 지하수 · 지진 · 저장 · 초기화
- 기본값 정본 resources/master_template/stability/ground_defaults.json — 지층 여섯 · 값마다 출처(07 표 · 절) · KDS 11 70 05 표 4.3-1~3 · 노면 10 kPa · 지하수 규칙 · 지진 끔 · 범위
- 07 원문 대조 — 성토 φ 25 → 28(07 §4 종합 28~30 아래값) · 나머지 범위 안
- API GET · PUT /api/projects/{id}/stab/ground · POST …/reset — 작업본 E02_StabGround/ground.json · 초기값 initial_snapshot/E02_StabGround/ground.json(E 생성 직후 기본값 + B06 측점 암 경계) · 측점은 서버 값
- 검사 · 측점 적용 = 파이썬 · TS 짝(E02_StabGround_Model.py ↔ logic/E02_StabGround_Check.ts) · 거울 시험
- 화면 — 카드 넷 · 칸 위 근거 · 기본값 툴팁 · 틀린 칸 표시 · sessionStorage 캐시 · 자동저장 없음
- test_528 E01 시험 — 생성 뒤 E02 초기값 부름 확인 더함

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

157 lines
6.6 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 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}