#!/usr/bin/env python3 """ 스키마 모니터링 시스템 - 코드 변경사항을 감지하고 스키마 분석을 자동으로 실행 """ import os import sys import time import json from datetime import datetime from typing import Dict, List from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from schema_analyzer import SchemaAnalyzer from auto_migrator import AutoMigrator class SchemaMonitor(FileSystemEventHandler): def __init__(self): self.analyzer = SchemaAnalyzer() self.migrator = AutoMigrator() self.last_check = datetime.now() self.monitored_files = [ 'backend/app/models.py', 'backend/app/auth/models.py' ] self.change_log = [] def on_modified(self, event): """파일 변경 감지""" if event.is_directory: return # 모니터링 대상 파일인지 확인 file_path = event.src_path.replace('\\', '/') if not any(monitored in file_path for monitored in self.monitored_files): return print(f"📝 파일 변경 감지: {file_path}") # 변경 로그 기록 self.change_log.append({ 'file': file_path, 'timestamp': datetime.now().isoformat(), 'event_type': 'modified' }) # 스키마 분석 실행 (디바운싱 적용) self._schedule_schema_check() def _schedule_schema_check(self): """스키마 체크 스케줄링 (디바운싱)""" # 마지막 체크로부터 5초 후에 실행 time.sleep(5) current_time = datetime.now() if (current_time - self.last_check).seconds >= 5: self.last_check = current_time self._run_schema_check() def _run_schema_check(self): """스키마 체크 실행""" print("\n🔍 스키마 변경사항 체크 중...") # 분석 실행 self.analyzer.analyze_code_models() self.analyzer.analyze_db_schema() analysis_result = self.analyzer.compare_schemas() # 변경사항이 있는지 확인 has_changes = ( analysis_result['missing_tables'] or analysis_result['missing_columns'] ) if has_changes: print("⚠️ 스키마 불일치 발견!") self.analyzer.print_summary() # 자동 마이그레이션 실행 여부 확인 self._handle_schema_changes(analysis_result) else: print("✅ 스키마가 동기화되어 있습니다.") def _handle_schema_changes(self, analysis_result: Dict): """스키마 변경사항 처리""" print("\n🤖 자동 마이그레이션을 실행하시겠습니까?") print("1. 자동 실행") print("2. SQL 파일만 생성") print("3. 무시") # 개발 환경에서는 자동으로 실행 choice = "1" # 자동 실행 if choice == "1": print("🚀 자동 마이그레이션 실행 중...") success = self.migrator._execute_migrations(analysis_result) if success: print("✅ 마이그레이션 완료!") self._notify_migration_success(analysis_result) else: print("❌ 마이그레이션 실패!") self._notify_migration_failure(analysis_result) elif choice == "2": migration_sql = self.analyzer.generate_migration_sql() filename = f"migration_{datetime.now().strftime('%Y%m%d_%H%M%S')}.sql" with open(filename, 'w', encoding='utf-8') as f: f.write(migration_sql) print(f"📝 마이그레이션 SQL 생성: {filename}") def _notify_migration_success(self, analysis_result: Dict): """마이그레이션 성공 알림""" notification = { 'type': 'MIGRATION_SUCCESS', 'timestamp': datetime.now().isoformat(), 'changes': { 'tables_created': len(analysis_result['missing_tables']), 'columns_added': len(analysis_result['missing_columns']) } } self._save_notification(notification) def _notify_migration_failure(self, analysis_result: Dict): """마이그레이션 실패 알림""" notification = { 'type': 'MIGRATION_FAILURE', 'timestamp': datetime.now().isoformat(), 'analysis_result': analysis_result } self._save_notification(notification) def _save_notification(self, notification: Dict): """알림 저장""" notifications_file = 'schema_notifications.json' notifications = [] if os.path.exists(notifications_file): with open(notifications_file, 'r', encoding='utf-8') as f: notifications = json.load(f) notifications.append(notification) # 최근 100개만 유지 notifications = notifications[-100:] with open(notifications_file, 'w', encoding='utf-8') as f: json.dump(notifications, f, indent=2, ensure_ascii=False) def start_monitoring(self): """모니터링 시작""" print("👀 스키마 모니터링 시작...") print("모니터링 대상 파일:") for file_path in self.monitored_files: print(f" - {file_path}") observer = Observer() observer.schedule(self, 'backend/app', recursive=True) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() print("\n🛑 모니터링 중단") observer.join() class SchemaValidator: """스키마 검증 도구""" def __init__(self): self.analyzer = SchemaAnalyzer() def validate_deployment_readiness(self) -> bool: """배포 준비 상태 검증""" print("🔍 배포 준비 상태 검증 중...") # 스키마 분석 self.analyzer.analyze_code_models() self.analyzer.analyze_db_schema() analysis_result = self.analyzer.compare_schemas() # 검증 결과 is_ready = not ( analysis_result['missing_tables'] or analysis_result['missing_columns'] ) if is_ready: print("✅ 배포 준비 완료! 스키마가 완전히 동기화되어 있습니다.") else: print("❌ 배포 준비 미완료! 스키마 불일치가 발견되었습니다.") self.analyzer.print_summary() return is_ready def generate_deployment_migration(self) -> str: """배포용 마이그레이션 스크립트 생성""" print("📝 배포용 마이그레이션 스크립트 생성 중...") self.analyzer.analyze_code_models() self.analyzer.analyze_db_schema() analysis_result = self.analyzer.compare_schemas() if not analysis_result['missing_tables'] and not analysis_result['missing_columns']: print("✅ 마이그레이션이 필요하지 않습니다.") return "" # 배포용 마이그레이션 스크립트 생성 migration_sql = self.analyzer.generate_migration_sql() # 안전성 체크 추가 safe_migration = self._add_safety_checks(migration_sql) filename = f"deployment_migration_{datetime.now().strftime('%Y%m%d_%H%M%S')}.sql" with open(filename, 'w', encoding='utf-8') as f: f.write(safe_migration) print(f"📄 배포용 마이그레이션 생성: {filename}") return filename def _add_safety_checks(self, migration_sql: str) -> str: """마이그레이션에 안전성 체크 추가""" safety_header = """-- 배포용 마이그레이션 스크립트 -- 생성 시간: {timestamp} -- 주의: 프로덕션 환경에서 실행하기 전에 백업을 수행하세요! -- 트랜잭션 시작 BEGIN; -- 백업 테이블 생성 (필요시) -- CREATE TABLE users_backup AS SELECT * FROM users; """.format(timestamp=datetime.now()) safety_footer = """ -- 검증 쿼리 (필요시 주석 해제) -- SELECT COUNT(*) FROM users WHERE status IS NOT NULL; -- 모든 것이 정상이면 커밋, 문제가 있으면 ROLLBACK 실행 COMMIT; -- ROLLBACK; """ return safety_header + migration_sql + safety_footer def main(): import argparse parser = argparse.ArgumentParser(description='TK-MP-Project 스키마 모니터링 도구') parser.add_argument('--mode', choices=['monitor', 'validate', 'deploy'], default='monitor', help='실행 모드') args = parser.parse_args() if args.mode == 'monitor': monitor = SchemaMonitor() monitor.start_monitoring() elif args.mode == 'validate': validator = SchemaValidator() is_ready = validator.validate_deployment_readiness() sys.exit(0 if is_ready else 1) elif args.mode == 'deploy': validator = SchemaValidator() migration_file = validator.generate_deployment_migration() if migration_file: print(f"배포 시 다음 명령으로 실행: psql -f {migration_file}") if __name__ == "__main__": main()