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>
This commit is contained in:
sinmb79
2026-03-24 21:49:44 +09:00
parent 0156d8ca4f
commit 48f1027f08
19 changed files with 1639 additions and 1 deletions

View File

@@ -0,0 +1,93 @@
"""Phase 2: agents, evms_snapshots, geofence_zones
Revision ID: 002
Revises: 001
Create Date: 2026-03-24
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "002"
down_revision = "001"
branch_labels = None
depends_on = None
def upgrade():
# ── agent_conversations ────────────────────────────────────────────────
op.create_table(
"agent_conversations",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("project_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("projects.id", ondelete="CASCADE"), nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
sa.Column("agent_type", sa.Enum("gongsa", "pumjil", "anjeon", "gumu", name="agent_type"), nullable=False),
sa.Column("title", sa.String(200), nullable=True),
sa.Column("status", sa.Enum("active", "closed", name="conversation_status"), server_default="active", nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
)
op.create_index("ix_agent_conversations_project", "agent_conversations", ["project_id"])
op.create_index("ix_agent_conversations_user", "agent_conversations", ["user_id"])
# ── agent_messages ─────────────────────────────────────────────────────
op.create_table(
"agent_messages",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("conversation_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("agent_conversations.id", ondelete="CASCADE"), nullable=False),
sa.Column("role", sa.String(20), nullable=False),
sa.Column("content", sa.Text, nullable=False),
sa.Column("metadata", postgresql.JSONB, nullable=True),
sa.Column("is_proactive", sa.Boolean, server_default="false", nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
)
op.create_index("ix_agent_messages_conversation", "agent_messages", ["conversation_id"])
# ── evms_snapshots ─────────────────────────────────────────────────────
op.create_table(
"evms_snapshots",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("project_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("projects.id", ondelete="CASCADE"), nullable=False),
sa.Column("snapshot_date", sa.Date, nullable=False, index=True),
sa.Column("total_budget", sa.Float, nullable=True),
sa.Column("planned_progress", sa.Float, nullable=True),
sa.Column("actual_progress", sa.Float, nullable=True),
sa.Column("pv", sa.Float, nullable=True),
sa.Column("ev", sa.Float, nullable=True),
sa.Column("ac", sa.Float, nullable=True),
sa.Column("spi", sa.Float, nullable=True),
sa.Column("cpi", sa.Float, nullable=True),
sa.Column("eac", sa.Float, nullable=True),
sa.Column("etc", sa.Float, nullable=True),
sa.Column("notes", sa.Text, nullable=True),
sa.Column("detail_json", postgresql.JSONB, nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
)
op.create_index("ix_evms_snapshots_project_date", "evms_snapshots", ["project_id", "snapshot_date"])
# ── geofence_zones ─────────────────────────────────────────────────────
op.create_table(
"geofence_zones",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("project_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("projects.id", ondelete="CASCADE"), nullable=False),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("zone_type", sa.String(50), nullable=False),
sa.Column("coordinates", postgresql.JSONB, nullable=False),
sa.Column("radius_m", sa.Float, nullable=True),
sa.Column("is_active", sa.Boolean, server_default="true", nullable=False),
sa.Column("description", sa.Text, nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
)
op.create_index("ix_geofence_zones_project", "geofence_zones", ["project_id"])
def downgrade():
op.drop_table("geofence_zones")
op.drop_table("evms_snapshots")
op.drop_table("agent_messages")
op.drop_table("agent_conversations")
op.execute("DROP TYPE IF EXISTS agent_type")
op.execute("DROP TYPE IF EXISTS conversation_status")