Files
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

159 lines
5.4 KiB
Python

import uuid
from fastapi import APIRouter, HTTPException, status
from sqlalchemy import select
from app.deps import CurrentUser, DB
from app.models.permit import PermitItem, PermitStatus
from app.models.project import Project
from pydantic import BaseModel
from datetime import date, datetime
from app.services.ai_engine import complete
class PermitCreate(BaseModel):
permit_type: str
authority: str | None = None
required: bool = True
deadline: date | None = None
notes: str | None = None
sort_order: int = 0
class PermitUpdate(BaseModel):
status: PermitStatus | None = None
submitted_date: date | None = None
approved_date: date | None = None
notes: str | None = None
deadline: date | None = None
class PermitResponse(BaseModel):
id: uuid.UUID
project_id: uuid.UUID
permit_type: str
authority: str | None
required: bool
deadline: date | None
status: PermitStatus
submitted_date: date | None
approved_date: date | None
notes: str | None
sort_order: int
created_at: datetime
model_config = {"from_attributes": True}
router = APIRouter(prefix="/projects/{project_id}/permits", tags=["인허가 체크리스트"])
async def _get_project_or_404(project_id: uuid.UUID, db: DB) -> Project:
result = await db.execute(select(Project).where(Project.id == project_id))
p = result.scalar_one_or_none()
if not p:
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
return p
@router.get("", response_model=list[PermitResponse])
async def list_permits(project_id: uuid.UUID, db: DB, current_user: CurrentUser):
result = await db.execute(
select(PermitItem).where(PermitItem.project_id == project_id).order_by(PermitItem.sort_order)
)
return result.scalars().all()
@router.post("", response_model=PermitResponse, status_code=status.HTTP_201_CREATED)
async def create_permit(project_id: uuid.UUID, data: PermitCreate, db: DB, current_user: CurrentUser):
await _get_project_or_404(project_id, db)
permit = PermitItem(**data.model_dump(), project_id=project_id)
db.add(permit)
await db.commit()
await db.refresh(permit)
return permit
@router.put("/{permit_id}", response_model=PermitResponse)
async def update_permit(project_id: uuid.UUID, permit_id: uuid.UUID, data: PermitUpdate, db: DB, current_user: CurrentUser):
result = await db.execute(select(PermitItem).where(PermitItem.id == permit_id, PermitItem.project_id == project_id))
permit = result.scalar_one_or_none()
if not permit:
raise HTTPException(status_code=404, detail="인허가 항목을 찾을 수 없습니다")
for field, value in data.model_dump(exclude_none=True).items():
setattr(permit, field, value)
await db.commit()
await db.refresh(permit)
return permit
@router.delete("/{permit_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_permit(project_id: uuid.UUID, permit_id: uuid.UUID, db: DB, current_user: CurrentUser):
result = await db.execute(select(PermitItem).where(PermitItem.id == permit_id, PermitItem.project_id == project_id))
permit = result.scalar_one_or_none()
if not permit:
raise HTTPException(status_code=404, detail="인허가 항목을 찾을 수 없습니다")
await db.delete(permit)
await db.commit()
class PermitDerivationRequest(BaseModel):
work_types: list[str]
construction_type: str
start_date: date | None = None
auto_create: bool = True
@router.post("/derive", response_model=list[PermitResponse], status_code=status.HTTP_201_CREATED)
async def derive_permits(
project_id: uuid.UUID,
data: PermitDerivationRequest,
db: DB,
current_user: CurrentUser,
):
"""공종 입력 → 필요 인허가 항목 AI 자동 도출"""
await _get_project_or_404(project_id, db)
system = """당신은 건설공사 인허가 전문가입니다.
공사 정보를 보고 필요한 인허가 항목을 JSON 배열로만 반환하세요.
각 항목: {"permit_type": "도로점용허가", "authority": "관할 시·군·구청", "notes": "착공 30일 전 신청"}
법령 근거를 notes에 간략히 포함하세요."""
user_msg = (
f"공사 유형: {data.construction_type}\n"
f"주요 공종: {', '.join(data.work_types)}\n"
f"착공 예정: {data.start_date or '미정'}\n\n"
"이 공사에 필요한 인허가 항목을 모두 도출해주세요. JSON 배열로만 반환하세요."
)
raw = await complete(
messages=[{"role": "user", "content": user_msg}],
system=system,
temperature=0.2,
)
import json, re
json_match = re.search(r"\[.*\]", raw, re.DOTALL)
if not json_match:
raise HTTPException(status_code=500, detail="AI 응답 파싱 실패")
items_data = json.loads(json_match.group())
created = []
if data.auto_create:
for idx, item in enumerate(items_data):
permit = PermitItem(
project_id=project_id,
permit_type=item.get("permit_type", "미정"),
authority=item.get("authority"),
required=True,
notes=item.get("notes"),
sort_order=idx,
deadline=data.start_date,
)
db.add(permit)
created.append(permit)
await db.commit()
for p in created:
await db.refresh(p)
return created