Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
✨ 주요 기능: - 완전한 데이터베이스 스키마 분석 및 자동 마이그레이션 시스템 - 44개 테이블 완전 지원 (운영 서버 43개 + 1개 추가) - 누락된 테이블/컬럼 자동 감지 및 생성 🔧 해결된 스키마 문제: - users.status 컬럼 누락 → 자동 추가 - files 테이블 4개 컬럼 누락 → 자동 추가 - materials 테이블 22개 컬럼 누락 → 자동 추가 - support_details, purchase_requests, purchase_request_items 테이블 누락 → 자동 생성 - material_purchase_tracking.description, purchase_status 컬럼 누락 → 자동 추가 🚀 자동화 도구: - schema_analyzer.py: 코드와 DB 스키마 비교 분석 - auto_migrator.py: 자동 마이그레이션 실행 - docker_migrator.py: Docker 환경용 간편 마이그레이션 - schema_monitor.py: 실시간 스키마 모니터링 📋 리비전 관리 시스템: - 8개 카테고리별 리비전 페이지 구현 - PIPE Cutting Plan 관리 시스템 - PIPE Issue Management 시스템 - 완전한 리비전 비교 및 추적 기능 🎯 사용법: docker exec tk-mp-backend python3 scripts/docker_migrator.py 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
279 lines
9.4 KiB
Python
279 lines
9.4 KiB
Python
#!/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()
|