- 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>
253 lines
13 KiB
Python
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)
|