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:
476
routers/stores.py
Normal file
476
routers/stores.py
Normal file
@@ -0,0 +1,476 @@
|
||||
"""
|
||||
매장 관리 라우터
|
||||
- 매장 목록 조회
|
||||
- 매장 생성
|
||||
- 매장 상세 조회
|
||||
- 매장 수정
|
||||
- 매장 비활성화
|
||||
- 매장별 통계
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from datetime import datetime, date
|
||||
from typing import List
|
||||
|
||||
from database import get_db
|
||||
from models import Store, User, WaitingList, DailyClosing, StoreSettings, Member, ClassInfo
|
||||
from schemas import (
|
||||
Store as StoreSchema,
|
||||
StoreCreate,
|
||||
StoreUpdate
|
||||
)
|
||||
from auth import get_current_user, require_franchise_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[StoreSchema])
|
||||
async def get_stores(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장 목록 조회
|
||||
|
||||
프랜차이즈 관리자와 매장 관리자 모두 접근 가능
|
||||
각자 자신의 프랜차이즈 매장 목록만 조회 가능
|
||||
|
||||
Returns:
|
||||
List[Store]: 프랜차이즈의 모든 매장 목록
|
||||
"""
|
||||
# system_admin은 모든 매장 조회 가능 (필터 없음)
|
||||
if current_user.role == 'system_admin':
|
||||
stores = db.query(Store).order_by(Store.created_at.desc()).all()
|
||||
|
||||
# franchise_manager는 관리하는 매장만 조회
|
||||
elif current_user.role == 'franchise_manager':
|
||||
if not current_user.managed_stores:
|
||||
return []
|
||||
|
||||
managed_ids = [s.id for s in current_user.managed_stores]
|
||||
stores = db.query(Store).filter(
|
||||
Store.id.in_(managed_ids)
|
||||
).order_by(Store.created_at.desc()).all()
|
||||
|
||||
# franchise_admin과 store_admin은 자신의 프랜차이즈 매장만 조회
|
||||
elif current_user.role in ['franchise_admin', 'store_admin']:
|
||||
if not current_user.franchise_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="프랜차이즈 정보가 없습니다"
|
||||
)
|
||||
stores = db.query(Store).filter(
|
||||
Store.franchise_id == current_user.franchise_id
|
||||
).order_by(Store.created_at.desc()).all()
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="매장 목록 조회 권한이 없습니다"
|
||||
)
|
||||
|
||||
return stores
|
||||
|
||||
|
||||
@router.post("/", response_model=StoreSchema, status_code=status.HTTP_201_CREATED)
|
||||
async def create_store(
|
||||
store_create: StoreCreate,
|
||||
current_user: User = Depends(require_franchise_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장 생성
|
||||
|
||||
프랜차이즈 관리자만 접근 가능
|
||||
|
||||
Args:
|
||||
store_create: 생성할 매장 정보
|
||||
|
||||
Returns:
|
||||
Store: 생성된 매장 정보
|
||||
"""
|
||||
# 매장 코드 자동 생성
|
||||
from models import Franchise
|
||||
franchise = db.query(Franchise).filter(Franchise.id == current_user.franchise_id).first()
|
||||
if not franchise:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="프랜차이즈를 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
# 프랜차이즈 코드의 첫 글자 가져오기
|
||||
prefix = franchise.code[0] if franchise.code else "S"
|
||||
|
||||
# 해당 프랜차이즈의 기존 매장 중 같은 prefix를 가진 매장 코드에서 가장 큰 번호 찾기
|
||||
stores = db.query(Store).filter(
|
||||
Store.franchise_id == current_user.franchise_id
|
||||
).all()
|
||||
|
||||
max_number = 0
|
||||
for store in stores:
|
||||
if store.code.startswith(prefix) and len(store.code) > 1:
|
||||
try:
|
||||
number = int(store.code[1:])
|
||||
if number > max_number:
|
||||
max_number = number
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# 새로운 매장 코드 생성 (예: S001, S002, S003...)
|
||||
new_code = f"{prefix}{str(max_number + 1).zfill(3)}"
|
||||
|
||||
# 매장 생성
|
||||
new_store = Store(
|
||||
franchise_id=current_user.franchise_id,
|
||||
name=store_create.name,
|
||||
code=new_code,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(new_store)
|
||||
db.commit()
|
||||
db.refresh(new_store)
|
||||
|
||||
# 기본 매장 설정 생성
|
||||
default_settings = StoreSettings(
|
||||
store_id=new_store.id,
|
||||
store_name=store_create.name,
|
||||
display_classes_count=3,
|
||||
list_direction="vertical",
|
||||
rows_per_class=1,
|
||||
admin_password="1234",
|
||||
max_waiting_limit=50,
|
||||
block_last_class_registration=False,
|
||||
show_waiting_number=True,
|
||||
mask_customer_name=False,
|
||||
show_order_number=True,
|
||||
board_display_order="number,name,order"
|
||||
)
|
||||
db.add(default_settings)
|
||||
db.commit()
|
||||
|
||||
return new_store
|
||||
|
||||
|
||||
@router.get("/{store_id}", response_model=StoreSchema)
|
||||
async def get_store(
|
||||
store_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장 상세 조회
|
||||
|
||||
Args:
|
||||
store_id: 매장 ID
|
||||
|
||||
Returns:
|
||||
Store: 매장 상세 정보
|
||||
"""
|
||||
store = db.query(Store).filter(Store.id == store_id).first()
|
||||
|
||||
if not store:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="매장을 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
# 권한 확인
|
||||
if current_user.role == 'franchise_admin':
|
||||
if current_user.franchise_id != store.franchise_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="접근 권한이 없습니다"
|
||||
)
|
||||
elif current_user.role == 'franchise_manager':
|
||||
managed_ids = [s.id for s in current_user.managed_stores]
|
||||
if store_id not in managed_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="접근 권한이 없습니다"
|
||||
)
|
||||
elif current_user.role == 'store_admin':
|
||||
if current_user.store_id != store_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="접근 권한이 없습니다"
|
||||
)
|
||||
elif current_user.role != 'system_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="접근 권한이 없습니다"
|
||||
)
|
||||
|
||||
return store
|
||||
|
||||
|
||||
@router.put("/{store_id}", response_model=StoreSchema)
|
||||
async def update_store(
|
||||
store_id: int,
|
||||
store_update: StoreUpdate,
|
||||
current_user: User = Depends(require_franchise_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장 정보 수정
|
||||
|
||||
Args:
|
||||
store_id: 매장 ID
|
||||
store_update: 수정할 매장 정보
|
||||
|
||||
Returns:
|
||||
Store: 수정된 매장 정보
|
||||
"""
|
||||
store = db.query(Store).filter(
|
||||
Store.id == store_id,
|
||||
Store.franchise_id == current_user.franchise_id
|
||||
).first()
|
||||
|
||||
if not store:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="매장을 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
# 매장 코드 변경 시 중복 체크
|
||||
if store_update.code and store_update.code != store.code:
|
||||
existing_store = db.query(Store).filter(Store.code == store_update.code).first()
|
||||
if existing_store:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="이미 존재하는 매장 코드입니다"
|
||||
)
|
||||
|
||||
# 수정
|
||||
update_data = store_update.dict(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(store, key, value)
|
||||
|
||||
store.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
|
||||
return store
|
||||
|
||||
|
||||
@router.delete("/{store_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_store(
|
||||
store_id: int,
|
||||
current_user: User = Depends(require_franchise_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장 비활성화
|
||||
|
||||
실제로 삭제하지 않고 is_active를 False로 변경
|
||||
|
||||
Args:
|
||||
store_id: 매장 ID
|
||||
"""
|
||||
store = db.query(Store).filter(
|
||||
Store.id == store_id,
|
||||
Store.franchise_id == current_user.franchise_id
|
||||
).first()
|
||||
|
||||
if not store:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="매장을 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
store.is_active = False
|
||||
store.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.post("/{store_id}/deactivate", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def deactivate_store(
|
||||
store_id: int,
|
||||
current_user: User = Depends(require_franchise_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장 비활성화
|
||||
|
||||
is_active를 False로 변경
|
||||
|
||||
Args:
|
||||
store_id: 매장 ID
|
||||
"""
|
||||
store = db.query(Store).filter(
|
||||
Store.id == store_id,
|
||||
Store.franchise_id == current_user.franchise_id
|
||||
).first()
|
||||
|
||||
if not store:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="매장을 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
store.is_active = False
|
||||
store.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.post("/{store_id}/activate", response_model=StoreSchema)
|
||||
async def activate_store(
|
||||
store_id: int,
|
||||
current_user: User = Depends(require_franchise_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장 활성화
|
||||
|
||||
is_active를 True로 변경
|
||||
|
||||
Args:
|
||||
store_id: 매장 ID
|
||||
|
||||
Returns:
|
||||
Store: 활성화된 매장 정보
|
||||
"""
|
||||
store = db.query(Store).filter(
|
||||
Store.id == store_id,
|
||||
Store.franchise_id == current_user.franchise_id
|
||||
).first()
|
||||
|
||||
if not store:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="매장을 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
store.is_active = True
|
||||
store.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
|
||||
return store
|
||||
|
||||
|
||||
@router.get("/code/{store_code}", response_model=StoreSchema)
|
||||
async def get_store_by_code(
|
||||
store_code: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장 코드로 매장 조회
|
||||
|
||||
URL 파라미터로 매장을 선택할 수 있도록 매장 코드로 조회
|
||||
모든 인증된 사용자가 접근 가능
|
||||
|
||||
Args:
|
||||
store_code: 매장 코드 (예: S001, S002)
|
||||
|
||||
Returns:
|
||||
Store: 매장 정보
|
||||
"""
|
||||
store = db.query(Store).filter(
|
||||
Store.code == store_code,
|
||||
Store.is_active == True
|
||||
).first()
|
||||
|
||||
if not store:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"매장 코드 '{store_code}'를 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
return store
|
||||
|
||||
|
||||
@router.get("/{store_id}/stats")
|
||||
async def get_store_stats(
|
||||
store_id: int,
|
||||
current_user: User = Depends(require_franchise_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""매장별 통계 조회
|
||||
|
||||
Args:
|
||||
store_id: 매장 ID
|
||||
|
||||
Returns:
|
||||
dict: 매장 통계 정보
|
||||
"""
|
||||
# 매장 존재 및 권한 확인
|
||||
store = db.query(Store).filter(
|
||||
Store.id == store_id,
|
||||
Store.franchise_id == current_user.franchise_id
|
||||
).first()
|
||||
|
||||
if not store:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="매장을 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
today = date.today()
|
||||
|
||||
# franchise_manager 권한 확인
|
||||
if current_user.role == 'franchise_manager':
|
||||
managed_ids = [s.id for s in current_user.managed_stores]
|
||||
if store_id not in managed_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="해당 매장에 대한 접근 권한이 없습니다"
|
||||
)
|
||||
|
||||
# 오늘의 대기 통계
|
||||
today_stats = db.query(DailyClosing).filter(
|
||||
DailyClosing.store_id == store_id,
|
||||
DailyClosing.business_date == today
|
||||
).first()
|
||||
|
||||
# 현재 대기 중인 고객 수
|
||||
current_waiting = db.query(func.count(WaitingList.id)).filter(
|
||||
WaitingList.store_id == store_id,
|
||||
WaitingList.status == 'waiting'
|
||||
).scalar()
|
||||
|
||||
# 총 회원 수
|
||||
total_members = db.query(func.count(Member.id)).filter(
|
||||
Member.store_id == store_id
|
||||
).scalar()
|
||||
|
||||
# 운영 중인 수업 수
|
||||
active_classes = db.query(func.count(ClassInfo.id)).filter(
|
||||
ClassInfo.store_id == store_id,
|
||||
ClassInfo.is_active == True
|
||||
).scalar()
|
||||
|
||||
# 최근 7일 통계
|
||||
from datetime import timedelta
|
||||
week_ago = today - timedelta(days=7)
|
||||
|
||||
weekly_stats = db.query(
|
||||
func.coalesce(func.sum(DailyClosing.total_waiting), 0).label('total_waiting'),
|
||||
func.coalesce(func.sum(DailyClosing.total_attended), 0).label('total_attended'),
|
||||
func.coalesce(func.sum(DailyClosing.total_cancelled), 0).label('total_cancelled')
|
||||
).filter(
|
||||
DailyClosing.store_id == store_id,
|
||||
DailyClosing.business_date >= week_ago,
|
||||
DailyClosing.business_date <= today
|
||||
).first()
|
||||
|
||||
return {
|
||||
'store_id': store_id,
|
||||
'store_name': store.name,
|
||||
'store_code': store.code,
|
||||
'is_active': store.is_active,
|
||||
'today': {
|
||||
'total_waiting': today_stats.total_waiting if today_stats else 0,
|
||||
'total_attended': today_stats.total_attended if today_stats else 0,
|
||||
'total_cancelled': today_stats.total_cancelled if today_stats else 0,
|
||||
'is_open': today_stats.is_closed == False if today_stats else False,
|
||||
'opening_time': today_stats.opening_time if today_stats else None,
|
||||
'closing_time': today_stats.closing_time if today_stats else None
|
||||
},
|
||||
'current_waiting': current_waiting,
|
||||
'total_members': total_members,
|
||||
'active_classes': active_classes,
|
||||
'weekly': {
|
||||
'total_waiting': weekly_stats.total_waiting if weekly_stats else 0,
|
||||
'total_attended': weekly_stats.total_attended if weekly_stats else 0,
|
||||
'total_cancelled': weekly_stats.total_cancelled if weekly_stats else 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user