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

326 lines
10 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List, Dict, Optional
from datetime import date
import json
from database import get_db
from models import ClassInfo, WaitingList, Store
from schemas import (
ClassInfo as ClassInfoSchema,
ClassInfoCreate,
ClassInfoUpdate
)
from auth import get_current_store
router = APIRouter()
# 요일 스케줄 기본값
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 딕셔너리로 안전하게 변환
Args:
schedule_str: JSON 형식의 weekday_schedule 문자열
Returns:
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()
# 모든 요일 키가 존재하는지 확인하고 없으면 기본값으로 채움
result = DEFAULT_WEEKDAY_SCHEDULE.copy()
for key in result.keys():
if key in schedule:
result[key] = bool(schedule[key])
return result
except (json.JSONDecodeError, TypeError, ValueError):
return DEFAULT_WEEKDAY_SCHEDULE.copy()
def serialize_weekday_schedule(schedule: Dict[str, bool]) -> str:
"""
weekday_schedule 딕셔너리를 JSON 문자열로 안전하게 변환
Args:
schedule: weekday_schedule 딕셔너리
Returns:
JSON 형식의 문자열
"""
if not schedule:
schedule = DEFAULT_WEEKDAY_SCHEDULE
# 모든 요일 키가 존재하는지 확인
result = DEFAULT_WEEKDAY_SCHEDULE.copy()
for key in result.keys():
if key in schedule:
result[key] = bool(schedule[key])
return json.dumps(result)
def prepare_class_response(db_class: ClassInfo, db: Session, today: date = None) -> dict:
"""
ClassInfo 객체를 API 응답용 딕셔너리로 변환
Args:
db_class: ClassInfo 모델 인스턴스
db: 데이터베이스 세션
today: 기준 날짜 (기본값: 오늘)
Returns:
API 응답용 딕셔너리
"""
if today is None:
today = date.today()
# 현재 대기자 수 조회
current_count = db.query(func.count(WaitingList.id)).filter(
WaitingList.class_id == db_class.id,
WaitingList.business_date == today,
WaitingList.status == "waiting"
).scalar() or 0
# weekday_schedule을 미리 파싱 (Pydantic validation 전에 변환)
parsed_schedule = parse_weekday_schedule(db_class.weekday_schedule)
# 수동으로 딕셔너리 생성 (from_orm 대신)
result = {
"id": db_class.id,
"class_number": db_class.class_number,
"class_name": db_class.class_name,
"start_time": db_class.start_time,
"end_time": db_class.end_time,
"max_capacity": db_class.max_capacity,
"is_active": db_class.is_active,
"weekday_schedule": parsed_schedule,
"class_type": db_class.class_type if hasattr(db_class, 'class_type') else 'all',
"created_at": db_class.created_at,
"updated_at": db_class.updated_at,
"current_count": current_count
}
return result
@router.post("/", response_model=ClassInfoSchema)
async def create_class(
class_info: ClassInfoCreate,
current_store: Store = Depends(get_current_store),
db: Session = Depends(get_db)
):
"""클래스(교시) 생성"""
# 같은 번호의 클래스가 있는지 확인 (매장별, class_type별)
existing = db.query(ClassInfo).filter(
ClassInfo.store_id == current_store.id,
ClassInfo.class_number == class_info.class_number,
ClassInfo.class_type == class_info.class_type
).first()
if existing:
class_type_name = {'weekday': '평일', 'weekend': '주말', 'all': '전체'}[class_info.class_type]
raise HTTPException(
status_code=400,
detail=f"{class_type_name} {class_info.class_number}교시가 이미 존재합니다."
)
# weekday_schedule을 JSON 문자열로 안전하게 변환
data = class_info.dict()
if 'weekday_schedule' in data:
data['weekday_schedule'] = serialize_weekday_schedule(data['weekday_schedule'])
db_class = ClassInfo(**data, store_id=current_store.id)
db.add(db_class)
db.commit()
db.refresh(db_class)
# 헬퍼 함수를 사용하여 응답 생성
return prepare_class_response(db_class, db)
@router.get("/", response_model=List[ClassInfoSchema])
async def get_classes(
include_inactive: bool = False,
class_type: Optional[str] = None,
current_store: Store = Depends(get_current_store),
db: Session = Depends(get_db)
):
"""클래스 목록 조회"""
query = db.query(ClassInfo).filter(ClassInfo.store_id == current_store.id)
if not include_inactive:
query = query.filter(ClassInfo.is_active == True)
# class_type 필터링
if class_type:
query = query.filter(ClassInfo.class_type == class_type)
classes = query.order_by(ClassInfo.class_number).all()
# 헬퍼 함수를 사용하여 각 클래스 정보 변환
today = date.today()
result = [prepare_class_response(cls, db, today) for cls in classes]
return result
@router.get("/{class_id}", response_model=ClassInfoSchema)
async def get_class(
class_id: int,
current_store: Store = Depends(get_current_store),
db: Session = Depends(get_db)
):
"""클래스 상세 조회"""
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="클래스를 찾을 수 없습니다.")
# 헬퍼 함수를 사용하여 응답 생성
return prepare_class_response(class_info, db)
@router.put("/{class_id}", response_model=ClassInfoSchema)
async def update_class(
class_id: int,
class_info: ClassInfoUpdate,
current_store: Store = Depends(get_current_store),
db: Session = Depends(get_db)
):
"""클래스 수정"""
db_class = db.query(ClassInfo).filter(
ClassInfo.id == class_id,
ClassInfo.store_id == current_store.id
).first()
if not db_class:
raise HTTPException(status_code=404, detail="클래스를 찾을 수 없습니다.")
# 업데이트할 필드만 수정
update_data = class_info.dict(exclude_unset=True)
# 클래스 번호 또는 타입 변경 시 중복 체크 (매장별, class_type별)
if "class_number" in update_data or "class_type" in update_data:
check_class_number = update_data.get("class_number", db_class.class_number)
check_class_type = update_data.get("class_type", db_class.class_type)
existing = db.query(ClassInfo).filter(
ClassInfo.store_id == current_store.id,
ClassInfo.class_number == check_class_number,
ClassInfo.class_type == check_class_type,
ClassInfo.id != class_id
).first()
if existing:
class_type_name = {'weekday': '평일', 'weekend': '주말', 'all': '전체'}[check_class_type]
raise HTTPException(
status_code=400,
detail=f"{class_type_name} {check_class_number}교시가 이미 존재합니다."
)
# weekday_schedule을 JSON 문자열로 안전하게 변환
if 'weekday_schedule' in update_data:
update_data['weekday_schedule'] = serialize_weekday_schedule(update_data['weekday_schedule'])
for field, value in update_data.items():
setattr(db_class, field, value)
db.commit()
db.refresh(db_class)
# 헬퍼 함수를 사용하여 응답 생성
return prepare_class_response(db_class, db)
@router.delete("/{class_id}")
async def delete_class(
class_id: int,
current_store: Store = Depends(get_current_store),
db: Session = Depends(get_db)
):
"""클래스 삭제 (비활성화)"""
db_class = db.query(ClassInfo).filter(
ClassInfo.id == class_id,
ClassInfo.store_id == current_store.id
).first()
if not db_class:
raise HTTPException(status_code=404, detail="클래스를 찾을 수 없습니다.")
# 실제 삭제 대신 비활성화
db_class.is_active = False
db.commit()
return {"message": f"{db_class.class_name}이(가) 비활성화되었습니다."}
@router.post("/{class_id}/activate")
async def activate_class(
class_id: int,
current_store: Store = Depends(get_current_store),
db: Session = Depends(get_db)
):
"""클래스 활성화"""
db_class = db.query(ClassInfo).filter(
ClassInfo.id == class_id,
ClassInfo.store_id == current_store.id
).first()
if not db_class:
raise HTTPException(status_code=404, detail="클래스를 찾을 수 없습니다.")
db_class.is_active = True
db.commit()
return {"message": f"{db_class.class_name}이(가) 활성화되었습니다."}
@router.get("/available/next")
async def get_next_available_class(
current_store: Store = Depends(get_current_store),
db: Session = Depends(get_db)
):
"""
다음 배치 가능한 클래스 조회
- 각 클래스의 현재 인원과 최대 수용 인원을 비교
- 여유가 있는 첫 번째 클래스 반환
"""
today = date.today()
classes = db.query(ClassInfo).filter(
ClassInfo.store_id == current_store.id,
ClassInfo.is_active == True
).order_by(ClassInfo.class_number).all()
for cls in classes:
current_count = db.query(func.count(WaitingList.id)).filter(
WaitingList.class_id == cls.id,
WaitingList.business_date == today,
WaitingList.status == "waiting"
).scalar()
if current_count < cls.max_capacity:
return {
"class_id": cls.id,
"class_name": cls.class_name,
"class_number": cls.class_number,
"current_count": current_count,
"max_capacity": cls.max_capacity,
"available_slots": cls.max_capacity - current_count
}
# 모든 클래스가 가득 찬 경우
raise HTTPException(status_code=400, detail="모든 클래스가 만석입니다.")