48f1027f08
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>
159 lines
5.4 KiB
Python
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
|