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
+38 -1
View File
@@ -9,10 +9,12 @@ from sqlalchemy import select
from app.deps import CurrentUser, DB
from app.models.project import Project
from app.models.daily_report import DailyReport, DailyReportPhoto
from app.services.vision_service import classify_photo, analyze_safety
from app.services.vision_service import classify_photo, analyze_safety, compare_with_drawing
router = APIRouter(prefix="/projects/{project_id}/vision", tags=["Vision AI"])
COMPARISON_TYPES = {"rebar": "철근 배근", "formwork": "거푸집", "general": "일반 비교"}
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp", "image/heic"}
MAX_SIZE_MB = 10
@@ -91,6 +93,41 @@ async def classify_field_photo(
return JSONResponse(content=result)
@router.post("/compare-drawing")
async def compare_drawing(
project_id: uuid.UUID,
db: DB,
current_user: CurrentUser,
field_photo: UploadFile = File(..., description="현장 사진"),
drawing: UploadFile = File(..., description="설계 도면 이미지"),
comparison_type: str = Form("rebar", description="rebar/formwork/general"),
):
"""
Vision AI Level 3 — 설계 도면 vs 현장 사진 비교 보조 판독
철근 배근, 거푸집 치수 등을 도면과 1차 비교합니다.
⚠️ 최종 합격/불합격 판정은 현장 책임자가 합니다.
"""
await _get_project_or_404(project_id, db)
if comparison_type not in COMPARISON_TYPES:
raise HTTPException(status_code=400, detail=f"comparison_type은 {list(COMPARISON_TYPES.keys())} 중 하나여야 합니다")
field_data = await field_photo.read()
drawing_data = await drawing.read()
if len(field_data) > MAX_SIZE_MB * 1024 * 1024 or len(drawing_data) > MAX_SIZE_MB * 1024 * 1024:
raise HTTPException(status_code=400, detail=f"파일 크기가 {MAX_SIZE_MB}MB를 초과합니다")
result = await compare_with_drawing(
field_photo=field_data,
drawing_image=drawing_data,
comparison_type=comparison_type,
field_media_type=field_photo.content_type or "image/jpeg",
drawing_media_type=drawing.content_type or "image/jpeg",
)
return JSONResponse(content=result)
@router.post("/safety-check")
async def safety_check(
project_id: uuid.UUID,