Files
waiting-system/routers/waiting_board.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

860 lines
29 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import func, and_
from datetime import datetime, date
from typing import List, Dict
import json
from database import get_db
from models import WaitingList, ClassInfo, StoreSettings, DailyClosing, ClassClosure, Store
from auth import get_current_store
from schemas import (
WaitingStatusUpdate,
WaitingOrderUpdate,
WaitingClassUpdate,
BatchAttendance,
WaitingBoard,
WaitingBoardItem,
EmptySeatInsert
)
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", # Monday
1: "tue", # Tuesday
2: "wed", # Wednesday
3: "thu", # Thursday
4: "fri", # Friday
5: "sat", # Saturday
6: "sun" # Sunday
}
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]:
"""
특정 날짜의 요일에 맞는 클래스만 필터링
Args:
classes: 필터링할 클래스 목록
target_date: 기준 날짜
Returns:
해당 요일에 운영되는 클래스 목록
"""
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 convert_class_to_dict(cls: ClassInfo) -> dict:
"""
ClassInfo 모델 객체를 dict로 변환 (Pydantic validation용)
weekday_schedule을 JSON 문자열에서 dict로 파싱
Args:
cls: ClassInfo 모델 인스턴스
Returns:
dict: 변환된 딕셔너리
"""
return {
"id": cls.id,
"class_number": cls.class_number,
"class_name": cls.class_name,
"start_time": cls.start_time,
"end_time": cls.end_time,
"max_capacity": cls.max_capacity,
"is_active": cls.is_active,
"weekday_schedule": parse_weekday_schedule(cls.weekday_schedule),
"class_type": cls.class_type if hasattr(cls, 'class_type') else 'all',
"created_at": cls.created_at,
"updated_at": cls.updated_at
}
@router.get("/display", response_model=WaitingBoard)
async def get_waiting_board(
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
대기현황판 데이터 조회
- 매장 설정에 따라 표시할 클래스 개수 결정
- 대기자 목록을 클래스별로 정렬하여 반환
"""
today = get_current_business_date(db, current_store.id)
# 매장 설정 조회
settings = db.query(StoreSettings).filter(
StoreSettings.store_id == current_store.id
).first()
if not settings:
raise HTTPException(status_code=404, detail="매장 설정을 찾을 수 없습니다.")
# 영업 정보 조회
business = db.query(DailyClosing).filter(
DailyClosing.business_date == today,
DailyClosing.store_id == current_store.id
).first()
# 대기 중인 목록 조회 (먼저 조회)
waiting_list = db.query(WaitingList).options(
joinedload(WaitingList.member)
).filter(
WaitingList.business_date == today,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).order_by(WaitingList.class_id, WaitingList.class_order).all()
# 대기자가 있는 클래스 ID 목록
classes_with_waiting = set(w.class_id for w in waiting_list)
# 일괄 출석이 완료된 클래스 ID 목록 (출석한 사람은 있지만 대기자는 없는 클래스)
completed_classes = db.query(WaitingList.class_id).filter(
WaitingList.business_date == today,
WaitingList.status == "attended",
WaitingList.store_id == current_store.id
).distinct().all()
completed_class_ids = set(c.class_id for c in completed_classes if c.class_id not in classes_with_waiting)
# 마감된 클래스 ID 목록
closed_classes = db.query(ClassClosure.class_id).filter(
ClassClosure.business_date == today,
ClassClosure.store_id == current_store.id
).all()
closed_class_ids = set(c.class_id for c in closed_classes)
# 활성화된 클래스 조회 및 오늘 요일에 맞는 클래스만 필터링
all_classes_raw = db.query(ClassInfo).filter(
ClassInfo.is_active == True,
ClassInfo.store_id == current_store.id
).order_by(ClassInfo.class_number).all()
# 헬퍼 함수를 사용하여 오늘 요일에 맞는 클래스만 필터링
all_classes = filter_classes_by_weekday(all_classes_raw, today)
# 완료된 클래스와 마감된 클래스는 제외
# 대기자가 있는 클래스를 우선 표시하되, 설정된 개수만큼 채우기
classes_with_waiting_list = [c for c in all_classes if c.id in classes_with_waiting and c.id not in closed_class_ids]
classes_without_waiting = [c for c in all_classes if c.id not in classes_with_waiting and c.id not in completed_class_ids and c.id not in closed_class_ids]
# 대기자 있는 클래스 우선 배치 + 부족한 만큼 다음 교시로 채우기
selected_classes = classes_with_waiting_list[:settings.display_classes_count]
# 설정된 개수에 미달하면 대기자 없는 클래스로 채우기
remaining_slots = settings.display_classes_count - len(selected_classes)
if remaining_slots > 0:
selected_classes.extend(classes_without_waiting[:remaining_slots])
classes = selected_classes
# 표시 데이터 변환
board_items = []
for waiting in waiting_list:
class_info = next((c for c in classes if c.id == waiting.class_id), None)
if not class_info:
continue
if waiting.member and waiting.member.name:
display_name = waiting.member.name
else:
display_name = waiting.name if waiting.name else waiting.phone[-4:]
board_items.append(WaitingBoardItem(
id=waiting.id,
waiting_number=waiting.waiting_number,
display_name=display_name,
class_id=waiting.class_id,
class_name=class_info.class_name,
class_order=waiting.class_order,
is_empty_seat=waiting.is_empty_seat or False,
status=waiting.status
))
# ClassInfo 객체들을 dict로 변환 (weekday_schedule 파싱 포함)
classes_dict = [convert_class_to_dict(cls) for cls in classes]
return WaitingBoard(
store_name=settings.store_name,
business_date=today,
classes=classes_dict,
waiting_list=board_items
)
@router.put("/{waiting_id}/status")
async def update_waiting_status(
waiting_id: int,
status_update: WaitingStatusUpdate,
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="이미 처리된 대기입니다.")
old_class_id = waiting.class_id
old_business_date = waiting.business_date
waiting.status = status_update.status
if status_update.status == "attended":
waiting.attended_at = datetime.now()
elif status_update.status == "cancelled":
waiting.cancelled_at = datetime.now()
# 해당 클래스의 남은 대기자들 순서 정규화: 1번째, 2번째, 3번째... 순서로 재정렬
remaining_waitings = db.query(WaitingList).filter(
WaitingList.business_date == old_business_date,
WaitingList.class_id == old_class_id,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).order_by(WaitingList.class_order).all()
for idx, w in enumerate(remaining_waitings, start=1):
w.class_order = idx
db.commit()
# SSE 브로드캐스트: 상태 변경 알림
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="status_changed",
data={
"waiting_id": waiting_id,
"status": status_update.status,
"waiting_number": waiting.waiting_number
},
franchise_id=str(current_store.franchise_id)
)
return {"message": f"상태가 {status_update.status}(으)로 변경되었습니다."}
@router.post("/{waiting_id}/call")
async def call_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="대기자를 찾을 수 없습니다.")
waiting.call_count += 1
waiting.last_called_at = datetime.now()
db.commit()
# SSE 브로드캐스트: 호출 알림
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="user_called",
data={
"waiting_id": waiting_id,
"waiting_number": waiting.waiting_number,
"call_count": waiting.call_count
},
franchise_id=str(current_store.franchise_id)
)
return {
"message": f"대기번호 {waiting.waiting_number}번이 호출되었습니다.",
"call_count": waiting.call_count
}
@router.put("/{waiting_id}/swap/{target_id}")
async def swap_waiting_order(
waiting_id: int,
target_id: int,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
대기자를 다른 위치에 삽입 (드래그 앤 드롭용)
dragged item을 target item 위치에 삽입하고, 나머지 항목들을 이동
"""
dragged = db.query(WaitingList).filter(
WaitingList.id == waiting_id,
WaitingList.store_id == current_store.id
).first()
target = db.query(WaitingList).filter(
WaitingList.id == target_id,
WaitingList.store_id == current_store.id
).first()
if not dragged or not target:
raise HTTPException(status_code=404, detail="대기자를 찾을 수 없습니다.")
if dragged.status != "waiting" or target.status != "waiting":
raise HTTPException(status_code=400, detail="대기 중인 상태만 순서 변경이 가능합니다.")
# 같은 클래스 내에서만 이동 가능
if dragged.class_id != target.class_id:
raise HTTPException(status_code=400, detail="같은 클래스 내에서만 순서를 변경할 수 있습니다.")
old_order = dragged.class_order
new_order = target.class_order
# 같은 위치면 아무것도 하지 않음
if old_order == new_order:
return {"message": "순서가 변경되지 않았습니다."}
# 같은 클래스 내의 모든 대기자 조회
class_waitings = db.query(WaitingList).filter(
WaitingList.class_id == dragged.class_id,
WaitingList.business_date == dragged.business_date,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).all()
# 순서 재조정
if old_order < new_order:
# 아래로 이동: old_order < x <= new_order인 항목들을 위로 한 칸씩 올림
for waiting in class_waitings:
if waiting.id != dragged.id and old_order < waiting.class_order <= new_order:
waiting.class_order -= 1
dragged.class_order = new_order
else:
# 위로 이동: new_order <= x < old_order인 항목들을 아래로 한 칸씩 내림
for waiting in class_waitings:
if waiting.id != dragged.id and new_order <= waiting.class_order < old_order:
waiting.class_order += 1
dragged.class_order = new_order
# 순서 정규화: 1번째, 2번째, 3번째... 순서로 재정렬
normalized_waitings = sorted(class_waitings, key=lambda x: x.class_order)
for idx, waiting in enumerate(normalized_waitings, start=1):
waiting.class_order = idx
db.commit()
# SSE 브로드캐스트: 순서 변경 알림
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="order_changed",
data={
"waiting_id": waiting_id,
"target_id": target_id
},
franchise_id=str(current_store.franchise_id)
)
return {"message": "순서가 변경되었습니다."}
@router.put("/{waiting_id}/order")
async def change_waiting_order(
waiting_id: int,
order_update: WaitingOrderUpdate,
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="대기 중인 상태만 순서 변경이 가능합니다.")
# 같은 클래스 내에서 순서 변경
if order_update.direction == "up":
# 위로 이동 - 바로 위 대기자와 순서 교체
target = db.query(WaitingList).filter(
WaitingList.business_date == waiting.business_date,
WaitingList.class_id == waiting.class_id,
WaitingList.class_order < waiting.class_order,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).order_by(WaitingList.class_order.desc()).first()
if not target:
raise HTTPException(status_code=400, detail="이미 맨 위입니다.")
# 순서 교체
waiting.class_order, target.class_order = target.class_order, waiting.class_order
elif order_update.direction == "down":
# 아래로 이동 - 바로 아래 대기자와 순서 교체
target = db.query(WaitingList).filter(
WaitingList.business_date == waiting.business_date,
WaitingList.class_id == waiting.class_id,
WaitingList.class_order > waiting.class_order,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).order_by(WaitingList.class_order).first()
if not target:
raise HTTPException(status_code=400, detail="이미 맨 아래입니다.")
# 순서 교체
waiting.class_order, target.class_order = target.class_order, waiting.class_order
db.commit()
# SSE 브로드캐스트: 순서 변경 알림
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="order_changed",
data={
"waiting_id": waiting_id,
"direction": order_update.direction
},
franchise_id=str(current_store.franchise_id)
)
return {"message": "순서가 변경되었습니다."}
@router.put("/{waiting_id}/move-class")
async def move_to_another_class(
waiting_id: int,
class_update: WaitingClassUpdate,
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="대기 중인 상태만 이동이 가능합니다.")
# 대상 클래스 확인
target_class = db.query(ClassInfo).filter(
ClassInfo.id == class_update.target_class_id,
ClassInfo.store_id == current_store.id
).first()
if not target_class:
raise HTTPException(status_code=404, detail="대상 클래스를 찾을 수 없습니다.")
# 대상 클래스의 마지막 순서 찾기
max_order = db.query(func.max(WaitingList.class_order)).filter(
WaitingList.business_date == waiting.business_date,
WaitingList.class_id == target_class.id,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).scalar()
new_order = (max_order or 0) + 1
# 클래스 이동
old_class_id = waiting.class_id
old_business_date = waiting.business_date
waiting.class_id = target_class.id
waiting.class_order = new_order
# 변경사항을 DB에 즉시 반영 (쿼리 전에 flush 필수)
db.flush()
# 기존 클래스의 순서 정규화: 1번째, 2번째, 3번째... 순서로 재정렬
old_class_waitings = db.query(WaitingList).filter(
WaitingList.business_date == old_business_date,
WaitingList.class_id == old_class_id,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).order_by(WaitingList.class_order).all()
for idx, w in enumerate(old_class_waitings, start=1):
w.class_order = idx
# 새 클래스의 순서 정규화: 1번째, 2번째, 3번째... 순서로 재정렬
new_class_waitings = db.query(WaitingList).filter(
WaitingList.business_date == old_business_date,
WaitingList.class_id == target_class.id,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).order_by(WaitingList.class_order).all()
for idx, w in enumerate(new_class_waitings, start=1):
w.class_order = idx
db.commit()
# SSE 브로드캐스트: 클래스 이동 알림
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="class_moved",
data={
"waiting_id": waiting_id,
"old_class_id": old_class_id,
"new_class_id": target_class.id,
"new_class_name": target_class.class_name
},
franchise_id=str(current_store.franchise_id)
)
return {"message": f"{target_class.class_name}(으)로 이동되었습니다."}
@router.post("/batch-attendance")
async def batch_attendance(
batch: BatchAttendance,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
교시 마감 처리
- 특정 교시를 마감하여 더 이상 대기자를 등록할 수 없게 함
- 대기자 상태는 변경하지 않고 그대로 유지 (비활성화 상태로 표시)
"""
today = get_current_business_date(db, current_store.id)
# 클래스 정보 조회
class_info = db.query(ClassInfo).filter(
ClassInfo.id == batch.class_id,
ClassInfo.store_id == current_store.id
).first()
if not class_info:
raise HTTPException(status_code=404, detail="클래스를 찾을 수 없습니다.")
# 이미 마감된 교시인지 확인
existing_closure = db.query(ClassClosure).filter(
ClassClosure.business_date == today,
ClassClosure.class_id == batch.class_id,
ClassClosure.store_id == current_store.id
).first()
if existing_closure:
raise HTTPException(status_code=400, detail="이미 마감된 교시입니다.")
# 해당 클래스의 대기 중인 목록 조회 (카운트용) -> 상태 변경용으로 수정
waiting_list = db.query(WaitingList).filter(
WaitingList.business_date == today,
WaitingList.class_id == batch.class_id,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).all()
waiting_count = len(waiting_list)
# 대기자 상태를 'attended'로 변경하고 출석 시간 기록
for waiting in waiting_list:
waiting.status = "attended"
waiting.attended_at = datetime.now()
# 교시 마감 레코드 생성
closure = ClassClosure(
business_date=today,
class_id=batch.class_id,
closed_at=datetime.now(),
store_id=current_store.id
)
db.add(closure)
db.commit()
# SSE 브로드캐스트: 교시 마감 알림
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="class_closed",
data={
"class_id": batch.class_id,
"class_name": class_info.class_name,
"waiting_count": waiting_count
},
franchise_id=str(current_store.franchise_id)
)
# SSE 브로드캐스트: 일괄 출석 알림 (출석현황 업데이트용)
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="batch_attendance",
data={
"class_id": batch.class_id,
"class_name": class_info.class_name,
"count": waiting_count
},
franchise_id=str(current_store.franchise_id)
)
return {
"message": f"{class_info.class_name}이(가) 마감되고 {waiting_count}명이 일괄 출석 처리되었습니다.",
"waiting_count": waiting_count
}
@router.delete("/close-class/{class_id}")
async def unclose_class(
class_id: int,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
교시 마감 해제
- 실수로 마감한 교시를 다시 열어 대기자를 등록할 수 있게 함
"""
today = get_current_business_date(db, current_store.id)
# 마감 레코드 조회
closure = db.query(ClassClosure).filter(
ClassClosure.business_date == today,
ClassClosure.class_id == class_id,
ClassClosure.store_id == current_store.id
).first()
if not closure:
raise HTTPException(status_code=404, detail="마감되지 않은 교시입니다.")
# 클래스 정보 조회
class_info = db.query(ClassInfo).filter(
ClassInfo.id == class_id,
ClassInfo.store_id == current_store.id
).first()
if not class_info:
raise HTTPException(status_code=404, detail="클래스를 찾을 수 없습니다.")
# 마감 레코드 삭제
db.delete(closure)
db.commit()
# SSE 브로드캐스트: 교시 마감 해제 알림
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="class_reopened",
data={
"class_id": class_id,
"class_name": class_info.class_name
},
franchise_id=str(current_store.franchise_id)
)
return {
"message": f"{class_info.class_name}의 마감이 해제되었습니다."
}
@router.get("/next-batch-class")
async def get_next_batch_class(
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
다음 교시 마감 대상 클래스 조회
- 대기자가 있고 마감되지 않은 첫 번째 클래스 반환
"""
today = get_current_business_date(db, current_store.id)
# 이미 마감된 교시 ID 목록
closed_class_ids = db.query(ClassClosure.class_id).filter(
ClassClosure.business_date == today,
ClassClosure.store_id == current_store.id
).all()
closed_class_ids = set(c.class_id for c in closed_class_ids)
# 활성화된 클래스 조회 및 오늘 요일에 맞는 클래스만 필터링
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)
for cls in classes:
# 마감된 교시는 건너뜀
if cls.id in closed_class_ids:
continue
count = db.query(func.count(WaitingList.id)).filter(
WaitingList.business_date == today,
WaitingList.class_id == cls.id,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).scalar()
if count > 0:
return {
"class_id": cls.id,
"class_name": cls.class_name,
"class_number": cls.class_number,
"waiting_count": count
}
return {
"class_id": None,
"message": "대기자가 없습니다."
}
@router.get("/closed-classes")
async def get_closed_classes(
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
오늘 마감된 교시 목록 조회
"""
today = get_current_business_date(db, current_store.id)
closed_classes = db.query(ClassClosure).filter(
ClassClosure.business_date == today,
ClassClosure.store_id == current_store.id
).all()
return {
"closed_class_ids": [c.class_id for c in closed_classes]
}
@router.post("/insert-empty-seat")
async def insert_empty_seat(
empty_seat: EmptySeatInsert,
db: Session = Depends(get_db),
current_store: Store = Depends(get_current_store)
):
"""
빈 좌석 삽입
- 선택한 대기자 뒤에 빈 좌석을 삽입
- 뒤의 대기자들 순서는 자동으로 밀림
"""
# 기준 대기자 조회
base_waiting = db.query(WaitingList).filter(
WaitingList.id == empty_seat.waiting_id,
WaitingList.store_id == current_store.id
).first()
if not base_waiting:
raise HTTPException(status_code=404, detail="대기자를 찾을 수 없습니다.")
if base_waiting.status != "waiting":
raise HTTPException(status_code=400, detail="대기 중인 상태만 빈 좌석을 삽입할 수 있습니다.")
today = get_current_business_date(db, current_store.id)
# 해당 클래스에서 기준 대기자보다 뒤에 있는 모든 대기자들의 순서를 1씩 증가
following_waitings = db.query(WaitingList).filter(
WaitingList.business_date == today,
WaitingList.class_id == base_waiting.class_id,
WaitingList.class_order > base_waiting.class_order,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).all()
for waiting in following_waitings:
waiting.class_order += 1
# 빈 좌석 생성 (기준 대기자 바로 뒤)
empty_seat_entry = WaitingList(
business_date=today,
waiting_number=0, # 빈 좌석은 대기번호 0
phone="empty",
name="빈좌석",
class_id=base_waiting.class_id,
class_order=base_waiting.class_order + 1,
is_empty_seat=True,
status="waiting",
registered_at=datetime.now(),
store_id=current_store.id
)
db.add(empty_seat_entry)
# 해당 클래스의 모든 대기자 순서 정규화: 1번째, 2번째, 3번째... 순서로 재정렬
all_class_waitings = db.query(WaitingList).filter(
WaitingList.business_date == today,
WaitingList.class_id == base_waiting.class_id,
WaitingList.status == "waiting",
WaitingList.store_id == current_store.id
).order_by(WaitingList.class_order).all()
for idx, w in enumerate(all_class_waitings, start=1):
w.class_order = idx
db.commit()
db.refresh(empty_seat_entry)
# 클래스 정보 조회
class_info = db.query(ClassInfo).filter(
ClassInfo.id == base_waiting.class_id,
ClassInfo.store_id == current_store.id
).first()
# SSE 브로드캐스트: 빈 좌석 삽입 알림
await sse_manager.broadcast(
store_id=str(current_store.id),
event_type="empty_seat_inserted",
data={
"id": empty_seat_entry.id,
"class_id": base_waiting.class_id,
"class_name": class_info.class_name,
"class_order": empty_seat_entry.class_order
}
)
return {
"message": f"{class_info.class_name} {base_waiting.class_order}번 뒤에 빈 좌석이 삽입되었습니다.",
"empty_seat_id": empty_seat_entry.id
}