From b389db69f0fe19c1897cc35b46f1c3570101c6b7 Mon Sep 17 00:00:00 2001 From: CHOSOOGEUN <241898164+CHOSOOGEUN@users.noreply.github.com> Date: Fri, 22 May 2026 14:18:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(events):=20GET=20/api/events=20=EC=97=90?= =?UTF-8?q?=20search=20=EC=BF=BC=EB=A6=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 팀원 요청 — 페이지네이션 적용된 클라이언트 측 필터가 아니라 전체에서 서버사이드 검색이 필요. frontend(EventsFilter) 의 검색 박스 placeholder 와 동일한 대상으로 백엔드에서도 매칭. 검색 대상: - EV-{id}, CAM-{id} 패턴 → Event.id / Event.camera_id 정확 매칭 - 숫자만 입력 → Event.id 매칭 - 부분 일치(ILIKE %s%): Camera.station_name, Camera.location, Event.event_type, Event.reason 다른 필터(status, type, date_from/to)와는 AND 결합. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/api/events.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/backend/app/api/events.py b/backend/app/api/events.py index 6311193..b5813a8 100644 --- a/backend/app/api/events.py +++ b/backend/app/api/events.py @@ -1,13 +1,14 @@ +import re from typing import Optional from datetime import datetime from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy import select +from sqlalchemy import select, or_ from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_admin from app.api.websocket import manager from app.database import get_db -from app.models.models import Event, Notification, Admin +from app.models.models import Event, Notification, Admin, Camera from app.schemas.schemas import EventCreate, EventResponse, EventStatusUpdate from app.workers.tasks import upload_clip_task from pydantic import BaseModel @@ -27,6 +28,7 @@ async def list_events( type: Optional[str] = None, date_from: Optional[datetime] = None, date_to: Optional[datetime] = None, + search: Optional[str] = None, limit: int = 50, offset: int = 0, db: AsyncSession = Depends(get_db), @@ -49,8 +51,8 @@ async def list_events( Returns: list[Event]: 조회된 이벤트 객체 리스트 """ - query = select(Event).order_by(Event.timestamp.desc()).offset(offset).limit(limit) - + query = select(Event) + if camera_id is not None: query = query.where(Event.camera_id == camera_id) if status: @@ -61,7 +63,29 @@ async def list_events( query = query.where(Event.timestamp >= date_from) if date_to: query = query.where(Event.timestamp <= date_to) - + + # 서버사이드 통합 검색: EV-번호 / CAM-번호 / 역이름 / 게이트(위치) / event_type / reason + if search and search.strip(): + s = search.strip() + pattern = f"%{s}%" + conds = [ + Camera.station_name.ilike(pattern), + Camera.location.ilike(pattern), + Event.event_type.ilike(pattern), + Event.reason.ilike(pattern), + ] + m = re.search(r"EV[-_]?(\d+)", s, re.IGNORECASE) + if m: + conds.append(Event.id == int(m.group(1))) + m = re.search(r"CAM[-_]?(\d+)", s, re.IGNORECASE) + if m: + conds.append(Event.camera_id == int(m.group(1))) + if s.isdigit(): + conds.append(Event.id == int(s)) + query = query.join(Camera, Event.camera_id == Camera.id).where(or_(*conds)) + + query = query.order_by(Event.timestamp.desc()).offset(offset).limit(limit) + result = await db.execute(query) return result.scalars().all()