Files
Aislo/E01_StabRegister/E01_StabRegister_Router.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

131 lines
5.3 KiB
Python

"""E01 안정성평가 생성 API (요구 345 · 347 · 요구서 4-1).
대상 = 같은 회사 임도(ROAD) 중 B06 횡단 단계(stage 3) 완료. 생성 때 해석에 쓰는 자료만
`E01_StabRegister/source/` 로 동결 복사(532-1 · 원본이 바뀌어도 불변)
— 실패하면 만든 행 · 폴더를 지움.
"""
import logging
from datetime import datetime
from pathlib import Path
from typing import Any
import aiomysql
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from common_util.common_util_auth import require_company
from common_util.common_util_project_copy import copy_project
from common_util.common_util_project_delete import hard_delete_project
from common_util.common_util_project_kind import create_kind_project
from common_util.common_util_storage import resolve_stored_project_path
from config.config_db import get_db_pool
from E02_StabGround.E02_StabGround_Router import seed_initial
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/e01", tags=["E01_StabRegister"])
_SOURCE_SQL = """
SELECT p.id AS project_id, p.name, p.region, p.road_type, p.project_year,
DATE_FORMAT(p.updated_at, '%%Y-%%m-%%dT%%H:%%i:%%s') AS updated_at,
s.state = 'COMPLETE' AS section_complete
FROM projects p
LEFT JOIN project_workflow_stages s ON s.project_id = p.id AND s.stage_no = 3
WHERE p.company_id = %s AND p.project_kind = 'ROAD' AND p.deleted_at IS NULL
"""
class CreateStabRequest(BaseModel):
project_name: str | None = Field(default=None, max_length=255)
source_project_id: str = Field(..., min_length=36, max_length=36)
async def _sources(company_id: Any, project_id: str | None = None) -> list[dict[str, Any]]:
sql, args = _SOURCE_SQL, [company_id]
if project_id:
sql, args = sql + " AND p.id = %s", [company_id, project_id]
async with (
get_db_pool().acquire() as connection,
connection.cursor(aiomysql.DictCursor) as cursor,
):
await cursor.execute(sql + " ORDER BY p.updated_at DESC", tuple(args))
return list(await cursor.fetchall())
@router.get("/source-candidates")
async def source_candidates(
session: dict[str, Any] = Depends(require_company),
) -> dict[str, Any]:
"""B06 까지 끝난 같은 회사 임도 프로젝트."""
rows = await _sources(session.get("company_id"))
projects = [
{k: v for k, v in r.items() if k != "section_complete"}
for r in rows
if r["section_complete"]
]
return {"status": "success", "projects": projects}
@router.post("/project")
async def post_stab_project(
request: Request,
payload: CreateStabRequest,
session: dict[str, Any] = Depends(require_company),
) -> dict[str, Any]:
"""안정성평가 프로젝트 생성 — 원본 B06 미완료면 400 · 횡단 없는 측점은 warnings."""
company_id = session.get("company_id")
rows = await _sources(company_id, payload.source_project_id) if company_id else []
if not rows:
raise HTTPException(status_code=404, detail="같은 회사 임도 프로젝트가 아닙니다.")
source = rows[0]
if not source["section_complete"]:
raise HTTPException(
status_code=400, detail="원본 임도의 횡단 설계(B06)가 끝나지 않았습니다."
)
name = (payload.project_name or "").strip() or f"{source['name']} 안정성평가"
project_id = await create_kind_project(
request=request,
user_id=int(session["user_id"]),
company_id=int(company_id),
kind="STABILITY",
name=name,
region=source["region"],
road_type=source["road_type"],
project_year=source["project_year"],
source_project_id=source["project_id"],
)
try:
manifest = await _copy_source(project_id, source["project_id"])
await seed_initial(project_id) # E02 초기값(기본값 + 복사본 B06 측점)
except Exception:
logger.exception("E01 복사 실패 — 만든 프로젝트를 지움: %s", project_id)
await hard_delete_project(project_id, int(session["user_id"]), request)
raise HTTPException(status_code=500, detail="원본 자료 복사에 실패했습니다.") from None
return {
"project_id": project_id,
"source_fingerprint": manifest["source_fingerprint"],
"warnings": manifest.get("warnings", []),
}
async def _copy_source(project_id: str, source_id: str) -> dict[str, Any]:
"""원본 → E 복사본 + 지문 · 복사 시각 기록."""
async with get_db_pool().acquire() as connection:
async with connection.cursor() as cursor:
await cursor.execute("SELECT storage_path FROM projects WHERE id = %s", (project_id,))
(storage_path,) = await cursor.fetchone()
manifest = await copy_project(
connection,
source_id,
scope="stability",
new_id=project_id,
project_root=Path(resolve_stored_project_path(storage_path)),
)
async with connection.cursor() as cursor:
await cursor.execute(
"UPDATE projects SET source_fingerprint = %s, source_copied_at = %s WHERE id = %s",
(manifest["source_fingerprint"], datetime.utcnow(), project_id),
)
return manifest