- Add main application files (main.py, models.py, schemas.py, etc.) - Add routers for all features (waiting, attendance, members, etc.) - Add HTML templates for admin and user interfaces - Add migration scripts and utility files - Add Docker configuration - Add documentation files - Add .gitignore to exclude database and cache files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
698 lines
22 KiB
Python
698 lines
22 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from starlette.responses import StreamingResponse
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, desc, case, and_, or_
|
|
from datetime import datetime, date, timedelta
|
|
from typing import List, Optional
|
|
|
|
from database import get_db
|
|
from models import Franchise, Store, Member, WaitingList, DailyClosing, User
|
|
from auth import require_franchise_admin
|
|
from sse_manager import sse_manager, event_generator
|
|
|
|
router = APIRouter()
|
|
|
|
# Helper function for permission check
|
|
def check_franchise_permission(current_user: User, franchise_id: int, store_id: Optional[int] = None) -> Optional[List[int]]:
|
|
"""
|
|
권한 체크 및 접근 가능한 매장 ID 목록 반환
|
|
- system_admin: None 반환 (모든 매장 접근 가능)
|
|
- franchise_admin: None 반환 (해당 프랜차이즈 내 모든 매장 접근 가능)
|
|
- franchise_manager: 관리 매장 ID 목록 반환 (store_id가 있으면 검증 포함)
|
|
"""
|
|
if current_user.role == "system_admin":
|
|
return None
|
|
|
|
if current_user.role == "franchise_admin":
|
|
# Note: franchise_id is int, current_user.franchise_id is int
|
|
if int(current_user.franchise_id) != int(franchise_id):
|
|
raise HTTPException(status_code=403, detail="권한이 없습니다.")
|
|
return None
|
|
|
|
if current_user.role == "franchise_manager":
|
|
if int(current_user.franchise_id) != int(franchise_id):
|
|
raise HTTPException(status_code=403, detail="권한이 없습니다.")
|
|
|
|
managed_ids = [s.id for s in current_user.managed_stores]
|
|
|
|
if store_id:
|
|
if store_id not in managed_ids:
|
|
raise HTTPException(status_code=403, detail="해당 매장에 대한 접근 권한이 없습니다.")
|
|
return [store_id]
|
|
|
|
return managed_ids if managed_ids else []
|
|
|
|
raise HTTPException(status_code=403, detail="권한이 없습니다.")
|
|
|
|
@router.get("/{franchise_id}/sse/stream")
|
|
async def stream_franchise_events(
|
|
franchise_id: str,
|
|
request: Request,
|
|
current_user: User = Depends(require_franchise_admin)
|
|
):
|
|
"""
|
|
프랜차이즈 관리자용 SSE 스트림 연결
|
|
- 해당 프랜차이즈 내 모든 매장의 이벤트를 수신
|
|
"""
|
|
# 권한 체크
|
|
allowed_ids = check_franchise_permission(current_user, int(franchise_id))
|
|
# Note: SSE manager currently connects to whole franchise.
|
|
# For manager, ideally we filter events. But SSE manager might not support partial subscription yet.
|
|
# Allowing connection for now, assuming frontend filters or backend broadcasts everything.
|
|
# If security critical, SSE logic needs update.
|
|
# For now, just basic auth check via helper (though helper returns list, we ignore it here slightly risking logic)
|
|
# Actually, if allowed_ids is list, it means Restricted.
|
|
# Implementing restricted SSE is complex.
|
|
|
|
queue = await sse_manager.connect_franchise(franchise_id)
|
|
|
|
return StreamingResponse(
|
|
event_generator(queue),
|
|
media_type="text/event-stream"
|
|
)
|
|
|
|
@router.get("/{franchise_id}/dashboard")
|
|
async def get_dashboard_stats(
|
|
franchise_id: int,
|
|
start_date: date,
|
|
end_date: date,
|
|
store_id: Optional[int] = None,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
대시보드 통계 조회
|
|
- 총 대기, 현 대기, 총 출석 (전체/기존/신규)
|
|
"""
|
|
allowed_store_ids = check_franchise_permission(current_user, franchise_id, store_id)
|
|
|
|
today = date.today()
|
|
|
|
# 1. 기본 쿼리 구성 (Store JOIN)
|
|
# Store 테이블과 조인하여 프랜차이즈 및 활성 상태 필터링
|
|
base_query = db.query(WaitingList).join(
|
|
Store, WaitingList.store_id == Store.id
|
|
).filter(
|
|
Store.franchise_id == franchise_id,
|
|
Store.is_active == True
|
|
)
|
|
|
|
if store_id:
|
|
base_query = base_query.filter(WaitingList.store_id == store_id)
|
|
|
|
if allowed_store_ids is not None:
|
|
base_query = base_query.filter(Store.id.in_(allowed_store_ids))
|
|
|
|
# 2. 헬퍼 함수: 통계 계산
|
|
def calculate_stats(query, date_condition, is_current_waiting=False):
|
|
# 날짜 조건 적용
|
|
filtered_query = query.filter(date_condition)
|
|
|
|
# 전체 카운트
|
|
total = filtered_query.count()
|
|
|
|
# 기존 회원 카운트 (기간 시작일 이전에 가입한 회원)
|
|
# 현 대기의 경우 '오늘' 기준이므로, 오늘 이전에 가입한 회원을 기존 회원으로 간주
|
|
threshold_date = today if is_current_waiting else start_date
|
|
|
|
existing = filtered_query.join(
|
|
Member, WaitingList.member_id == Member.id
|
|
).filter(
|
|
Member.created_at < datetime.combine(threshold_date, datetime.min.time())
|
|
).count()
|
|
|
|
# 신규 (전체 - 기존)
|
|
new = total - existing
|
|
|
|
return {"total": total, "existing": existing, "new": new}
|
|
|
|
# 3. 총 대기 (선택된 기간 내 모든 대기 접수)
|
|
total_waiting_stats = calculate_stats(
|
|
base_query,
|
|
and_(
|
|
WaitingList.business_date >= start_date,
|
|
WaitingList.business_date <= end_date
|
|
)
|
|
)
|
|
|
|
# 4. 현 대기 (오늘 현재 대기 중인 인원)
|
|
# 기간 필터와 무관하게 '오늘' 기준, status='waiting'
|
|
current_waiting_query = base_query.filter(WaitingList.status == "waiting")
|
|
current_waiting_stats = calculate_stats(
|
|
current_waiting_query,
|
|
WaitingList.business_date == today,
|
|
is_current_waiting=True
|
|
)
|
|
|
|
# 5. 총 출석 (선택된 기간 내 출석 완료)
|
|
attendance_query = base_query.filter(WaitingList.status == "attended")
|
|
attendance_stats = calculate_stats(
|
|
attendance_query,
|
|
and_(
|
|
WaitingList.attended_at >= datetime.combine(start_date, datetime.min.time()),
|
|
WaitingList.attended_at <= datetime.combine(end_date, datetime.max.time())
|
|
)
|
|
)
|
|
|
|
# 6. 매장별 상세 현황 (Store Comparison)
|
|
# Query all stores in the franchise with their waiting and attendance counts
|
|
store_stats_query = db.query(
|
|
Store.id,
|
|
Store.name,
|
|
Store.is_active,
|
|
func.count(
|
|
case(
|
|
(and_(
|
|
WaitingList.business_date >= start_date,
|
|
WaitingList.business_date <= end_date
|
|
), WaitingList.id),
|
|
else_=None
|
|
)
|
|
).label("waiting_count"),
|
|
func.count(
|
|
case(
|
|
(and_(
|
|
WaitingList.status == "attended",
|
|
WaitingList.attended_at >= datetime.combine(start_date, datetime.min.time()),
|
|
WaitingList.attended_at <= datetime.combine(end_date, datetime.max.time())
|
|
), WaitingList.id),
|
|
else_=None
|
|
)
|
|
).label("attendance_count")
|
|
).outerjoin(
|
|
WaitingList,
|
|
Store.id == WaitingList.store_id
|
|
).filter(
|
|
Store.franchise_id == franchise_id,
|
|
Store.is_active == True
|
|
)
|
|
|
|
if store_id:
|
|
store_stats_query = store_stats_query.filter(Store.id == store_id)
|
|
|
|
if allowed_store_ids is not None:
|
|
store_stats_query = store_stats_query.filter(Store.id.in_(allowed_store_ids))
|
|
|
|
store_stats_results = store_stats_query.group_by(
|
|
Store.id, Store.name, Store.is_active
|
|
).order_by(
|
|
Store.name
|
|
).all()
|
|
|
|
store_stats = [
|
|
{
|
|
"store_id": r.id,
|
|
"store_name": r.name,
|
|
"is_active": r.is_active,
|
|
"waiting_count": r.waiting_count,
|
|
"attendance_count": r.attendance_count
|
|
}
|
|
for r in store_stats_results
|
|
]
|
|
|
|
return {
|
|
"total_waiting": total_waiting_stats,
|
|
"current_waiting": current_waiting_stats,
|
|
"total_attendance": attendance_stats,
|
|
"store_stats": store_stats
|
|
}
|
|
|
|
@router.get("/{franchise_id}/attendance/list")
|
|
async def get_attendance_list(
|
|
franchise_id: int,
|
|
start_date: date,
|
|
end_date: date,
|
|
store_id: Optional[int] = None,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
출석 목록 상세 조회 (전체 매장 또는 특정 매장)
|
|
- 기간 내 출석 완료된 목록
|
|
"""
|
|
allowed_store_ids = check_franchise_permission(current_user, franchise_id, store_id)
|
|
|
|
# 기본 쿼리: WaitingList와 Store, Member 조인
|
|
query = db.query(
|
|
WaitingList.id,
|
|
WaitingList.phone,
|
|
WaitingList.attended_at,
|
|
WaitingList.status,
|
|
Store.name.label("store_name"),
|
|
Member.name.label("member_name"),
|
|
Member.id.label("member_id")
|
|
).join(
|
|
Store, WaitingList.store_id == Store.id
|
|
).outerjoin(
|
|
Member, WaitingList.member_id == Member.id
|
|
).filter(
|
|
Store.franchise_id == franchise_id,
|
|
Store.is_active == True,
|
|
WaitingList.status == 'attended',
|
|
WaitingList.attended_at >= datetime.combine(start_date, datetime.min.time()),
|
|
WaitingList.attended_at <= datetime.combine(end_date, datetime.max.time())
|
|
)
|
|
|
|
if store_id:
|
|
query = query.filter(WaitingList.store_id == store_id)
|
|
|
|
if allowed_store_ids is not None:
|
|
query = query.filter(Store.id.in_(allowed_store_ids))
|
|
|
|
results = query.order_by(
|
|
desc(WaitingList.attended_at)
|
|
).all()
|
|
|
|
return [
|
|
{
|
|
"id": r.id,
|
|
"phone": r.phone,
|
|
"attended_at": r.attended_at,
|
|
"status": r.status,
|
|
"store_name": r.store_name,
|
|
"member_name": r.member_name or "비회원",
|
|
"member_id": r.member_id
|
|
}
|
|
for r in results
|
|
]
|
|
|
|
@router.get("/{franchise_id}/attendance/ranking")
|
|
async def get_attendance_ranking(
|
|
franchise_id: int,
|
|
start_date: date,
|
|
end_date: date,
|
|
store_id: Optional[int] = None,
|
|
limit: int = 10,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
회원 출석 순위 조회
|
|
- 기간별, 매장별(옵션) 출석이 많은 순으로 조회
|
|
"""
|
|
allowed_store_ids = check_franchise_permission(current_user, franchise_id, store_id)
|
|
|
|
# 기본 쿼리: WaitingList와 Member, Store 조인
|
|
query = db.query(
|
|
Member.id,
|
|
Member.name,
|
|
Member.phone,
|
|
Store.name.label("store_name"),
|
|
func.count(WaitingList.id).label("attendance_count"),
|
|
func.max(WaitingList.attended_at).label("last_attended_at")
|
|
).join(
|
|
WaitingList, Member.id == WaitingList.member_id
|
|
).join(
|
|
Store, WaitingList.store_id == Store.id
|
|
).filter(
|
|
WaitingList.status == "attended",
|
|
WaitingList.attended_at >= datetime.combine(start_date, datetime.min.time()),
|
|
WaitingList.attended_at <= datetime.combine(end_date, datetime.max.time())
|
|
)
|
|
|
|
# 매장 필터링
|
|
if store_id:
|
|
query = query.filter(WaitingList.store_id == store_id)
|
|
else:
|
|
# 프랜차이즈 내 모든 매장 (또는 허용된 매장)
|
|
if allowed_store_ids is not None:
|
|
query = query.filter(WaitingList.store_id.in_(allowed_store_ids))
|
|
else:
|
|
store_ids = db.query(Store.id).filter(Store.franchise_id == franchise_id).all()
|
|
store_ids = [s[0] for s in store_ids]
|
|
query = query.filter(WaitingList.store_id.in_(store_ids))
|
|
|
|
# 그룹화 및 정렬
|
|
results = query.group_by(
|
|
Member.id, Member.name, Member.phone, Store.name
|
|
).order_by(
|
|
desc("attendance_count")
|
|
).limit(limit).all()
|
|
|
|
return [
|
|
{
|
|
"member_id": r.id,
|
|
"name": r.name,
|
|
"phone": r.phone,
|
|
"store_name": r.store_name,
|
|
"attendance_count": r.attendance_count,
|
|
"last_attended_at": r.last_attended_at
|
|
}
|
|
for r in results
|
|
]
|
|
|
|
@router.get("/{franchise_id}/attendance/trends")
|
|
async def get_attendance_trends(
|
|
franchise_id: int,
|
|
start_date: date,
|
|
end_date: date,
|
|
period: str = Query("day", enum=["day", "month", "week"]),
|
|
store_id: Optional[int] = None,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
출석 추세 조회 (일별/주별/월별)
|
|
"""
|
|
# 권한 체크: system_admin은 모든 프랜차이즈 접근 가능, franchise_admin은 자신의 프랜차이즈만
|
|
if current_user.role == "franchise_admin" and current_user.franchise_id != franchise_id:
|
|
raise HTTPException(status_code=403, detail="권한이 없습니다.")
|
|
|
|
# 날짜 포맷 설정 (SQLite 기준)
|
|
if period == "month":
|
|
date_format = "%Y-%m"
|
|
elif period == "week":
|
|
date_format = "%Y-%W"
|
|
else:
|
|
date_format = "%Y-%m-%d"
|
|
|
|
# 쿼리 구성
|
|
query = db.query(
|
|
func.strftime(date_format, WaitingList.attended_at).label("period"),
|
|
func.count(WaitingList.id).label("count")
|
|
).filter(
|
|
WaitingList.status == "attended",
|
|
WaitingList.attended_at >= datetime.combine(start_date, datetime.min.time()),
|
|
WaitingList.attended_at <= datetime.combine(end_date, datetime.max.time())
|
|
)
|
|
|
|
# 매장 필터링
|
|
if store_id:
|
|
query = query.filter(WaitingList.store_id == store_id)
|
|
else:
|
|
store_ids = db.query(Store.id).filter(Store.franchise_id == franchise_id).all()
|
|
store_ids = [s[0] for s in store_ids]
|
|
query = query.filter(WaitingList.store_id.in_(store_ids))
|
|
|
|
# 그룹화 및 정렬
|
|
results = query.group_by("period").order_by("period").all()
|
|
|
|
return [
|
|
{
|
|
"period": r.period,
|
|
"count": r.count
|
|
}
|
|
for r in results
|
|
]
|
|
|
|
@router.get("/{franchise_id}/members/{member_id}/history")
|
|
async def get_member_history(
|
|
franchise_id: int,
|
|
member_id: int,
|
|
start_date: date,
|
|
end_date: date,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
특정 회원의 출석 이력 조회
|
|
"""
|
|
# 권한 체크: system_admin은 모든 프랜차이즈 접근 가능, franchise_admin은 자신의 프랜차이즈만
|
|
if current_user.role == "franchise_admin" and current_user.franchise_id != franchise_id:
|
|
raise HTTPException(status_code=403, detail="권한이 없습니다.")
|
|
|
|
# 회원 존재 확인 및 프랜차이즈 소속 확인 (간소화: 멤버 ID로 조회)
|
|
member = db.query(Member).filter(Member.id == member_id).first()
|
|
if not member:
|
|
raise HTTPException(status_code=404, detail="회원을 찾을 수 없습니다.")
|
|
|
|
# 출석 이력 조회
|
|
history = db.query(
|
|
WaitingList.attended_at,
|
|
Store.name.label("store_name"),
|
|
WaitingList.status
|
|
).join(
|
|
Store, WaitingList.store_id == Store.id
|
|
).filter(
|
|
WaitingList.member_id == member_id,
|
|
WaitingList.status == "attended",
|
|
WaitingList.attended_at >= datetime.combine(start_date, datetime.min.time()),
|
|
WaitingList.attended_at <= datetime.combine(end_date, datetime.max.time())
|
|
).order_by(
|
|
desc(WaitingList.attended_at)
|
|
).all()
|
|
|
|
return [
|
|
{
|
|
"attended_at": r.attended_at,
|
|
"store_name": r.store_name,
|
|
"status": r.status
|
|
}
|
|
for r in history
|
|
]
|
|
|
|
@router.get("/{franchise_id}/store_comparison")
|
|
async def get_store_comparison(
|
|
franchise_id: int,
|
|
start_date: date,
|
|
end_date: date,
|
|
store_id: Optional[int] = None,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
매장별 출석 비교 조회
|
|
"""
|
|
allowed_store_ids = check_franchise_permission(current_user, franchise_id, store_id)
|
|
|
|
# 프랜차이즈 정보 조회 (이름 제거용)
|
|
franchise = db.query(Franchise).filter(Franchise.id == franchise_id).first()
|
|
franchise_name = franchise.name if franchise else ""
|
|
|
|
# LEFT JOIN을 사용하여 출석 기록이 없는 매장도 포함
|
|
# business_date 기준으로 기간 필터링
|
|
query = db.query(
|
|
Store.id,
|
|
Store.name,
|
|
func.count(WaitingList.id).label("waiting_count"),
|
|
func.count(
|
|
case(
|
|
(WaitingList.status == "attended", WaitingList.id),
|
|
else_=None
|
|
)
|
|
).label("attendance_count")
|
|
).outerjoin(
|
|
WaitingList,
|
|
(Store.id == WaitingList.store_id) &
|
|
(WaitingList.business_date >= start_date) &
|
|
(WaitingList.business_date <= end_date)
|
|
).filter(
|
|
Store.franchise_id == franchise_id,
|
|
Store.is_active == True
|
|
)
|
|
|
|
# 특정 매장 필터링
|
|
if store_id:
|
|
query = query.filter(Store.id == store_id)
|
|
|
|
if allowed_store_ids is not None:
|
|
query = query.filter(Store.id.in_(allowed_store_ids))
|
|
|
|
results = query.group_by(
|
|
Store.id, Store.name
|
|
).order_by(
|
|
Store.name
|
|
).all()
|
|
|
|
return [
|
|
{
|
|
"store_id": r.id,
|
|
"store_name": r.name,
|
|
"waiting_count": r.waiting_count,
|
|
"attendance_count": r.attendance_count
|
|
}
|
|
for r in results
|
|
]
|
|
|
|
@router.get("/{franchise_id}/waiting/list")
|
|
async def get_waiting_list_details(
|
|
franchise_id: int,
|
|
start_date: Optional[date] = None,
|
|
end_date: Optional[date] = None,
|
|
store_id: Optional[int] = None,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
대기 목록 상세 조회 (전체 매장 또는 특정 매장)
|
|
- start_date, end_date가 없으면 오늘 날짜 기준
|
|
- 있으면 해당 기간의 대기 목록 조회
|
|
"""
|
|
allowed_store_ids = check_franchise_permission(current_user, franchise_id, store_id)
|
|
|
|
# 날짜 기본값 설정
|
|
today = date.today()
|
|
if not start_date:
|
|
start_date = today
|
|
if not end_date:
|
|
end_date = today
|
|
|
|
query = db.query(
|
|
WaitingList.id,
|
|
WaitingList.waiting_number,
|
|
WaitingList.phone,
|
|
WaitingList.created_at,
|
|
WaitingList.business_date,
|
|
WaitingList.status,
|
|
Store.name.label("store_name"),
|
|
Member.name.label("member_name"),
|
|
Member.id.label("member_id"),
|
|
Member.created_at.label("member_created_at")
|
|
).join(
|
|
Store, WaitingList.store_id == Store.id
|
|
).outerjoin( # 비회원 대기도 있을 수 있으므로 outerjoin
|
|
Member, WaitingList.member_id == Member.id
|
|
).filter(
|
|
Store.franchise_id == franchise_id,
|
|
Store.is_active == True,
|
|
WaitingList.business_date >= start_date,
|
|
WaitingList.business_date <= end_date
|
|
)
|
|
|
|
if store_id:
|
|
query = query.filter(WaitingList.store_id == store_id)
|
|
|
|
if allowed_store_ids is not None:
|
|
query = query.filter(Store.id.in_(allowed_store_ids))
|
|
|
|
results = query.order_by(
|
|
WaitingList.created_at
|
|
).all()
|
|
|
|
return [
|
|
{
|
|
"id": r.id,
|
|
"waiting_number": r.waiting_number,
|
|
"phone": r.phone,
|
|
"party_size": 1, # DB에 컬럼이 없어서 기본값 1로 고정
|
|
"created_at": r.created_at,
|
|
"business_date": r.business_date,
|
|
"status": r.status,
|
|
"store_name": r.store_name,
|
|
"member_name": r.member_name or "비회원",
|
|
"member_id": r.member_id,
|
|
"member_created_at": r.member_created_at
|
|
}
|
|
for r in results
|
|
]
|
|
|
|
@router.get("/{franchise_id}/members/new")
|
|
async def get_new_members(
|
|
franchise_id: int,
|
|
start_date: date,
|
|
end_date: date,
|
|
store_id: Optional[int] = None,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
신규 회원 목록 조회
|
|
"""
|
|
allowed_store_ids = check_franchise_permission(current_user, franchise_id, store_id)
|
|
|
|
# 프랜차이즈 내 모든 매장 ID 조회
|
|
store_ids_query = db.query(Store.id).filter(
|
|
Store.franchise_id == franchise_id,
|
|
Store.is_active == True
|
|
)
|
|
|
|
if store_id:
|
|
store_ids_query = store_ids_query.filter(Store.id == store_id)
|
|
|
|
if allowed_store_ids is not None:
|
|
store_ids_query = store_ids_query.filter(Store.id.in_(allowed_store_ids))
|
|
|
|
store_ids = [s[0] for s in store_ids_query.all()]
|
|
|
|
if not store_ids:
|
|
return []
|
|
|
|
# 신규 회원 조회
|
|
query = db.query(
|
|
Member.id,
|
|
Member.name,
|
|
Member.phone,
|
|
Member.created_at,
|
|
Store.name.label("store_name")
|
|
).join(
|
|
Store, Member.store_id == Store.id
|
|
).filter(
|
|
Member.store_id.in_(store_ids),
|
|
Member.created_at >= datetime.combine(start_date, datetime.min.time()),
|
|
Member.created_at <= datetime.combine(end_date, datetime.max.time())
|
|
).order_by(
|
|
desc(Member.created_at)
|
|
)
|
|
|
|
results = query.all()
|
|
|
|
return [
|
|
{
|
|
"id": r.id,
|
|
"name": r.name,
|
|
"phone": r.phone,
|
|
"created_at": r.created_at,
|
|
"store_name": r.store_name
|
|
}
|
|
for r in results
|
|
]
|
|
|
|
@router.get("/{franchise_id}/members/search")
|
|
async def search_members(
|
|
franchise_id: int,
|
|
query: str,
|
|
current_user: User = Depends(require_franchise_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
회원 검색 (프랜차이즈 전체)
|
|
"""
|
|
# This also needs checking, though store_id param is not present.
|
|
# Helper without store_id returns all allowed stores.
|
|
allowed_store_ids = check_franchise_permission(current_user, franchise_id)
|
|
# If None, query all in franchise. If List, query only those.
|
|
|
|
if not query or len(query) < 2:
|
|
return [] # 검색어 너무 짧으면 빈 배열
|
|
|
|
# 프랜차이즈 내 모든 매장 ID 조회
|
|
store_ids_query = db.query(Store.id).filter(
|
|
Store.franchise_id == franchise_id
|
|
)
|
|
|
|
if allowed_store_ids is not None:
|
|
store_ids_query = store_ids_query.filter(Store.id.in_(allowed_store_ids))
|
|
|
|
store_ids = [s[0] for s in store_ids_query.all()]
|
|
|
|
if not store_ids:
|
|
return []
|
|
|
|
# 검색
|
|
results = db.query(
|
|
Member.id,
|
|
Member.name,
|
|
Member.phone,
|
|
Member.created_at,
|
|
Store.name.label("store_name")
|
|
).join(
|
|
Store, Member.store_id == Store.id
|
|
).filter(
|
|
Member.store_id.in_(store_ids),
|
|
or_(
|
|
Member.name.contains(query),
|
|
Member.phone.endswith(query)
|
|
)
|
|
).limit(20).all()
|
|
|
|
return [
|
|
{
|
|
"id": r.id,
|
|
"name": r.name,
|
|
"phone": r.phone,
|
|
"created_at": r.created_at,
|
|
"store_name": r.store_name
|
|
}
|
|
for r in results
|
|
]
|