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

253 lines
13 KiB
Python

from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Date, Time, Table, Float
from sqlalchemy.orm import relationship
from database import Base
from datetime import datetime, date
# M:N 관계를 위한 연결 테이블
user_stores = Table('user_stores', Base.metadata,
Column('user_id', Integer, ForeignKey('users.id')),
Column('store_id', Integer, ForeignKey('store.id'))
)
class Franchise(Base):
"""프랜차이즈"""
__tablename__ = "franchise"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False) # 프랜차이즈명
code = Column(String, unique=True, nullable=False, index=True) # 프랜차이즈 코드
member_type = Column(String, default="store") # store: 매장별 관리, franchise: 프랜차이즈 통합 관리
is_active = Column(Boolean, default=True) # 활성화 여부
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# 관계 설정
stores = relationship("Store", back_populates="franchise")
users = relationship("User", back_populates="franchise", foreign_keys="User.franchise_id")
class Store(Base):
"""매장"""
__tablename__ = "store"
id = Column(Integer, primary_key=True, index=True)
franchise_id = Column(Integer, ForeignKey("franchise.id"), nullable=False, index=True)
name = Column(String, nullable=False) # 매장명
code = Column(String, unique=True, nullable=False, index=True) # 매장 코드
is_active = Column(Boolean, default=True) # 활성화 여부
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# 관계 설정
franchise = relationship("Franchise", back_populates="stores")
users = relationship("User", back_populates="store", foreign_keys="User.store_id")
store_settings = relationship("StoreSettings", back_populates="store")
daily_closings = relationship("DailyClosing", back_populates="store")
classes = relationship("ClassInfo", back_populates="store")
members = relationship("Member", back_populates="store")
waiting_list = relationship("WaitingList", back_populates="store")
# New relationship for Multi-Store Managers
managers = relationship("User", secondary=user_stores, back_populates="managed_stores")
class User(Base):
"""사용자 (인증)"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, nullable=False, index=True) # 로그인 ID
password_hash = Column(String, nullable=False) # 비밀번호 해시
role = Column(String, nullable=False) # system_admin, franchise_admin, store_admin, franchise_manager
franchise_id = Column(Integer, ForeignKey("franchise.id"), nullable=True, index=True) # 프랜차이즈 관리자인 경우
store_id = Column(Integer, ForeignKey("store.id"), nullable=True, index=True) # 매장 관리자인 경우 relative
is_active = Column(Boolean, default=True) # 활성화 여부
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# 관계 설정
franchise = relationship("Franchise", back_populates="users", foreign_keys=[franchise_id])
store = relationship("Store", back_populates="users", foreign_keys=[store_id])
# New relationship for Multi-Store Managers
managed_stores = relationship("Store", secondary=user_stores, back_populates="managers")
class StoreSettings(Base):
"""매장 설정"""
__tablename__ = "store_settings"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(Integer, ForeignKey("store.id"), nullable=False, index=True) # 매장 ID
store_name = Column(String, nullable=False)
display_classes_count = Column(Integer, default=3) # 대기현황판에 보여줄 클래스 수
list_direction = Column(String, default="vertical") # vertical or horizontal
rows_per_class = Column(Integer, default=1) # 클래스당 줄 수
admin_password = Column(String, default="1234")
max_waiting_limit = Column(Integer, default=50) # 최대 대기 등록 제한 (0 = 무제한)
use_max_waiting_limit = Column(Boolean, default=True) # 최대 대기 인원 제한 사용 여부
block_last_class_registration = Column(Boolean, default=False) # 마지막 교시 정원 초과 시 대기접수 차단
auto_register_member = Column(Boolean, default=False) # 대기 등록 시 자동 회원가입
business_day_start = Column(Integer, default=5) # 영업일 기준 시간 (0~23)
auto_closing = Column(Boolean, default=True) # 영업일 변경 시 자동 마감 및 리셋 여부 (False: 대기자 이월)
closing_action = Column(String, default="reset") # 자동 마감 시 미처리 대기자 처리 방식 ('reset' or 'attended')
# 출석 횟수 표시 설정
attendance_count_type = Column(String, default="days") # 'days': 최근 N일, 'monthly': 이번 달
attendance_lookback_days = Column(Integer, default=30) # N일 (기본 30일)
# 대기현황판 표시 설정
show_waiting_number = Column(Boolean, default=True) # 대기번호 표시 유무
mask_customer_name = Column(Boolean, default=False) # 이름 마스킹 (홍O동)
name_display_length = Column(Integer, default=0) # 이름 표시 자릿수 (0 = 전체 표시)
show_order_number = Column(Boolean, default=True) # 순번(1번째) 표시 유무
board_display_order = Column(String, default="number,name,order") # 표시 순서
# 폰트 설정
manager_font_family = Column(String, default="Nanum Gothic")
manager_font_size = Column(String, default="15px")
board_font_family = Column(String, default="Nanum Gothic")
board_font_size = Column(String, default="24px")
# 대기접수 키패드 설정
keypad_style = Column(String, default="modern") # modern, bold, dark, colorful
keypad_font_size = Column(String, default="large") # small, medium, large, xlarge
# 개점 설정
daily_opening_rule = Column(String, default="strict") # strict: 1일 1회, flexible: 2회 이상(다음날)
# 대기접수 완료 모달 설정
waiting_modal_timeout = Column(Integer, default=5) # 대기접수 모달 타이머 (초)
show_member_name_in_waiting_modal = Column(Boolean, default=True) # 대기접수 모달 회원명 표시 여부
show_new_member_text_in_waiting_modal = Column(Boolean, default=True) # 대기접수 모달 신규회원 문구 표시 여부
enable_waiting_voice_alert = Column(Boolean, default=False) # 대기접수 완료 음성 안내 여부
waiting_voice_message = Column(String, nullable=True) # 대기접수 완료 음성 안내 커스텀 메시지
waiting_voice_name = Column(String, nullable=True) # 대기접수 완료 음성 안내 선택된 목소리 이름
waiting_voice_rate = Column(Float, default=1.0) # 대기접수 완료 음성 안내 속도 (0.1 ~ 10, 기본 1.0)
waiting_voice_pitch = Column(Float, default=1.0) # 대기접수 완료 음성 안내 높낮이 (0 ~ 2, 기본 1.0)
# 대기관리자 화면 레이아웃 설정
waiting_manager_max_width = Column(Integer, nullable=True) # 대기관리자 화면 최대 너비 (px), None이면 기본값(95%)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# 관계 설정
store = relationship("Store", back_populates="store_settings")
class DailyClosing(Base):
"""일마감"""
__tablename__ = "daily_closing"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(Integer, ForeignKey("store.id"), nullable=False, index=True) # 매장 ID
business_date = Column(Date, nullable=False, index=True) # 영업일
opening_time = Column(DateTime) # 개점 시간
closing_time = Column(DateTime) # 마감 시간
is_closed = Column(Boolean, default=False) # 마감 여부
total_waiting = Column(Integer, default=0) # 총 대기 수
total_attended = Column(Integer, default=0) # 총 출석 수
total_cancelled = Column(Integer, default=0) # 총 취소 수
created_at = Column(DateTime, default=datetime.now)
# 관계 설정
store = relationship("Store", back_populates="daily_closings")
class ClassInfo(Base):
"""클래스(교시) 정보"""
__tablename__ = "class_info"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(Integer, ForeignKey("store.id"), nullable=False, index=True) # 매장 ID
class_number = Column(Integer, nullable=False) # 교시 번호 (1, 2, 3, ...)
class_name = Column(String, nullable=False) # 교시명 (1교시, 2교시, ...)
start_time = Column(Time, nullable=False) # 시작 시간
end_time = Column(Time, nullable=False) # 종료 시간
max_capacity = Column(Integer, default=10) # 최대 수용 인원
is_active = Column(Boolean, default=True) # 활성화 여부
weekday_schedule = Column(String, default='{"mon":true,"tue":true,"wed":true,"thu":true,"fri":true,"sat":true,"sun":true}') # 요일 스케줄 (JSON)
class_type = Column(String, default='all') # 클래스 타입: weekday(평일), weekend(주말), all(전체)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# 관계 설정
store = relationship("Store", back_populates="classes")
waiting_list = relationship("WaitingList", back_populates="class_info")
class Member(Base):
"""회원 정보"""
__tablename__ = "members"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(Integer, ForeignKey("store.id"), nullable=False, index=True) # 매장 ID
name = Column(String, nullable=False)
phone = Column(String, nullable=False, index=True) # unique 제거 (매장별/프랜차이즈별 로직으로 처리)
barcode = Column(String, unique=True, nullable=True, index=True) # 바코드 (유일 코드)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# 관계 설정
store = relationship("Store", back_populates="members")
waiting_list = relationship("WaitingList", back_populates="member")
class WaitingList(Base):
"""대기자 목록"""
__tablename__ = "waiting_list"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(Integer, ForeignKey("store.id"), nullable=False, index=True) # 매장 ID
business_date = Column(Date, nullable=False, index=True) # 영업일
waiting_number = Column(Integer, nullable=False) # 대기번호
phone = Column(String, nullable=False) # 핸드폰번호
name = Column(String) # 대기자명 (회원인 경우 자동 입력)
class_id = Column(Integer, ForeignKey("class_info.id"), nullable=False)
class_order = Column(Integer, nullable=False) # 해당 클래스 내 순서
member_id = Column(Integer, ForeignKey("members.id"), index=True) # 회원 ID (있는 경우)
is_empty_seat = Column(Boolean, default=False) # 빈 좌석 여부
status = Column(String, default="waiting", index=True) # waiting, attended, cancelled, no_show
registered_at = Column(DateTime, default=datetime.now) # 접수 시간
attended_at = Column(DateTime, index=True) # 출석 시간
cancelled_at = Column(DateTime) # 취소 시간
call_count = Column(Integer, default=0) # 호출 횟수
last_called_at = Column(DateTime) # 마지막 호출 시간
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# 관계 설정
store = relationship("Store", back_populates="waiting_list")
class_info = relationship("ClassInfo", back_populates="waiting_list")
member = relationship("Member", back_populates="waiting_list")
class ClassClosure(Base):
"""교시 마감 정보"""
__tablename__ = "class_closure"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(Integer, ForeignKey("store.id"), nullable=False, index=True) # 매장 ID
business_date = Column(Date, nullable=False, index=True) # 영업일
class_id = Column(Integer, ForeignKey("class_info.id"), nullable=False) # 교시 ID
closed_at = Column(DateTime, default=datetime.now) # 마감 시간
created_at = Column(DateTime, default=datetime.now)
class WaitingHistory(Base):
"""대기 이력 (통계용)"""
__tablename__ = "waiting_history"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(Integer, ForeignKey("store.id"), nullable=False, index=True) # 매장 ID
business_date = Column(Date, nullable=False, index=True)
waiting_number = Column(Integer, nullable=False)
phone = Column(String, nullable=False)
name = Column(String)
class_id = Column(Integer)
class_name = Column(String)
status = Column(String) # attended, cancelled, no_show
registered_at = Column(DateTime)
completed_at = Column(DateTime)
waiting_time_minutes = Column(Integer) # 대기 시간 (분)
created_at = Column(DateTime, default=datetime.now)