- 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>
217 lines
8.1 KiB
Python
217 lines
8.1 KiB
Python
"""
|
|
프랜차이즈 시스템 추가 마이그레이션
|
|
|
|
새로운 테이블:
|
|
- franchise: 프랜차이즈
|
|
- store: 매장
|
|
- users: 사용자 (인증)
|
|
|
|
기존 테이블 수정:
|
|
- store_id 컬럼 추가: store_settings, daily_closing, class_info, members, waiting_list, class_closure, waiting_history
|
|
"""
|
|
|
|
import sqlite3
|
|
from pathlib import Path
|
|
import shutil
|
|
from datetime import datetime
|
|
import bcrypt
|
|
|
|
# 비밀번호 해싱 함수
|
|
def hash_password(password: str) -> str:
|
|
"""비밀번호를 bcrypt로 해싱"""
|
|
password_bytes = password.encode('utf-8')
|
|
salt = bcrypt.gensalt()
|
|
hashed = bcrypt.hashpw(password_bytes, salt)
|
|
return hashed.decode('utf-8')
|
|
|
|
def backup_database(db_path):
|
|
"""데이터베이스 백업"""
|
|
backup_path = db_path.parent / f"waiting_system_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db"
|
|
shutil.copy2(db_path, backup_path)
|
|
print(f"✓ 데이터베이스 백업 완료: {backup_path}")
|
|
return backup_path
|
|
|
|
def migrate():
|
|
db_path = Path(__file__).parent / "waiting_system.db"
|
|
|
|
if not db_path.exists():
|
|
print("❌ 데이터베이스 파일을 찾을 수 없습니다.")
|
|
return
|
|
|
|
# 백업
|
|
backup_path = backup_database(db_path)
|
|
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
print("\n=== 프랜차이즈 시스템 마이그레이션 시작 ===\n")
|
|
|
|
# 1. 새 테이블 생성
|
|
print("1. 새 테이블 생성 중...")
|
|
|
|
# Franchise 테이블
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS franchise (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
code TEXT NOT NULL UNIQUE,
|
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
print(" ✓ franchise 테이블 생성")
|
|
|
|
# Store 테이블
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS store (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
franchise_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
code TEXT NOT NULL UNIQUE,
|
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (franchise_id) REFERENCES franchise (id)
|
|
)
|
|
""")
|
|
print(" ✓ store 테이블 생성")
|
|
|
|
# Users 테이블
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
franchise_id INTEGER,
|
|
store_id INTEGER,
|
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (franchise_id) REFERENCES franchise (id),
|
|
FOREIGN KEY (store_id) REFERENCES store (id)
|
|
)
|
|
""")
|
|
print(" ✓ users 테이블 생성")
|
|
|
|
# 2. 기본 데이터 생성
|
|
print("\n2. 기본 데이터 생성 중...")
|
|
|
|
# 기본 프랜차이즈 생성
|
|
cursor.execute("""
|
|
INSERT OR IGNORE INTO franchise (id, name, code, is_active)
|
|
VALUES (1, '본사', 'HQ', 1)
|
|
""")
|
|
print(" ✓ 기본 프랜차이즈 생성 (ID: 1, 코드: HQ, 이름: 본사)")
|
|
|
|
# 기본 매장 생성
|
|
cursor.execute("""
|
|
INSERT OR IGNORE INTO store (id, franchise_id, name, code, is_active)
|
|
VALUES (1, 1, '1호점', 'S001', 1)
|
|
""")
|
|
print(" ✓ 기본 매장 생성 (ID: 1, 코드: S001, 이름: 1호점)")
|
|
|
|
# 기본 관리자 생성
|
|
admin_password_hash = hash_password("admin123")
|
|
cursor.execute("""
|
|
INSERT OR IGNORE INTO users (username, password_hash, role, franchise_id, is_active)
|
|
VALUES ('admin', ?, 'franchise_admin', 1, 1)
|
|
""", (admin_password_hash,))
|
|
print(" ✓ 기본 관리자 생성 (username: admin, password: admin123, role: franchise_admin)")
|
|
|
|
# 3. 기존 테이블에 store_id 컬럼 추가
|
|
print("\n3. 기존 테이블에 store_id 컬럼 추가 중...")
|
|
|
|
tables_to_migrate = [
|
|
"store_settings",
|
|
"daily_closing",
|
|
"class_info",
|
|
"members",
|
|
"waiting_list",
|
|
"class_closure",
|
|
"waiting_history"
|
|
]
|
|
|
|
for table_name in tables_to_migrate:
|
|
# 컬럼 존재 여부 확인
|
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
columns = [col[1] for col in cursor.fetchall()]
|
|
|
|
if 'store_id' not in columns:
|
|
# store_id 컬럼 추가 (nullable로 먼저 추가)
|
|
cursor.execute(f"""
|
|
ALTER TABLE {table_name}
|
|
ADD COLUMN store_id INTEGER
|
|
""")
|
|
print(f" ✓ {table_name} 테이블에 store_id 컬럼 추가")
|
|
else:
|
|
print(f" - {table_name} 테이블은 이미 store_id 컬럼이 있음")
|
|
|
|
# 4. 기존 데이터를 1호점에 연결
|
|
print("\n4. 기존 데이터를 1호점(ID: 1)에 연결 중...")
|
|
|
|
for table_name in tables_to_migrate:
|
|
cursor.execute(f"""
|
|
UPDATE {table_name}
|
|
SET store_id = 1
|
|
WHERE store_id IS NULL
|
|
""")
|
|
updated_count = cursor.rowcount
|
|
print(f" ✓ {table_name}: {updated_count}개 레코드 업데이트")
|
|
|
|
# 5. SQLite는 ALTER TABLE로 NOT NULL 제약 조건을 추가할 수 없으므로
|
|
# 새 테이블을 만들고 데이터를 복사하는 방식으로 처리해야 하지만,
|
|
# 단순화를 위해 현재 단계에서는 생략 (애플리케이션 레벨에서 검증)
|
|
print("\n5. 제약 조건 처리...")
|
|
print(" ⚠ SQLite 제한으로 NOT NULL 제약 조건은 애플리케이션 레벨에서 적용됨")
|
|
|
|
# daily_closing의 unique 제약 조건 수정
|
|
# business_date만 unique였던 것을 (store_id, business_date) 조합으로 변경
|
|
print("\n6. daily_closing 테이블 unique 제약 조건 수정 중...")
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS daily_closing_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
store_id INTEGER NOT NULL,
|
|
business_date DATE NOT NULL,
|
|
opening_time TIMESTAMP,
|
|
closing_time TIMESTAMP,
|
|
is_closed BOOLEAN NOT NULL DEFAULT 0,
|
|
total_waiting INTEGER NOT NULL DEFAULT 0,
|
|
total_attended INTEGER NOT NULL DEFAULT 0,
|
|
total_cancelled INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (store_id) REFERENCES store (id),
|
|
UNIQUE (store_id, business_date)
|
|
)
|
|
""")
|
|
|
|
cursor.execute("""
|
|
INSERT INTO daily_closing_new
|
|
SELECT * FROM daily_closing
|
|
""")
|
|
|
|
cursor.execute("DROP TABLE daily_closing")
|
|
cursor.execute("ALTER TABLE daily_closing_new RENAME TO daily_closing")
|
|
print(" ✓ daily_closing 테이블 재생성 완료")
|
|
|
|
conn.commit()
|
|
print("\n✅ 마이그레이션 완료!")
|
|
print(f"\n기본 로그인 정보:")
|
|
print(f" Username: admin")
|
|
print(f" Password: admin123")
|
|
print(f"\n⚠ 보안을 위해 첫 로그인 후 비밀번호를 변경하세요!")
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
print(f"\n❌ 마이그레이션 실패: {e}")
|
|
print(f"\n백업 파일로 복원하려면 다음 명령을 실행하세요:")
|
|
print(f" cp {backup_path} {db_path}")
|
|
raise
|
|
finally:
|
|
conn.close()
|
|
|
|
if __name__ == "__main__":
|
|
migrate()
|