From c0b73bbc90a1a80e45eaa098668b236aa5fc4bf9 Mon Sep 17 00:00:00 2001 From: CHOSOOGEUN <241898164+CHOSOOGEUN@users.noreply.github.com> Date: Mon, 11 May 2026 14:22:18 +0900 Subject: [PATCH] =?UTF-8?q?feat(events):=20event=5Ftype/reason=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=85=B8=EC=B6=9C=20+=20status=20=EA=B0=92=20?= =?UTF-8?q?=EC=A0=95=EA=B7=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 요청: GET /api/events/ 응답에 event_type(항상 포함), reason(false_alarm 일 때만) 받고 싶음. 확인 결과 event_type 은 모델에는 있는데 EventResponse 에 빠져 있었고, reason 은 컬럼 자체가 없었음(POST /false-alarm 받아만 두고 DB 미저장, broadcast 만 함). 또 frontend(이지현 PR #15) 가 status='false_alarm' 을 기대하는데 backend 는 'dismissed' 로 박고 있어 표시도 깨지는 상태였음. 변경: - models.Event: reason String NULL 컬럼 추가, status/event_type 주석 정정 - schemas.EventResponse: event_type(str), reason(Optional[str]) 필드 추가 - schemas.EventCreate: event_type Optional 추가 (AI 분류 결과 동행) - schemas.EventStatusUpdate: Literal['confirmed','false_alarm'] (dismissed 제거) - api.report_false_alarm: status='false_alarm', event.reason=body.reason 저장 - api.create_event: model_dump(exclude_none=True) — 모델 default 적용 보장 - api.get_event_stats: false_alarm_count 쿼리 status='false_alarm' - alembic c1a2b3d4e5f6: events.reason 추가 + 기존 status='dismissed' 행을 'false_alarm' 으로 백필 (운영 DB 손상 없이 표준화) Co-Authored-By: Claude Opus 4.7 (1M context) --- ...3d4e5f6_add_reason_and_normalize_status.py | 31 +++++++++++++++++++ backend/app/api/events.py | 8 +++-- backend/app/models/models.py | 5 +-- backend/app/schemas/schemas.py | 5 ++- 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 backend/alembic/versions/c1a2b3d4e5f6_add_reason_and_normalize_status.py 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 ──────────────────────────────────────────────────────────────