- 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>
326 lines
10 KiB
Python
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="모든 클래스가 만석입니다.")
|