Files
conai/backend/app/api/completion.py
sinmb79 5a044a3882 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>
2026-03-24 22:02:29 +09:00

114 lines
3.8 KiB
Python

"""준공도서 패키지 API"""
import uuid
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
import io
from sqlalchemy import select
from app.deps import CurrentUser, DB
from app.models.project import Project
from app.services.completion_service import build_completion_package
router = APIRouter(prefix="/projects/{project_id}/completion", tags=["준공도서"])
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.get("/download")
async def download_completion_package(
project_id: uuid.UUID,
db: DB,
current_user: CurrentUser,
):
"""
준공도서 ZIP 패키지 다운로드
포함 문서:
- 준공 요약 (전체 실적)
- 품질시험 목록 (전체)
- 검측 이력 (전체)
- 인허가 현황 (전체)
"""
await _get_project_or_404(project_id, db)
zip_bytes, filename = await build_completion_package(db, project_id)
return StreamingResponse(
io.BytesIO(zip_bytes),
media_type="application/zip",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"},
)
@router.get("/checklist")
async def completion_checklist(
project_id: uuid.UUID,
db: DB,
current_user: CurrentUser,
):
"""
준공 준비 체크리스트 — 부족한 서류/데이터 현황 반환
"""
from app.models.daily_report import DailyReport
from app.models.quality import QualityTest
from app.models.inspection import InspectionRequest, InspectionStatus
from app.models.permit import PermitItem, PermitStatus
from sqlalchemy import func
await _get_project_or_404(project_id, db)
async def count(model, where):
r = await db.execute(select(func.count()).where(*where))
return r.scalar() or 0
total_dr = await count(DailyReport, [DailyReport.project_id == project_id])
total_qt = await count(QualityTest, [QualityTest.project_id == project_id])
total_insp = await count(InspectionRequest, [InspectionRequest.project_id == project_id])
done_insp = await count(InspectionRequest, [
InspectionRequest.project_id == project_id,
InspectionRequest.status == InspectionStatus.COMPLETED,
])
total_per = await count(PermitItem, [PermitItem.project_id == project_id])
approved_per = await count(PermitItem, [
PermitItem.project_id == project_id,
PermitItem.status == PermitStatus.APPROVED,
])
checks = [
{
"item": "작업일보",
"count": total_dr,
"status": "준비완료" if total_dr > 0 else "누락",
"ok": total_dr > 0,
},
{
"item": "품질시험 기록",
"count": total_qt,
"status": "준비완료" if total_qt > 0 else "누락",
"ok": total_qt > 0,
},
{
"item": "검측 완료",
"count": f"{done_insp}/{total_insp}",
"status": "완료" if total_insp > 0 and done_insp == total_insp else f"미완료 {total_insp - done_insp}",
"ok": total_insp > 0 and done_insp == total_insp,
},
{
"item": "인허가 취득",
"count": f"{approved_per}/{total_per}",
"status": "완료" if total_per > 0 and approved_per == total_per else f"미취득 {total_per - approved_per}",
"ok": total_per > 0 and approved_per == total_per,
},
]
all_ok = all(c["ok"] for c in checks)
return {
"ready": all_ok,
"summary": "준공 준비 완료" if all_ok else "준공 서류 미비 항목이 있습니다",
"checks": checks,
}