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>
135 lines
4.2 KiB
Python
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
|