🔧 완전한 스키마 자동화 시스템 구축
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
This commit is contained in:
199
backend/scripts/auto_migrator.py
Normal file
199
backend/scripts/auto_migrator.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
자동 마이그레이션 실행기 - 스키마 분석 결과를 바탕으로 자동으로 DB를 업데이트
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
from schema_analyzer import SchemaAnalyzer
|
||||
|
||||
class AutoMigrator:
|
||||
def __init__(self):
|
||||
self.db_config = {
|
||||
'host': 'localhost',
|
||||
'port': 5432,
|
||||
'database': 'tk_mp_bom',
|
||||
'user': 'tkmp_user',
|
||||
'password': 'tkmp_password'
|
||||
}
|
||||
self.analyzer = SchemaAnalyzer()
|
||||
self.migration_log = []
|
||||
|
||||
def run_full_analysis_and_migration(self):
|
||||
"""전체 분석 및 마이그레이션 실행"""
|
||||
print("🚀 자동 마이그레이션 시작")
|
||||
|
||||
# 1. 스키마 분석
|
||||
print("\n1️⃣ 스키마 분석 중...")
|
||||
self.analyzer.analyze_code_models()
|
||||
self.analyzer.analyze_db_schema()
|
||||
analysis_result = self.analyzer.compare_schemas()
|
||||
|
||||
# 2. 분석 결과 확인
|
||||
if not analysis_result['missing_tables'] and not analysis_result['missing_columns']:
|
||||
print("✅ 스키마가 이미 최신 상태입니다!")
|
||||
return True
|
||||
|
||||
# 3. 마이그레이션 실행
|
||||
print("\n2️⃣ 마이그레이션 실행 중...")
|
||||
success = self._execute_migrations(analysis_result)
|
||||
|
||||
# 4. 결과 로깅
|
||||
self._log_migration_result(success, analysis_result)
|
||||
|
||||
return success
|
||||
|
||||
def _execute_migrations(self, analysis_result) -> bool:
|
||||
"""마이그레이션 실행"""
|
||||
try:
|
||||
conn = psycopg2.connect(**self.db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 트랜잭션 시작
|
||||
conn.autocommit = False
|
||||
|
||||
# 1. 누락된 테이블 생성
|
||||
for table_name in analysis_result['missing_tables']:
|
||||
success = self._create_missing_table(cursor, table_name)
|
||||
if not success:
|
||||
conn.rollback()
|
||||
return False
|
||||
|
||||
# 2. 누락된 컬럼 추가
|
||||
for missing_col in analysis_result['missing_columns']:
|
||||
success = self._add_missing_column(cursor, missing_col)
|
||||
if not success:
|
||||
conn.rollback()
|
||||
return False
|
||||
|
||||
# 커밋
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print("✅ 마이그레이션 성공!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 마이그레이션 실패: {e}")
|
||||
if 'conn' in locals():
|
||||
conn.rollback()
|
||||
return False
|
||||
|
||||
def _create_missing_table(self, cursor, table_name: str) -> bool:
|
||||
"""누락된 테이블 생성"""
|
||||
try:
|
||||
create_sql = self.analyzer._generate_create_table_sql(table_name)
|
||||
print(f" 📋 테이블 생성: {table_name}")
|
||||
cursor.execute(create_sql)
|
||||
|
||||
self.migration_log.append({
|
||||
'type': 'CREATE_TABLE',
|
||||
'table': table_name,
|
||||
'sql': create_sql,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 테이블 생성 실패 {table_name}: {e}")
|
||||
return False
|
||||
|
||||
def _add_missing_column(self, cursor, missing_col: dict) -> bool:
|
||||
"""누락된 컬럼 추가"""
|
||||
try:
|
||||
alter_sql = self.analyzer._generate_add_column_sql(missing_col)
|
||||
print(f" 🔧 컬럼 추가: {missing_col['table']}.{missing_col['column']}")
|
||||
cursor.execute(alter_sql)
|
||||
|
||||
self.migration_log.append({
|
||||
'type': 'ADD_COLUMN',
|
||||
'table': missing_col['table'],
|
||||
'column': missing_col['column'],
|
||||
'sql': alter_sql,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 컬럼 추가 실패 {missing_col['table']}.{missing_col['column']}: {e}")
|
||||
return False
|
||||
|
||||
def _log_migration_result(self, success: bool, analysis_result: dict):
|
||||
"""마이그레이션 결과 로깅"""
|
||||
log_entry = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'success': success,
|
||||
'analysis_result': analysis_result,
|
||||
'migration_log': self.migration_log
|
||||
}
|
||||
|
||||
# 로그 파일에 저장
|
||||
import json
|
||||
log_filename = f"migration_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
|
||||
with open(log_filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(log_entry, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"📄 마이그레이션 로그 저장: {log_filename}")
|
||||
|
||||
def fix_immediate_issues(self):
|
||||
"""즉시 해결이 필요한 문제들 수정"""
|
||||
print("🔧 즉시 해결 필요한 문제들 수정 중...")
|
||||
|
||||
immediate_fixes = [
|
||||
{
|
||||
'description': 'users 테이블에 status 컬럼 추가',
|
||||
'sql': "ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active'",
|
||||
'check_sql': "SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'status'"
|
||||
}
|
||||
]
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(**self.db_config)
|
||||
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
for fix in immediate_fixes:
|
||||
# 이미 존재하는지 확인
|
||||
cursor.execute(fix['check_sql'])
|
||||
if cursor.fetchone():
|
||||
print(f" ✅ 이미 존재: {fix['description']}")
|
||||
continue
|
||||
|
||||
# 수정 실행
|
||||
cursor.execute(fix['sql'])
|
||||
conn.commit()
|
||||
print(f" 🔧 수정 완료: {fix['description']}")
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 즉시 수정 실패: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
migrator = AutoMigrator()
|
||||
|
||||
# 1. 즉시 해결 필요한 문제들 수정
|
||||
migrator.fix_immediate_issues()
|
||||
|
||||
# 2. 전체 분석 및 마이그레이션
|
||||
success = migrator.run_full_analysis_and_migration()
|
||||
|
||||
if success:
|
||||
print("\n🎉 모든 마이그레이션이 성공적으로 완료되었습니다!")
|
||||
else:
|
||||
print("\n💥 마이그레이션 중 오류가 발생했습니다. 로그를 확인해주세요.")
|
||||
|
||||
return success
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
405
backend/scripts/docker_migrator.py
Normal file
405
backend/scripts/docker_migrator.py
Normal file
@@ -0,0 +1,405 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Docker 환경용 마이그레이션 스크립트
|
||||
컨테이너 내부에서 실행되는 간단한 마이그레이션 도구
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
|
||||
class DockerMigrator:
|
||||
def __init__(self):
|
||||
# Docker 환경의 DB 연결 설정
|
||||
self.db_config = {
|
||||
'host': os.getenv('DB_HOST', 'tk-mp-postgres'),
|
||||
'port': int(os.getenv('DB_PORT', 5432)),
|
||||
'database': os.getenv('DB_NAME', 'tk_mp_bom'),
|
||||
'user': os.getenv('DB_USER', 'tkmp_user'),
|
||||
'password': os.getenv('DB_PASSWORD', 'tkmp_password_2025')
|
||||
}
|
||||
|
||||
def check_and_fix_schema(self):
|
||||
"""스키마 체크 및 수정"""
|
||||
print("🔍 스키마 체크 시작...")
|
||||
|
||||
fixes_applied = []
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(**self.db_config)
|
||||
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 1. users.status 컬럼 체크 및 추가
|
||||
if self._check_and_add_users_status(cursor):
|
||||
fixes_applied.append("users.status 컬럼 추가")
|
||||
|
||||
# 2. files 테이블 누락 컬럼들 체크 및 추가
|
||||
if self._check_and_add_files_columns(cursor):
|
||||
fixes_applied.append("files 테이블 누락 컬럼들 추가")
|
||||
|
||||
# 3. materials 테이블 누락 컬럼들 체크 및 추가
|
||||
if self._check_and_add_materials_columns(cursor):
|
||||
fixes_applied.append("materials 테이블 누락 컬럼들 추가")
|
||||
|
||||
# 3.5. material_purchase_tracking 테이블 누락 컬럼들 체크 및 추가
|
||||
if self._check_and_add_mpt_columns(cursor):
|
||||
fixes_applied.append("material_purchase_tracking 테이블 누락 컬럼들 추가")
|
||||
|
||||
# 4. 누락된 상세 테이블들 체크 및 생성
|
||||
if self._check_and_create_detail_tables(cursor):
|
||||
fixes_applied.append("누락된 상세 테이블들 생성")
|
||||
|
||||
# 5. 기타 필요한 수정사항들...
|
||||
# 향후 추가될 수 있는 다른 스키마 수정사항들
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if fixes_applied:
|
||||
print("✅ 스키마 수정 완료:")
|
||||
for fix in fixes_applied:
|
||||
print(f" - {fix}")
|
||||
else:
|
||||
print("✅ 스키마가 이미 최신 상태입니다.")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 스키마 체크 실패: {e}")
|
||||
return False
|
||||
|
||||
def _check_and_add_users_status(self, cursor) -> bool:
|
||||
"""users.status 컬럼 체크 및 추가"""
|
||||
try:
|
||||
# status 컬럼이 존재하는지 확인
|
||||
cursor.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'status'
|
||||
""")
|
||||
|
||||
if cursor.fetchone():
|
||||
return False # 이미 존재함
|
||||
|
||||
# status 컬럼 추가
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active'")
|
||||
|
||||
# 기존 사용자들의 status를 'active'로 설정
|
||||
cursor.execute("UPDATE users SET status = 'active' WHERE status IS NULL")
|
||||
|
||||
print(" 🔧 users.status 컬럼 추가됨")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ users.status 컬럼 추가 실패: {e}")
|
||||
return False
|
||||
|
||||
def _check_and_add_files_columns(self, cursor) -> bool:
|
||||
"""files 테이블 누락 컬럼들 체크 및 추가"""
|
||||
try:
|
||||
# 필요한 컬럼들과 그 정의
|
||||
required_columns = {
|
||||
'job_no': 'VARCHAR(100)',
|
||||
'bom_name': 'VARCHAR(255)',
|
||||
'description': 'TEXT',
|
||||
'parsed_count': 'INTEGER'
|
||||
}
|
||||
|
||||
added_columns = []
|
||||
|
||||
for column_name, column_type in required_columns.items():
|
||||
# 컬럼이 존재하는지 확인
|
||||
cursor.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'files' AND column_name = %s
|
||||
""", (column_name,))
|
||||
|
||||
if not cursor.fetchone():
|
||||
# 컬럼 추가
|
||||
cursor.execute(f"ALTER TABLE files ADD COLUMN {column_name} {column_type}")
|
||||
added_columns.append(column_name)
|
||||
print(f" 🔧 files.{column_name} 컬럼 추가됨")
|
||||
|
||||
return len(added_columns) > 0
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ files 테이블 컬럼 추가 실패: {e}")
|
||||
return False
|
||||
|
||||
def _check_and_add_materials_columns(self, cursor) -> bool:
|
||||
"""materials 테이블 누락 컬럼들 체크 및 추가 (테스팅 서버 기준)"""
|
||||
try:
|
||||
# 테스팅 서버 기준 필요한 컬럼들
|
||||
required_columns = {
|
||||
# 사이즈 정보
|
||||
'main_nom': 'VARCHAR(50)',
|
||||
'red_nom': 'VARCHAR(50)',
|
||||
'row_number': 'INTEGER',
|
||||
|
||||
# 재질 정보
|
||||
'full_material_grade': 'VARCHAR(100)',
|
||||
'standard': 'VARCHAR(100)',
|
||||
'grade': 'VARCHAR(100)',
|
||||
'subcategory': 'VARCHAR(100)',
|
||||
|
||||
# 사용자 입력 정보
|
||||
'brand': 'VARCHAR(100)',
|
||||
'user_requirement': 'TEXT',
|
||||
|
||||
# 메타데이터
|
||||
'material_hash': 'VARCHAR(64)',
|
||||
'classified_by': 'VARCHAR(100)',
|
||||
'updated_by': 'VARCHAR(100)',
|
||||
'revision_status': 'VARCHAR(20) DEFAULT \'current\'',
|
||||
|
||||
# 추가 필드들
|
||||
'length': 'NUMERIC(10,3)',
|
||||
'total_length': 'NUMERIC(10,3)',
|
||||
'is_active': 'BOOLEAN DEFAULT true',
|
||||
'purchase_confirmed': 'BOOLEAN DEFAULT false',
|
||||
'confirmed_quantity': 'NUMERIC(10,3)',
|
||||
'purchase_status': 'VARCHAR(20)',
|
||||
'purchase_confirmed_by': 'VARCHAR(100)',
|
||||
'purchase_confirmed_at': 'TIMESTAMP',
|
||||
'normalized_description': 'TEXT'
|
||||
}
|
||||
|
||||
added_columns = []
|
||||
|
||||
for column_name, column_type in required_columns.items():
|
||||
# 컬럼이 존재하는지 확인
|
||||
cursor.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'materials' AND column_name = %s
|
||||
""", (column_name,))
|
||||
|
||||
if not cursor.fetchone():
|
||||
# 컬럼 추가
|
||||
cursor.execute(f"ALTER TABLE materials ADD COLUMN {column_name} {column_type}")
|
||||
added_columns.append(column_name)
|
||||
print(f" 🔧 materials.{column_name} 컬럼 추가됨")
|
||||
|
||||
if added_columns:
|
||||
print(f" ✅ materials 테이블에 {len(added_columns)}개 컬럼 추가 완료")
|
||||
|
||||
return len(added_columns) > 0
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ materials 테이블 컬럼 추가 실패: {e}")
|
||||
return False
|
||||
|
||||
def _check_and_add_mpt_columns(self, cursor) -> bool:
|
||||
"""material_purchase_tracking 테이블 누락 컬럼들 체크 및 추가"""
|
||||
try:
|
||||
# 필요한 컬럼들
|
||||
required_columns = {
|
||||
'description': 'TEXT',
|
||||
'purchase_status': 'VARCHAR(20)'
|
||||
}
|
||||
|
||||
added_columns = []
|
||||
|
||||
for column_name, column_type in required_columns.items():
|
||||
# 컬럼이 존재하는지 확인
|
||||
cursor.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'material_purchase_tracking' AND column_name = %s
|
||||
""", (column_name,))
|
||||
|
||||
if not cursor.fetchone():
|
||||
# 컬럼 추가
|
||||
cursor.execute(f"ALTER TABLE material_purchase_tracking ADD COLUMN {column_name} {column_type}")
|
||||
added_columns.append(column_name)
|
||||
print(f" 🔧 material_purchase_tracking.{column_name} 컬럼 추가됨")
|
||||
|
||||
if added_columns:
|
||||
print(f" ✅ material_purchase_tracking 테이블에 {len(added_columns)}개 컬럼 추가 완료")
|
||||
|
||||
return len(added_columns) > 0
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ material_purchase_tracking 테이블 컬럼 추가 실패: {e}")
|
||||
return False
|
||||
|
||||
def _check_and_create_detail_tables(self, cursor) -> bool:
|
||||
"""누락된 상세 테이블들 체크 및 생성"""
|
||||
try:
|
||||
# 필요한 상세 테이블들과 그 구조
|
||||
required_tables = {
|
||||
'support_details': """
|
||||
CREATE TABLE support_details (
|
||||
id SERIAL PRIMARY KEY,
|
||||
material_id INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
support_type VARCHAR(50),
|
||||
support_subtype VARCHAR(50),
|
||||
load_rating VARCHAR(50),
|
||||
load_capacity VARCHAR(50),
|
||||
material_standard VARCHAR(100),
|
||||
material_grade VARCHAR(100),
|
||||
pipe_size VARCHAR(50),
|
||||
length_mm NUMERIC(10,3),
|
||||
width_mm NUMERIC(10,3),
|
||||
height_mm NUMERIC(10,3),
|
||||
classification_confidence DOUBLE PRECISION,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
'special_material_details': """
|
||||
CREATE TABLE special_material_details (
|
||||
id SERIAL PRIMARY KEY,
|
||||
material_id INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
special_type VARCHAR(50),
|
||||
special_subtype VARCHAR(50),
|
||||
material_standard VARCHAR(100),
|
||||
material_grade VARCHAR(100),
|
||||
size_spec VARCHAR(50),
|
||||
classification_confidence DOUBLE PRECISION,
|
||||
additional_info JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
|
||||
)
|
||||
""",
|
||||
'purchase_requests': """
|
||||
CREATE TABLE purchase_requests (
|
||||
request_id SERIAL PRIMARY KEY,
|
||||
request_no VARCHAR(50) UNIQUE,
|
||||
file_id INTEGER,
|
||||
job_no VARCHAR(50),
|
||||
category VARCHAR(50),
|
||||
material_count INTEGER,
|
||||
excel_file_path VARCHAR(500),
|
||||
requested_by INTEGER,
|
||||
requested_at TIMESTAMP,
|
||||
status VARCHAR(20) DEFAULT 'requested',
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (file_id) REFERENCES files(id),
|
||||
FOREIGN KEY (requested_by) REFERENCES users(user_id)
|
||||
)
|
||||
""",
|
||||
'purchase_request_items': """
|
||||
CREATE TABLE purchase_request_items (
|
||||
item_id SERIAL PRIMARY KEY,
|
||||
request_id INTEGER NOT NULL,
|
||||
material_id INTEGER NOT NULL,
|
||||
quantity INTEGER,
|
||||
unit VARCHAR(20),
|
||||
description TEXT,
|
||||
user_requirement TEXT,
|
||||
is_ordered BOOLEAN DEFAULT false,
|
||||
is_received BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (request_id) REFERENCES purchase_requests(request_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
||||
created_tables = []
|
||||
|
||||
for table_name, create_sql in required_tables.items():
|
||||
# 테이블이 존재하는지 확인
|
||||
cursor.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = %s
|
||||
)
|
||||
""", (table_name,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
if isinstance(result, dict):
|
||||
table_exists = result.get('exists', False)
|
||||
else:
|
||||
table_exists = result[0] if result else False
|
||||
if not table_exists:
|
||||
# 테이블 생성
|
||||
cursor.execute(create_sql)
|
||||
created_tables.append(table_name)
|
||||
print(f" 🏗️ {table_name} 테이블 생성됨")
|
||||
|
||||
if created_tables:
|
||||
print(f" ✅ {len(created_tables)}개 상세 테이블 생성 완료")
|
||||
|
||||
return len(created_tables) > 0
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 상세 테이블 생성 실패: {e}")
|
||||
import traceback
|
||||
print(f" 상세 오류: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
def verify_critical_tables(self):
|
||||
"""중요 테이블들이 존재하는지 확인"""
|
||||
print("🔍 중요 테이블 존재 여부 확인...")
|
||||
|
||||
critical_tables = [
|
||||
'users', 'projects', 'files', 'materials',
|
||||
'pipe_details', 'fitting_details', 'flange_details',
|
||||
'valve_details', 'gasket_details', 'bolt_details'
|
||||
]
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(**self.db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
missing_tables = []
|
||||
|
||||
for table in critical_tables:
|
||||
cursor.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = %s
|
||||
)
|
||||
""", (table,))
|
||||
|
||||
if not cursor.fetchone()[0]:
|
||||
missing_tables.append(table)
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if missing_tables:
|
||||
print("❌ 누락된 중요 테이블들:")
|
||||
for table in missing_tables:
|
||||
print(f" - {table}")
|
||||
return False
|
||||
else:
|
||||
print("✅ 모든 중요 테이블이 존재합니다.")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 테이블 확인 실패: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("🚀 Docker 환경 마이그레이션 시작")
|
||||
|
||||
migrator = DockerMigrator()
|
||||
|
||||
# 1. 중요 테이블 존재 여부 확인
|
||||
if not migrator.verify_critical_tables():
|
||||
print("💥 중요 테이블이 누락되어 있습니다!")
|
||||
return False
|
||||
|
||||
# 2. 스키마 체크 및 수정
|
||||
if not migrator.check_and_fix_schema():
|
||||
print("💥 스키마 수정 실패!")
|
||||
return False
|
||||
|
||||
print("🎉 마이그레이션 완료!")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
361
backend/scripts/schema_analyzer.py
Normal file
361
backend/scripts/schema_analyzer.py
Normal file
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
스키마 분석기 - 코드와 DB 스키마를 비교하여 누락된 테이블/컬럼을 찾는 도구
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
from typing import Dict, List, Set, Tuple
|
||||
from datetime import datetime
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
# 프로젝트 루트를 Python 경로에 추가
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
class SchemaAnalyzer:
|
||||
def __init__(self):
|
||||
self.db_config = {
|
||||
'host': 'localhost',
|
||||
'port': 5432,
|
||||
'database': 'tk_mp_bom',
|
||||
'user': 'tkmp_user',
|
||||
'password': 'tkmp_password'
|
||||
}
|
||||
self.code_tables = {}
|
||||
self.db_tables = {}
|
||||
self.analysis_result = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'missing_tables': [],
|
||||
'missing_columns': [],
|
||||
'extra_tables': [],
|
||||
'schema_issues': []
|
||||
}
|
||||
|
||||
def analyze_code_models(self) -> Dict[str, Dict]:
|
||||
"""코드에서 SQLAlchemy 모델을 분석하여 테이블 구조 추출"""
|
||||
print("📋 코드 모델 분석 중...")
|
||||
|
||||
model_files = [
|
||||
'backend/app/models.py',
|
||||
'backend/app/auth/models.py'
|
||||
]
|
||||
|
||||
for file_path in model_files:
|
||||
if os.path.exists(file_path):
|
||||
self._parse_model_file(file_path)
|
||||
|
||||
print(f"✅ 코드에서 {len(self.code_tables)}개 테이블 발견")
|
||||
return self.code_tables
|
||||
|
||||
def _parse_model_file(self, file_path: str):
|
||||
"""모델 파일을 파싱하여 테이블 정보 추출"""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 클래스 정의 찾기
|
||||
class_pattern = r'class\s+(\w+)\(Base\):(.*?)(?=class\s+\w+\(Base\):|$)'
|
||||
classes = re.findall(class_pattern, content, re.DOTALL)
|
||||
|
||||
for class_name, class_content in classes:
|
||||
table_info = self._parse_class_content(class_name, class_content)
|
||||
if table_info:
|
||||
table_name = table_info['table_name']
|
||||
self.code_tables[table_name] = table_info
|
||||
|
||||
def _parse_class_content(self, class_name: str, content: str) -> Dict:
|
||||
"""클래스 내용을 파싱하여 테이블 정보 추출"""
|
||||
# __tablename__ 찾기
|
||||
tablename_match = re.search(r'__tablename__\s*=\s*["\']([^"\']+)["\']', content)
|
||||
if not tablename_match:
|
||||
return None
|
||||
|
||||
table_name = tablename_match.group(1)
|
||||
|
||||
# 컬럼 정의 찾기
|
||||
column_pattern = r'(\w+)\s*=\s*Column\((.*?)\)'
|
||||
columns = {}
|
||||
|
||||
for match in re.finditer(column_pattern, content, re.DOTALL):
|
||||
column_name = match.group(1)
|
||||
column_def = match.group(2)
|
||||
|
||||
# 컬럼 타입과 속성 파싱
|
||||
column_info = self._parse_column_definition(column_def)
|
||||
columns[column_name] = column_info
|
||||
|
||||
return {
|
||||
'class_name': class_name,
|
||||
'table_name': table_name,
|
||||
'columns': columns,
|
||||
'file_path': None # 나중에 설정
|
||||
}
|
||||
|
||||
def _parse_column_definition(self, column_def: str) -> Dict:
|
||||
"""컬럼 정의를 파싱하여 타입과 속성 추출"""
|
||||
# 기본 타입 매핑
|
||||
type_mapping = {
|
||||
'Integer': 'integer',
|
||||
'String': 'character varying',
|
||||
'Text': 'text',
|
||||
'Boolean': 'boolean',
|
||||
'DateTime': 'timestamp without time zone',
|
||||
'Numeric': 'numeric',
|
||||
'JSON': 'json'
|
||||
}
|
||||
|
||||
# 타입 추출
|
||||
type_match = re.search(r'(Integer|String|Text|Boolean|DateTime|Numeric|JSON)', column_def)
|
||||
column_type = 'unknown'
|
||||
if type_match:
|
||||
sqlalchemy_type = type_match.group(1)
|
||||
column_type = type_mapping.get(sqlalchemy_type, sqlalchemy_type.lower())
|
||||
|
||||
# 속성 추출
|
||||
nullable = 'nullable=False' not in column_def
|
||||
primary_key = 'primary_key=True' in column_def
|
||||
unique = 'unique=True' in column_def
|
||||
default = None
|
||||
|
||||
# default 값 추출
|
||||
default_match = re.search(r'default=([^,)]+)', column_def)
|
||||
if default_match:
|
||||
default = default_match.group(1).strip()
|
||||
|
||||
return {
|
||||
'type': column_type,
|
||||
'nullable': nullable,
|
||||
'primary_key': primary_key,
|
||||
'unique': unique,
|
||||
'default': default
|
||||
}
|
||||
|
||||
def analyze_db_schema(self) -> Dict[str, Dict]:
|
||||
"""실제 DB 스키마 분석"""
|
||||
print("🗄️ DB 스키마 분석 중...")
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(**self.db_config)
|
||||
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 테이블 목록 조회
|
||||
cursor.execute("""
|
||||
SELECT tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename
|
||||
""")
|
||||
|
||||
tables = [row['tablename'] for row in cursor.fetchall()]
|
||||
|
||||
# 각 테이블의 컬럼 정보 조회
|
||||
for table_name in tables:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default,
|
||||
character_maximum_length
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = %s
|
||||
AND table_schema = 'public'
|
||||
ORDER BY ordinal_position
|
||||
""", (table_name,))
|
||||
|
||||
columns = {}
|
||||
for row in cursor.fetchall():
|
||||
columns[row['column_name']] = {
|
||||
'type': row['data_type'],
|
||||
'nullable': row['is_nullable'] == 'YES',
|
||||
'default': row['column_default'],
|
||||
'max_length': row['character_maximum_length']
|
||||
}
|
||||
|
||||
self.db_tables[table_name] = {
|
||||
'table_name': table_name,
|
||||
'columns': columns
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print(f"✅ DB에서 {len(self.db_tables)}개 테이블 발견")
|
||||
return self.db_tables
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ DB 연결 실패: {e}")
|
||||
return {}
|
||||
|
||||
def compare_schemas(self) -> Dict:
|
||||
"""코드와 DB 스키마 비교"""
|
||||
print("🔍 스키마 비교 중...")
|
||||
|
||||
code_table_names = set(self.code_tables.keys())
|
||||
db_table_names = set(self.db_tables.keys())
|
||||
|
||||
# 누락된 테이블 (코드에는 있지만 DB에는 없음)
|
||||
missing_tables = code_table_names - db_table_names
|
||||
self.analysis_result['missing_tables'] = list(missing_tables)
|
||||
|
||||
# 추가 테이블 (DB에는 있지만 코드에는 없음)
|
||||
extra_tables = db_table_names - code_table_names
|
||||
self.analysis_result['extra_tables'] = list(extra_tables)
|
||||
|
||||
# 공통 테이블의 컬럼 비교
|
||||
common_tables = code_table_names & db_table_names
|
||||
|
||||
for table_name in common_tables:
|
||||
missing_columns = self._compare_table_columns(table_name)
|
||||
if missing_columns:
|
||||
self.analysis_result['missing_columns'].extend(missing_columns)
|
||||
|
||||
return self.analysis_result
|
||||
|
||||
def _compare_table_columns(self, table_name: str) -> List[Dict]:
|
||||
"""특정 테이블의 컬럼 비교"""
|
||||
code_columns = set(self.code_tables[table_name]['columns'].keys())
|
||||
db_columns = set(self.db_tables[table_name]['columns'].keys())
|
||||
|
||||
missing_columns = []
|
||||
|
||||
# 누락된 컬럼들
|
||||
for column_name in code_columns - db_columns:
|
||||
column_info = self.code_tables[table_name]['columns'][column_name]
|
||||
missing_columns.append({
|
||||
'table': table_name,
|
||||
'column': column_name,
|
||||
'type': column_info['type'],
|
||||
'nullable': column_info['nullable'],
|
||||
'default': column_info['default']
|
||||
})
|
||||
|
||||
return missing_columns
|
||||
|
||||
def generate_migration_sql(self) -> str:
|
||||
"""누락된 스키마에 대한 마이그레이션 SQL 생성"""
|
||||
sql_statements = []
|
||||
sql_statements.append("-- 자동 생성된 마이그레이션 SQL")
|
||||
sql_statements.append(f"-- 생성 시간: {datetime.now()}")
|
||||
sql_statements.append("")
|
||||
|
||||
# 누락된 테이블 생성
|
||||
for table_name in self.analysis_result['missing_tables']:
|
||||
if table_name in self.code_tables:
|
||||
create_sql = self._generate_create_table_sql(table_name)
|
||||
sql_statements.append(create_sql)
|
||||
sql_statements.append("")
|
||||
|
||||
# 누락된 컬럼 추가
|
||||
for missing_col in self.analysis_result['missing_columns']:
|
||||
alter_sql = self._generate_add_column_sql(missing_col)
|
||||
sql_statements.append(alter_sql)
|
||||
|
||||
return "\n".join(sql_statements)
|
||||
|
||||
def _generate_create_table_sql(self, table_name: str) -> str:
|
||||
"""테이블 생성 SQL 생성"""
|
||||
table_info = self.code_tables[table_name]
|
||||
columns = []
|
||||
|
||||
for col_name, col_info in table_info['columns'].items():
|
||||
col_def = f" {col_name} {col_info['type']}"
|
||||
|
||||
if not col_info['nullable']:
|
||||
col_def += " NOT NULL"
|
||||
|
||||
if col_info['default']:
|
||||
col_def += f" DEFAULT {col_info['default']}"
|
||||
|
||||
if col_info['primary_key']:
|
||||
col_def += " PRIMARY KEY"
|
||||
|
||||
columns.append(col_def)
|
||||
|
||||
return f"CREATE TABLE {table_name} (\n" + ",\n".join(columns) + "\n);"
|
||||
|
||||
def _generate_add_column_sql(self, missing_col: Dict) -> str:
|
||||
"""컬럼 추가 SQL 생성"""
|
||||
sql = f"ALTER TABLE {missing_col['table']} ADD COLUMN {missing_col['column']} {missing_col['type']}"
|
||||
|
||||
if missing_col['default']:
|
||||
sql += f" DEFAULT {missing_col['default']}"
|
||||
|
||||
if not missing_col['nullable']:
|
||||
sql += " NOT NULL"
|
||||
|
||||
return sql + ";"
|
||||
|
||||
def save_analysis_report(self, filename: str = "schema_analysis_report.json"):
|
||||
"""분석 결과를 JSON 파일로 저장"""
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.analysis_result, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"📄 분석 보고서 저장: {filename}")
|
||||
|
||||
def print_summary(self):
|
||||
"""분석 결과 요약 출력"""
|
||||
print("\n" + "="*60)
|
||||
print("📊 스키마 분석 결과 요약")
|
||||
print("="*60)
|
||||
|
||||
print(f"🔍 분석 시간: {self.analysis_result['timestamp']}")
|
||||
print(f"📋 코드 테이블: {len(self.code_tables)}개")
|
||||
print(f"🗄️ DB 테이블: {len(self.db_tables)}개")
|
||||
|
||||
if self.analysis_result['missing_tables']:
|
||||
print(f"\n❌ 누락된 테이블 ({len(self.analysis_result['missing_tables'])}개):")
|
||||
for table in self.analysis_result['missing_tables']:
|
||||
print(f" - {table}")
|
||||
|
||||
if self.analysis_result['missing_columns']:
|
||||
print(f"\n❌ 누락된 컬럼 ({len(self.analysis_result['missing_columns'])}개):")
|
||||
for col in self.analysis_result['missing_columns']:
|
||||
print(f" - {col['table']}.{col['column']} ({col['type']})")
|
||||
|
||||
if self.analysis_result['extra_tables']:
|
||||
print(f"\n➕ 추가 테이블 ({len(self.analysis_result['extra_tables'])}개):")
|
||||
for table in self.analysis_result['extra_tables']:
|
||||
print(f" - {table}")
|
||||
|
||||
if not any([self.analysis_result['missing_tables'],
|
||||
self.analysis_result['missing_columns']]):
|
||||
print("\n✅ 스키마가 완전히 동기화되어 있습니다!")
|
||||
|
||||
def main():
|
||||
print("🚀 TK-MP-Project 스키마 분석기 시작")
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
|
||||
# 1. 코드 모델 분석
|
||||
analyzer.analyze_code_models()
|
||||
|
||||
# 2. DB 스키마 분석
|
||||
analyzer.analyze_db_schema()
|
||||
|
||||
# 3. 스키마 비교
|
||||
analyzer.compare_schemas()
|
||||
|
||||
# 4. 결과 출력
|
||||
analyzer.print_summary()
|
||||
|
||||
# 5. 마이그레이션 SQL 생성
|
||||
if analyzer.analysis_result['missing_tables'] or analyzer.analysis_result['missing_columns']:
|
||||
migration_sql = analyzer.generate_migration_sql()
|
||||
|
||||
with open('migration.sql', 'w', encoding='utf-8') as f:
|
||||
f.write(migration_sql)
|
||||
|
||||
print(f"\n📝 마이그레이션 SQL 생성: migration.sql")
|
||||
print("다음 명령으로 실행 가능:")
|
||||
print("docker exec tk-mp-postgres psql -U tkmp_user -d tk_mp_bom -f /path/to/migration.sql")
|
||||
|
||||
# 6. 분석 보고서 저장
|
||||
analyzer.save_analysis_report()
|
||||
|
||||
return analyzer.analysis_result
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
278
backend/scripts/schema_monitor.py
Normal file
278
backend/scripts/schema_monitor.py
Normal file
@@ -0,0 +1,278 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user