diff --git a/backend/alembic/versions/c1a2b3d4e5f6_add_reason_and_normalize_status.py b/backend/alembic/versions/c1a2b3d4e5f6_add_reason_and_normalize_status.py new file mode 100644 index 0000000..3f57274 --- /dev/null +++ b/backend/alembic/versions/c1a2b3d4e5f6_add_reason_and_normalize_status.py @@ -0,0 +1,31 @@ +"""add reason column to events and normalize status (dismissed → false_alarm) + +Revision ID: c1a2b3d4e5f6 +Revises: 3dfc3e213f69 +Create Date: 2026-05-11 16:00:00.000000 + +- events.reason String NULL 컬럼 추가 +- 기존 status='dismissed' 행을 'false_alarm' 으로 백필 (frontend 와 정렬) +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = 'c1a2b3d4e5f6' +down_revision: Union[str, None] = '3dfc3e213f69' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('events', sa.Column('reason', sa.String(), nullable=True)) + # frontend 가 'false_alarm' 을 기대 → 기존 'dismissed' 행 백필 + op.execute("UPDATE events SET status='false_alarm' WHERE status='dismissed'") + + +def downgrade() -> None: + op.execute("UPDATE events SET status='dismissed' WHERE status='false_alarm'") + op.drop_column('events', 'reason') diff --git a/backend/app/api/events.py b/backend/app/api/events.py index c94e90b..6311193 100644 --- a/backend/app/api/events.py +++ b/backend/app/api/events.py @@ -77,7 +77,7 @@ async def get_event_stats( today_total = await db.scalar(select(func.count(Event.id)).where(Event.timestamp >= start_of_day)) pending_count = await db.scalar(select(func.count(Event.id)).where(Event.status == 'pending')) confirmed_count = await db.scalar(select(func.count(Event.id)).where(Event.status == 'confirmed')) - false_alarm_count = await db.scalar(select(func.count(Event.id)).where(Event.status == 'dismissed')) + false_alarm_count = await db.scalar(select(func.count(Event.id)).where(Event.status == 'false_alarm')) return { "today_total": today_total or 0, @@ -122,7 +122,8 @@ async def create_event( - AI 추론 엔진(inference.py)에서 호출됩니다. """ # 1. DB에 사건 기록 (Persistence) - event = Event(**body.model_dump()) + # exclude_none: 비전송 필드는 모델 default 가 적용되도록 (event_type → 'unknown') + event = Event(**body.model_dump(exclude_none=True)) db.add(event) await db.flush() @@ -159,7 +160,8 @@ async def report_false_alarm( if not event: raise HTTPException(status_code=404, detail="해당 사건 기록을 찾을 수 없습니다.") - event.status = "dismissed" + event.status = "false_alarm" + event.reason = body.reason event.handled_by = current_admin.id event.handled_at = datetime.now() diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 0693c9e..bf73b8c 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -41,8 +41,9 @@ class Event(Base): clip_url: Mapped[Optional[str]] = mapped_column(String, nullable=True) track_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) confidence: Mapped[Optional[float]] = mapped_column(Float, nullable=True) - status: Mapped[str] = mapped_column(String, default="pending") # pending | confirmed | dismissed - event_type: Mapped[str] = mapped_column(String, default="unknown") # tag_fail | jump | emergency_door + status: Mapped[str] = mapped_column(String, default="pending") # pending | confirmed | false_alarm + event_type: Mapped[str] = mapped_column(String, default="unknown") # tailgating | unpaid | jump | crawling | unknown + reason: Mapped[Optional[str]] = mapped_column(String, nullable=True) # status == "false_alarm" 일 때 오탐 사유 handled_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("admins.id"), nullable=True) handled_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py index b6eaba0..0acdad3 100644 --- a/backend/app/schemas/schemas.py +++ b/backend/app/schemas/schemas.py @@ -45,6 +45,7 @@ class EventCreate(BaseModel): clip_url: Optional[str] = None track_id: Optional[int] = None confidence: Optional[float] = None + event_type: Optional[str] = None # AI 가 분류 결과를 함께 전송 (없으면 모델 default 'unknown') class EventResponse(BaseModel): @@ -55,6 +56,8 @@ class EventResponse(BaseModel): track_id: Optional[int] confidence: Optional[float] status: str + event_type: str + reason: Optional[str] = None handled_by: Optional[int] handled_at: Optional[datetime] @@ -62,7 +65,7 @@ class EventResponse(BaseModel): class EventStatusUpdate(BaseModel): - status: Literal["confirmed", "dismissed"] + status: Literal["confirmed", "false_alarm"] # ── Notification ──────────────────────────────────────────────────────────────