Files
waiting-system/routers/waiting.py
Jun-dev f699a29a85 Add waiting system application files
- 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>
2025-12-14 00:29:39 +09:00

803 lines
29 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from core.logger import logger
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import func, and_
from datetime import datetime, date
from typing import List, Optional, Dict
import json
from database import get_db
from models import WaitingList, ClassInfo, Member, DailyClosing, ClassClosure, Store, StoreSettings
from auth import get_current_store
from schemas import (
WaitingListCreate,
WaitingListResponse,
WaitingList as WaitingListSchema,
WaitingListDetail
)
from sse_manager import sse_manager
from utils import get_today_date
router = APIRouter()
def get_current_business_date(db: Session, store_id: int) -> date:
"""
현재 영업일 조회 (Sync with daily_closing.py)
1. 활성화된 영업일 우선
2. 없으면 시간 기반 계산
"""
# 1. 활성화된 영업일 확인
active_closing = db.query(DailyClosing).filter(
DailyClosing.store_id == store_id,
DailyClosing.is_closed == False
).order_by(DailyClosing.business_date.desc()).first()
if active_closing:
return active_closing.business_date
# 2. 설정 기반 계산
settings = db.query(StoreSettings).filter(StoreSettings.store_id == store_id).first()
start_hour = settings.business_day_start if settings else 5
return get_today_date(start_hour)
# 요일 매핑
WEEKDAY_MAP = {
0: "mon", 1: "tue", 2: "wed", 3: "thu",
4: "fri", 5: "sat", 6: "sun"
}
DEFAULT_WEEKDAY_SCHEDULE = {
"mon": True, "tue": True, "wed": True, "thu": True,
"fri": True, "sat": True, "sun": True
}
def parse_weekday_schedule(schedule_str: str) -> Dict[str, bool]:
"""JSON 문자열을 weekday_schedule 딕셔너리로 안전하게 변환"""
if not schedule_str:
return DEFAULT_WEEKDAY_SCHEDULE.copy()
try:
schedule = json.loads(schedule_str)
if not isinstance(schedule, dict):
return DEFAULT_WEEKDAY_SCHEDULE.copy()
return schedule
except (json.JSONDecodeError, TypeError, ValueError):
return DEFAULT_WEEKDAY_SCHEDULE.copy()
def filter_classes_by_weekday(classes: List[ClassInfo], target_date: date) -> List[ClassInfo]:
"""특정 날짜의 요일에 맞는 클래스만 필터링"""
weekday = WEEKDAY_MAP[target_date.weekday()]
filtered_classes = []
for cls in classes:
schedule = parse_weekday_schedule(cls.weekday_schedule)
if schedule.get(weekday, True):
filtered_classes.append(cls)
return filtered_classes
def get_next_waiting_number(db: Session, business_date: date, store_id: int) -> int:
"""다음 대기번호 생성"""
max_number = db.query(func.max(WaitingList.waiting_number)).filter(
WaitingList.business_date == business_date,
WaitingList.store_id == store_id
).scalar()
return (max_number or 0) + 1
def get_available_class(db: Session, business_date: date, store_id: int):
"""배치 가능한 클래스 찾기 - 순차적으로 다음 클래스에 배치 (마감된 교시 제외)"""
classes_raw = db.query(ClassInfo).filter(
ClassInfo.is_active == True,
ClassInfo.store_id == store_id
).order_by(ClassInfo.class_number).all()
# 헬퍼 함수를 사용하여 오늘 요일에 맞는 클래스만 필터링
classes = filter_classes_by_weekday(classes_raw, business_date)
if not classes:
raise HTTPException(status_code=400, detail="오늘 운영하는 클래스가 없습니다.")
# 마감된 교시 ID 목록 조회
closed_class_ids = db.query(ClassClosure.class_id).filter(
ClassClosure.business_date == business_date,
ClassClosure.store_id == store_id
).all()
closed_class_ids = set(c.class_id for c in closed_class_ids)
# 마감되지 않은 교시만 필터링
available_classes = [c for c in classes if c.id not in closed_class_ids]
if not available_classes:
raise HTTPException(status_code=400, detail="모든 교시가 마감되었습니다. 대기 접수를 받을 수 없습니다.")
# 순차적 배정 로직 개선: 1교시부터 차레대로 빈 자리 확인
logger.debug(f"[ClassAssign] Finding slot for Store {store_id} on {business_date}")
# 순차적 배정 로직 개선: 1교시부터 차레대로 빈 자리 확인
# "마지막 등록자" 기준이 아니라 "빈 자리" 기준으로 변경하여 중간에 빈 교시가 있으면 채워넣도록 함
for cls in available_classes:
# 해당 클래스의 총 정원 점유율 계산 (Waiting + Called + Attended)
total_occupancy = db.query(func.count(WaitingList.id)).filter(
WaitingList.class_id == cls.id,
WaitingList.business_date == business_date,
WaitingList.status.in_(["waiting", "called", "attended"]),
WaitingList.store_id == store_id
).scalar()
logger.debug(f"[ClassAssign] Checking {cls.class_name} (ID: {cls.id}): {total_occupancy}/{cls.max_capacity}")
if total_occupancy < cls.max_capacity:
logger.info(f"[ClassAssign] Assigned {cls.class_name} (ID: {cls.id}). Occupancy before: {total_occupancy}")
return cls, total_occupancy + 1
# 모든 교시가 꽉 찬 경우
logger.warning("[ClassAssign] All classes are full.")
raise HTTPException(status_code=400, detail="모든 교시의 정원이 마감되었습니다.")
@router.post("/register", response_model=WaitingListResponse)
async def register_waiting(
waiting: WaitingListCreate,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
대기 접수
- 핸드폰번호로 접수
- 회원인 경우 자동으로 이름 매칭
- 자동으로 클래스 배치
"""
today = get_current_business_date(db, current_store.id)
# 영업 중인지 확인
business = db.query(DailyClosing).filter(
DailyClosing.business_date == today,
DailyClosing.is_closed == False,
DailyClosing.store_id == current_store.id
).first()
if not business:
raise HTTPException(status_code=400, detail="영업 중이 아닙니다. 개점을 먼저 진행해주세요.")
# 이미 대기 중인지 확인
existing = db.query(WaitingList).filter(
WaitingList.business_date == today,
WaitingList.phone == waiting.phone,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).first()
if existing:
raise HTTPException(status_code=400, detail="이미 대기 중인 번호입니다.\n핸드폰번호를 다시 확인하여 주세요.")
# 매장 설정 조회
from models import StoreSettings
settings = db.query(StoreSettings).filter(StoreSettings.store_id == current_store.id).first()
# 1. 최대 대기 인원 제한 체크 (use_max_waiting_limit가 활성화된 경우에만)
if settings and settings.use_max_waiting_limit and settings.max_waiting_limit > 0:
# 현재 대기 중인 총 인원 확인
current_waiting_count = db.query(func.count(WaitingList.id)).filter(
WaitingList.business_date == today,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).scalar()
if current_waiting_count >= settings.max_waiting_limit:
raise HTTPException(
status_code=400,
detail=f"대기 인원이 가득 찼습니다. (최대 {settings.max_waiting_limit}명)"
)
# 2. 마지막 교시 정원 초과 차단 체크
if settings and settings.block_last_class_registration:
# 오늘 운영되는 클래스 조회
classes_raw = db.query(ClassInfo).filter(
ClassInfo.is_active == True,
ClassInfo.store_id == current_store.id
).order_by(ClassInfo.class_number).all()
# 오늘 요일에 맞는 클래스만 필터링
classes = filter_classes_by_weekday(classes_raw, today)
if classes:
# 마지막 교시 찾기 (class_number가 가장 큰 것)
last_class = max(classes, key=lambda c: c.class_number)
# 마지막 교시의 현재 대기 인원 확인
# Current count must include waiting and attended users to respect total capacity
last_class_count = db.query(func.count(WaitingList.id)).filter(
WaitingList.class_id == last_class.id,
WaitingList.business_date == today,
WaitingList.status.in_(["waiting", "called", "attended"]),
WaitingList.store_id == current_store.id
).scalar()
# 정원 초과 시 차단
if last_class_count >= last_class.max_capacity:
raise HTTPException(
status_code=400,
detail="교시 접수가 마감되었습니다."
)
# 회원 정보 조회
member = db.query(Member).filter(
Member.phone == waiting.phone,
Member.store_id == current_store.id
).first()
is_new_member = (member is None)
# 자동 회원가입 로직
if not member and settings and settings.auto_register_member:
# 이름이 없는 경우 핸드폰 번호 뒷자리 사용
member_name = waiting.name if waiting.name else waiting.phone[-4:]
new_member = Member(
store_id=current_store.id,
name=member_name,
phone=waiting.phone,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(new_member)
db.flush() # ID 생성을 위해 flush
member = new_member
print(f"자동 회원가입 완료: {member.name} ({member.phone})")
member_id = member.id if member else None
name = member.name if member else waiting.name
# 다음 대기번호 생성
waiting_number = get_next_waiting_number(db, today, current_store.id)
# 배치 가능한 클래스 찾기
all_classes_raw = db.query(ClassInfo).filter(
ClassInfo.store_id == current_store.id,
ClassInfo.is_active == True
).order_by(ClassInfo.class_number).all()
# 오늘 요일에 운영되는 클래스만 필터링
all_classes = filter_classes_by_weekday(all_classes_raw, today)
if not all_classes:
raise HTTPException(status_code=400, detail="오늘 운영하는 교시가 없습니다.")
# 마감된 교시 목록 조회
closed_class_ids = [
cc.class_id for cc in db.query(ClassClosure).filter(
ClassClosure.store_id == current_store.id,
ClassClosure.business_date == today
).all()
]
# 시작 교시 인덱스 결정 및 유효성 검증
start_index = 0
if waiting.class_id:
# 요청된 class_id가 실제로 오늘 운영되는 교시 목록에 있는지 확인
class_found = False
for i, cls in enumerate(all_classes):
if cls.id == waiting.class_id:
start_index = i
class_found = True
print(f"[REGISTER] Requested class_id={waiting.class_id} found at index {i}")
break
if not class_found:
# 요청된 교시가 없으면 경고 로그 출력하고 자동 배치로 전환
print(f"[WARNING] Requested class_id={waiting.class_id} not found in active classes for today. Available class IDs: {[c.id for c in all_classes]}")
print(f"[REGISTER] Falling back to automatic class assignment")
start_index = 0
# 순차 탐색 (Overflow Logic)
target_class = None
class_order = 0
for i in range(start_index, len(all_classes)):
cls = all_classes[i]
# 1. 마감 여부 체크
if cls.id in closed_class_ids:
print(f"[REGISTER] Class {cls.id} ({cls.class_name}) is closed, skipping")
continue
# 2. 정원 체크 (대기 + 호출 + 출석 모두 포함)
current_count = db.query(func.count(WaitingList.id)).filter(
WaitingList.class_id == cls.id,
WaitingList.business_date == today,
WaitingList.status.in_(["waiting", "called", "attended"]),
WaitingList.store_id == current_store.id
).scalar()
print(f"[REGISTER] Class {cls.id} ({cls.class_name}): {current_count}/{cls.max_capacity}")
if current_count < cls.max_capacity:
target_class = cls
class_order = current_count + 1
print(f"[REGISTER] Assigned to class {cls.id} ({cls.class_name}) as order {class_order}")
break
if not target_class:
# 모든 교시가 마감되었거나 정원 초과
print(f"[REGISTER ERROR] No available class found. Requested class_id={waiting.class_id}, Available classes: {len(all_classes)}, Closed: {len(closed_class_ids)}")
if waiting.class_id:
raise HTTPException(status_code=400, detail="선택한 교시 및 이후 모든 교시가 마감되었거나 정원이 초과되었습니다.")
else:
raise HTTPException(status_code=400, detail="등록 가능한 교시가 없습니다 (모두 마감 또는 정원 초과).")
# 대기자 등록
new_waiting = WaitingList(
business_date=today,
waiting_number=waiting_number,
phone=waiting.phone,
name=name,
class_id=target_class.id,
class_order=class_order,
member_id=member_id,
status="waiting",
registered_at=datetime.now(),
store_id=current_store.id
)
db.add(new_waiting)
db.commit()
db.refresh(new_waiting)
# SSE 브로드캐스트: 새로운 대기자 등록 알림
print(f"Broadcasting new_user event: store_id={current_store.id}, franchise_id={current_store.franchise_id}")
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="new_user",
data={
"id": new_waiting.id,
"waiting_id": new_waiting.id, # 프론트엔드 호환성을 위해 추가
"waiting_number": waiting_number,
"class_id": target_class.id,
"class_name": target_class.class_name,
"class_order": class_order,
"name": name,
"phone": waiting.phone,
"display_name": name if name else waiting.phone[-4:]
},
franchise_id=str(current_store.franchise_id)
)
# 응답 메시지 생성
message = f"대기번호: {waiting_number}\n{target_class.class_name} {class_order}번째\n대기 등록이 완료되었습니다."
return WaitingListResponse(
id=new_waiting.id,
waiting_number=waiting_number,
class_id=target_class.id,
class_name=target_class.class_name,
class_order=class_order,
phone=waiting.phone,
name=name,
status="waiting",
registered_at=new_waiting.registered_at,
message=message,
is_new_member=is_new_member
)
@router.post("/", response_model=WaitingListResponse)
async def create_waiting(
waiting: WaitingListCreate,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
대기 접수 (Alias for /register)
- dashboard 등에서 호출 표준화
"""
return await register_waiting(waiting, db, current_store)
@router.get("/next-slot")
async def get_next_slot(
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
다음 대기 등록 시 배정될 예정인 교시 조회 (Reception Desk용 Single Source of Truth)
"""
today = get_current_business_date(db, current_store.id)
# 총 대기 인원 (waiting only) for overall status
total_waiting = db.query(func.count(WaitingList.id)).filter(
WaitingList.business_date == today,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).scalar()
# 1. Available Classes (Same logic as register_waiting)
all_classes_raw = db.query(ClassInfo).filter(
ClassInfo.store_id == current_store.id,
ClassInfo.is_active == True
).order_by(ClassInfo.class_number).all()
classes = filter_classes_by_weekday(all_classes_raw, today)
if not classes:
return {
"class_id": -1,
"class_name": "운영 교시 없음",
"class_order": 0,
"max_capacity": 0,
"is_full": True,
"total_waiting": total_waiting
}
# 2. Closed Classes
closed_ids = [
cc.class_id for cc in db.query(ClassClosure).filter(
ClassClosure.store_id == current_store.id,
ClassClosure.business_date == today
).all()
]
# 3. Find First Available Slot (Sequential)
next_class = None
next_order = 0
is_fully_booked = True
for cls in classes:
if cls.id in closed_ids:
continue
# Get Occupancy (Waiting + Called + Attended)
total_occupancy = db.query(func.count(WaitingList.id)).filter(
WaitingList.class_id == cls.id,
WaitingList.business_date == today,
WaitingList.status.in_(["waiting", "called", "attended"]),
WaitingList.store_id == current_store.id
).scalar()
if total_occupancy < cls.max_capacity:
next_class = cls
next_order = total_occupancy + 1
is_fully_booked = False
break
if is_fully_booked:
return {
"class_id": -1,
"class_name": "접수 마감",
"class_order": 0,
"max_capacity": 0,
"is_full": True,
"total_waiting": total_waiting
}
return {
"class_id": next_class.id,
"class_name": next_class.class_name,
"class_order": next_order,
"max_capacity": next_class.max_capacity,
"is_full": False,
"total_waiting": total_waiting
}
@router.get("/check/{phone}")
async def check_waiting_status(
phone: str,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
대기 현황 조회 (모바일용)
- 핸드폰번호로 조회
"""
today = date.today()
waiting = db.query(WaitingList).filter(
WaitingList.business_date == today,
WaitingList.phone == phone,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).first()
if not waiting:
return {
"found": False,
"message": "대기 내역이 없습니다."
}
# 클래스 정보 조회
class_info = db.query(ClassInfo).filter(
ClassInfo.id == waiting.class_id,
ClassInfo.store_id == current_store.id
).first()
# 앞에 대기 중인 사람 수 계산
ahead_count = db.query(func.count(WaitingList.id)).filter(
WaitingList.business_date == today,
WaitingList.status == "waiting",
WaitingList.waiting_number < waiting.waiting_number,
WaitingList.store_id == current_store.id
).scalar()
return {
"found": True,
"waiting_number": waiting.waiting_number,
"class_name": class_info.class_name,
"class_order": waiting.class_order,
"ahead_count": ahead_count,
"registered_at": waiting.registered_at,
"message": f"대기번호 {waiting.waiting_number}\n{class_info.class_name} {waiting.class_order}번째\n앞에 {ahead_count}명 대기 중"
}
@router.get("/list")
async def get_waiting_list(
business_date: Optional[date] = None,
status: Optional[str] = None,
class_id: Optional[int] = None,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
대기자 목록 조회
- 날짜별, 상태별, 클래스별 필터링 가능
수동으로 응답 형식을 생성하여 weekday_schedule 파싱 문제 해결
"""
if not business_date:
business_date = get_current_business_date(db, current_store.id)
# class_info와 member를 eager load
query = db.query(WaitingList).options(
joinedload(WaitingList.class_info),
joinedload(WaitingList.member)
).filter(
WaitingList.business_date == business_date,
WaitingList.store_id == current_store.id
)
if status:
query = query.filter(WaitingList.status == status)
if class_id:
query = query.filter(WaitingList.class_id == class_id)
# 교시별로 정렬 (class_id 우선, 그 다음 교시 내 순서인 class_order)
waiting_list = query.order_by(
WaitingList.class_id,
WaitingList.class_order
).all()
# 최근 30일 출석 수 일괄 조회 (N+1 문제 방지)
member_ids = [w.member_id for w in waiting_list if w.member_id]
member_attendance_counts = {}
if member_ids:
from datetime import timedelta
# 출석 카운트 설정 조회
settings = db.query(StoreSettings).filter(StoreSettings.store_id == current_store.id).first()
count_type = settings.attendance_count_type if settings else 'days'
start_date = business_date
if count_type == 'monthly':
# 이번 달 1일 부터 조회
start_date = business_date.replace(day=1)
else:
# 최근 N일 (기본 30일)
lookback_days = settings.attendance_lookback_days if settings else 30
start_date = business_date - timedelta(days=lookback_days)
attendance_counts = db.query(
WaitingList.member_id,
func.count(WaitingList.id)
).filter(
WaitingList.member_id.in_(member_ids),
WaitingList.status == 'attended',
WaitingList.business_date >= start_date,
WaitingList.business_date <= business_date # 미래 날짜 제외
).group_by(WaitingList.member_id).all()
member_attendance_counts = {member_id: count for member_id, count in attendance_counts}
# 수동으로 dict 생성 (weekday_schedule 파싱 포함)
result = []
for waiting in waiting_list:
# class_info 변환
class_info_dict = {
"id": waiting.class_info.id,
"class_number": waiting.class_info.class_number,
"class_name": waiting.class_info.class_name,
"start_time": waiting.class_info.start_time,
"end_time": waiting.class_info.end_time,
"max_capacity": waiting.class_info.max_capacity,
"is_active": waiting.class_info.is_active,
"weekday_schedule": parse_weekday_schedule(waiting.class_info.weekday_schedule),
"class_type": waiting.class_info.class_type if hasattr(waiting.class_info, 'class_type') else 'all',
"created_at": waiting.class_info.created_at,
"updated_at": waiting.class_info.updated_at,
"current_count": 0 # 이 엔드포인트에서는 current_count 계산하지 않음
}
# member 변환 (있는 경우)
member_dict = None
if waiting.member:
member_dict = {
"id": waiting.member.id,
"name": waiting.member.name,
"phone": waiting.member.phone,
"created_at": waiting.member.created_at
}
# waiting 정보 + class_info + member
waiting_dict = {
"id": waiting.id,
"business_date": waiting.business_date,
"waiting_number": waiting.waiting_number,
"phone": waiting.phone,
"name": waiting.member.name if waiting.member and waiting.member.name else waiting.name,
"class_id": waiting.class_id,
"class_order": waiting.class_order,
"member_id": waiting.member_id,
"is_empty_seat": waiting.is_empty_seat,
"status": waiting.status,
"registered_at": waiting.registered_at,
"attended_at": waiting.attended_at,
"cancelled_at": waiting.cancelled_at,
"call_count": waiting.call_count,
"last_called_at": waiting.last_called_at,
"message": f"대기번호 {waiting.waiting_number}\n{waiting.class_info.class_name} {waiting.class_order}번째",
# 최근 30일 출석 수 (회원이 없는 경우 0)
"last_month_attendance_count": member_attendance_counts.get(waiting.member_id, 0),
"created_at": waiting.created_at,
"updated_at": waiting.updated_at,
"class_info": class_info_dict,
"member": member_dict
}
result.append(waiting_dict)
return result
@router.get("/list/by-class")
async def get_waiting_list_by_class(
business_date: Optional[date] = None,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
클래스별로 그룹화된 대기자 목록 조회
오늘 요일에 운영되는 클래스만 반환
"""
if not business_date:
business_date = get_current_business_date(db, current_store.id)
# 모든 활성 클래스 조회
classes_raw = db.query(ClassInfo).filter(
ClassInfo.is_active == True,
ClassInfo.store_id == current_store.id
).order_by(ClassInfo.class_number).all()
# 헬퍼 함수를 사용하여 오늘 요일에 맞는 클래스만 필터링
classes = filter_classes_by_weekday(classes_raw, business_date)
result = []
for cls in classes:
waiting_list = db.query(WaitingList).options(
joinedload(WaitingList.member)
).filter(
WaitingList.business_date == business_date,
WaitingList.class_id == cls.id,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).order_by(WaitingList.class_order).all()
# 현재 대기 중인 인원 수 (Display용)
current_count = len(waiting_list)
# 총 정원 계산용 (Waiting + Called + Attended)
total_registered_count = db.query(func.count(WaitingList.id)).filter(
WaitingList.class_id == cls.id,
WaitingList.business_date == business_date,
WaitingList.status.in_(["waiting", "called", "attended"]),
WaitingList.store_id == current_store.id
).scalar()
# Member 이름 우선 사용 로직
def get_display_name(w):
if w.member and w.member.name:
return w.member.name
return w.name if w.name else w.phone[-4:]
result.append({
"class_id": cls.id,
"class_name": cls.class_name,
"class_number": cls.class_number,
"start_time": cls.start_time.strftime("%H:%M"),
"end_time": cls.end_time.strftime("%H:%M"),
"max_capacity": cls.max_capacity,
"current_count": current_count,
"total_count": total_registered_count, # Predict Logic용
"waiting_list": [
{
"id": w.id,
"waiting_number": w.waiting_number,
"name": w.member.name if w.member and w.member.name else w.name,
"phone": w.phone,
"display_name": get_display_name(w),
"class_order": w.class_order,
"registered_at": w.registered_at,
"member_id": w.member_id
}
for w in waiting_list
]
})
return result
@router.get("/{waiting_id}", response_model=WaitingListResponse)
async def get_waiting_detail(
waiting_id: int,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
대기 상세 조회
"""
waiting = db.query(WaitingList).options(
joinedload(WaitingList.class_info),
joinedload(WaitingList.member)
).filter(
WaitingList.id == waiting_id,
WaitingList.store_id == current_store.id
).first()
if not waiting:
raise HTTPException(status_code=404, detail="대기 내역을 찾을 수 없습니다.")
# 응답 메시지 생성
message = f"대기번호: {waiting.waiting_number}\n{waiting.class_info.class_name} {waiting.class_order}번째\n대기 중입니다."
return WaitingListResponse(
id=waiting.id,
waiting_number=waiting.waiting_number,
class_id=waiting.class_id,
class_name=waiting.class_info.class_name,
class_order=waiting.class_order,
phone=waiting.phone,
name=waiting.member.name if waiting.member and waiting.member.name else waiting.name,
status=waiting.status,
registered_at=waiting.registered_at,
message=message
)
@router.delete("/{waiting_id}")
async def cancel_waiting(
waiting_id: int,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
대기 취소
- 대기자가 직접 취소하는 경우
"""
waiting = db.query(WaitingList).filter(
WaitingList.id == waiting_id,
WaitingList.store_id == current_store.id
).first()
if not waiting:
raise HTTPException(status_code=404, detail="대기 내역을 찾을 수 없습니다.")
if waiting.status != "waiting":
raise HTTPException(status_code=400, detail="이미 처리된 대기입니다.")
waiting.status = "cancelled"
waiting.cancelled_at = datetime.now()
db.commit()
return {"message": "대기가 취소되었습니다."}