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)