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>
This commit is contained in:
325
routers/class_management.py
Normal file
325
routers/class_management.py
Normal file
@@ -0,0 +1,325 @@
|
||||
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="모든 클래스가 만석입니다.")
|
||||
Reference in New Issue
Block a user