Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/app/api/cameras.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ async def list_cameras(
"""
[통합 v1] 등록된 모든 카메라의 상태를 조회합니다. (보안 인증 필수)
"""
result = await db.execute(select(Camera))
# 토글/재조회 시 순서 흔들림 방지 — 항상 id 오름차순
result = await db.execute(select(Camera).order_by(Camera.id))
return result.scalars().all()


Expand Down
33 changes: 25 additions & 8 deletions backend/app/api/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ async def list_events(
camera_id: Optional[int] = None,
status: Optional[str] = None,
type: Optional[str] = None,
station: Optional[str] = None,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
search: Optional[str] = None,
Expand Down Expand Up @@ -64,25 +65,34 @@ async def list_events(
if date_to:
query = query.where(Event.timestamp <= date_to)

# Camera 조인은 station 필터나 search 가 들어올 때만 (중복 join 방지)
station_q = station.strip() if station else ""
search_q = search.strip() if search else ""
if station_q or search_q:
query = query.join(Camera, Event.camera_id == Camera.id)

# 역 이름 명시 필터
if station_q:
query = query.where(Camera.station_name.ilike(f"%{station_q}%"))

# 서버사이드 통합 검색: EV-번호 / CAM-번호 / 역이름 / 게이트(위치) / event_type / reason
if search and search.strip():
s = search.strip()
pattern = f"%{s}%"
if search_q:
pattern = f"%{search_q}%"
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)
m = re.search(r"EV[-_]?(\d+)", search_q, re.IGNORECASE)
if m:
conds.append(Event.id == int(m.group(1)))
m = re.search(r"CAM[-_]?(\d+)", s, re.IGNORECASE)
m = re.search(r"CAM[-_]?(\d+)", search_q, 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))
if search_q.isdigit():
conds.append(Event.id == int(search_q))
query = query.where(or_(*conds))

query = query.order_by(Event.timestamp.desc()).offset(offset).limit(limit)

Expand Down Expand Up @@ -145,6 +155,13 @@ async def create_event(
[GateGuard] AI 실시간 추론 결과로부터 무임승차 이벤트를 기록하고 관제 대시보드에 즉시 브로드캐스트합니다.
- AI 추론 엔진(inference.py)에서 호출됩니다.
"""
# 0. 카메라 검증 — 존재해야 하고 활성 상태여야 이벤트를 받음
camera = await db.scalar(select(Camera).where(Camera.id == body.camera_id))
if not camera:
raise HTTPException(status_code=404, detail=f"카메라 #{body.camera_id} 가 존재하지 않습니다.")
if not camera.is_active:
raise HTTPException(status_code=400, detail=f"카메라 #{body.camera_id} 가 비활성 상태입니다.")

# 1. DB에 사건 기록 (Persistence)
# exclude_none: 비전송 필드는 모델 default 가 적용되도록 (event_type → 'unknown')
event = Event(**body.model_dump(exclude_none=True))
Expand Down
Loading