Files
conai/backend/app/api/evms.py
sinmb79 48f1027f08 feat: Phase 2 구현 — AI 에이전트 4인방, EVMS, Vision AI, Geofence
AI 에이전트 (Layer 2):
- GONGSA: 공사 담당 (공정 브리핑, 공기 지연 감지, 날씨 연동 작업 조정)
- PUMJIL: 품질 담당 (시공 전 체크리스트, Vision 보조 판독, 시험 기한 추적)
- ANJEON: 안전 담당 (위험 공정 경보, TBM 생성, 중대재해처벌법 Q&A)
- GUMU: 공무 담당 (인허가 능동 추적, 기성청구 제안, 보고서 초안)
- 에이전트 라우터 (키워드 기반 자동 분배), 아침 브리핑 엔드포인트

EVMS 기본:
- PV·EV·AC·SPI·CPI 산출 (WBS/Task 기반)
- EAC·ETC 예측, 스냅샷 이력 저장

Vision AI:
- Level 1: 현장 사진 분류 (Claude Vision), 작업일보 자동 첨부
- Level 2: 안전장비(안전모/조끼) 착용 감지

Geofence 위험구역:
- 구역 CRUD (굴착면, 크레인 반경, 밀폐공간 등)
- 진입 이벤트 웹훅 (익명 — 개인 이동 경로 비수집)

인허가 자동도출:
- 공종 입력 → AI가 필요 인허가 목록 자동 도출 + 체크리스트 생성

DB 마이그레이션 (002):
- agent_conversations, agent_messages, evms_snapshots, geofence_zones

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:49:44 +09:00

135 lines
4.2 KiB
Python

"""EVMS API — PV·EV·AC·SPI·CPI"""
import uuid
from datetime import date
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.deps import CurrentUser, DB
from app.models.evms import EVMSSnapshot
from app.models.project import Project
from app.services.evms_service import compute_evms
router = APIRouter(prefix="/projects/{project_id}/evms", tags=["EVMS"])
class EVMSRequest(BaseModel):
snapshot_date: date = date.today()
actual_cost: float | None = None # 실제 투입 비용 (없으면 추정)
save: bool = True # DB 저장 여부
class EVMSResponse(BaseModel):
id: uuid.UUID | None = None
project_id: uuid.UUID
snapshot_date: date
total_budget: float | None
planned_progress: float | None
actual_progress: float | None
pv: float | None
ev: float | None
ac: float | None
spi: float | None
cpi: float | None
eac: float | None
etc: float | None
detail_json: dict | None = None
model_config = {"from_attributes": True}
async def _get_project_or_404(project_id: uuid.UUID, db: DB) -> Project:
r = await db.execute(select(Project).where(Project.id == project_id))
p = r.scalar_one_or_none()
if not p:
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
return p
@router.post("/compute", response_model=EVMSResponse)
async def compute_evms_endpoint(
project_id: uuid.UUID,
data: EVMSRequest,
db: DB,
current_user: CurrentUser,
):
"""EVMS 계산 (옵션: DB 저장)"""
await _get_project_or_404(project_id, db)
result = await compute_evms(db, project_id, data.snapshot_date, data.actual_cost)
if data.save:
snapshot = EVMSSnapshot(
project_id=project_id,
snapshot_date=data.snapshot_date,
total_budget=result["total_budget"],
planned_progress=result["planned_progress"],
actual_progress=result["actual_progress"],
pv=result["pv"],
ev=result["ev"],
ac=result["ac"],
spi=result["spi"],
cpi=result["cpi"],
eac=result["eac"],
etc=result["etc"],
detail_json={"tasks": result["detail"]},
)
db.add(snapshot)
await db.commit()
await db.refresh(snapshot)
return EVMSResponse(
id=snapshot.id,
project_id=project_id,
snapshot_date=snapshot.snapshot_date,
total_budget=snapshot.total_budget,
planned_progress=snapshot.planned_progress,
actual_progress=snapshot.actual_progress,
pv=snapshot.pv, ev=snapshot.ev, ac=snapshot.ac,
spi=snapshot.spi, cpi=snapshot.cpi,
eac=snapshot.eac, etc=snapshot.etc,
detail_json=snapshot.detail_json,
)
return EVMSResponse(
project_id=project_id,
snapshot_date=data.snapshot_date,
total_budget=result["total_budget"],
planned_progress=result["planned_progress"],
actual_progress=result["actual_progress"],
pv=result["pv"], ev=result["ev"], ac=result["ac"],
spi=result["spi"], cpi=result["cpi"],
eac=result["eac"], etc=result["etc"],
detail_json={"tasks": result["detail"]},
)
@router.get("", response_model=list[EVMSResponse])
async def list_snapshots(
project_id: uuid.UUID, db: DB, current_user: CurrentUser
):
"""EVMS 스냅샷 이력 조회"""
r = await db.execute(
select(EVMSSnapshot)
.where(EVMSSnapshot.project_id == project_id)
.order_by(EVMSSnapshot.snapshot_date.desc())
)
return r.scalars().all()
@router.get("/latest", response_model=EVMSResponse)
async def latest_snapshot(
project_id: uuid.UUID, db: DB, current_user: CurrentUser
):
"""최근 EVMS 스냅샷 조회"""
r = await db.execute(
select(EVMSSnapshot)
.where(EVMSSnapshot.project_id == project_id)
.order_by(EVMSSnapshot.snapshot_date.desc())
.limit(1)
)
snap = r.scalar_one_or_none()
if not snap:
raise HTTPException(status_code=404, detail="EVMS 스냅샷이 없습니다. /compute 를 먼저 실행하세요.")
return snap