feat: Phase 3 구현 — 완전 자동화, 준공도서, Vision L3, 발주처 포털

EVMS 완전 자동화:
- 공기 지연 AI 예측 (SPI 기반 준공일 예측)
- 기성청구 가능 금액 자동 산출
- 매일 자정 EVMS 스냅샷 자동 생성 (APScheduler)
- 매일 07:00 GONGSA 아침 브리핑 자동 생성

준공도서 패키지:
- 준공 요약 + 품질시험 목록 + 검측 이력 + 인허가 현황 → ZIP 번들
- 준공 준비 체크리스트 API
- 4종 HTML 템플릿 (WeasyPrint PDF 출력)

Vision AI Level 3:
- 설계 도면 vs 현장 사진 비교 보조 판독 (Claude Vision)
- 철근 배근, 거푸집 치수 1차 분석

설계도서 파싱:
- PDF 이미지/텍스트에서 공종·수량·규격 자동 추출
- Pandoc HWP 출력 지원

발주처 전용 포털:
- 토큰 기반 읽기 전용 API
- 공사 현황 대시보드, 공정률 추이 차트

에이전트 협업 고도화:
- 협업 시나리오 (concrete_pour, excavation, weekly_report)
- GONGSA→PUMJIL→ANJEON 순차 처리

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sinmb79
2026-03-24 22:02:29 +09:00
parent 48f1027f08
commit 5a044a3882
17 changed files with 1350 additions and 8 deletions
+59 -1
View File
@@ -9,7 +9,7 @@ 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
from app.services.evms_service import compute_evms, predict_delay, compute_progress_claim
router = APIRouter(prefix="/projects/{project_id}/evms", tags=["EVMS"])
@@ -117,6 +117,64 @@ async def list_snapshots(
return r.scalars().all()
@router.get("/delay-forecast")
async def delay_forecast(
project_id: uuid.UUID, db: DB, current_user: CurrentUser
):
"""공기 지연 AI 예측 (최근 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 스냅샷이 없습니다")
project = await _get_project_or_404(project_id, db)
planned_end = project.end_date
if planned_end and not isinstance(planned_end, date):
from datetime import date as ddate
planned_end = ddate.fromisoformat(str(planned_end))
forecast = await predict_delay(
db, project_id,
spi=snap.spi,
planned_end=planned_end,
snapshot_date=snap.snapshot_date,
)
forecast["spi"] = snap.spi
forecast["cpi"] = snap.cpi
forecast["snapshot_date"] = str(snap.snapshot_date)
return forecast
@router.get("/progress-claim")
async def progress_claim(
project_id: uuid.UUID,
already_claimed_pct: float = 0.0,
db: DB = None,
current_user: CurrentUser = None,
):
"""기성청구 가능 금액 산출"""
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 await compute_progress_claim(
total_budget=snap.total_budget or 0,
actual_progress=snap.actual_progress or 0,
already_claimed_pct=already_claimed_pct,
)
@router.get("/latest", response_model=EVMSResponse)
async def latest_snapshot(
project_id: uuid.UUID, db: DB, current_user: CurrentUser