Files
TK-BOM-Project/backend/scripts/complete_migrate.py
Hyungi Ahn 3398f71b80
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
🔄 전반적인 시스템 리팩토링 완료
 백엔드 구조 개선:
- DatabaseService: 공통 DB 쿼리 로직 통합
- FileUploadService: 파일 업로드 로직 모듈화 및 트랜잭션 관리 개선
- 서비스 레이어 패턴 도입으로 코드 재사용성 향상

 프론트엔드 컴포넌트 개선:
- LoadingSpinner, ErrorMessage, ConfirmDialog 공통 컴포넌트 생성
- 재사용 가능한 컴포넌트 라이브러리 구축
- deprecated/backup 파일들 완전 제거

 성능 최적화:
- optimize_database.py: 핵심 DB 인덱스 자동 생성
- 쿼리 최적화 및 통계 업데이트 자동화
- VACUUM ANALYZE 자동 실행

 코드 정리:
- 개별 SQL 마이그레이션 파일들을 legacy/ 폴더로 정리
- 중복된 마이그레이션 스크립트 정리
- 깔끔하고 체계적인 프로젝트 구조 완성

 자동 마이그레이션 시스템 강화:
- complete_migrate.py: SQLAlchemy 기반 완전한 마이그레이션
- analyze_and_fix_schema.py: 백엔드 코드 분석 기반 스키마 수정
- fix_missing_tables.py: 누락된 테이블/컬럼 자동 생성
- start.sh: 배포 시 자동 실행 순서 최적화
2025-10-20 08:41:06 +09:00

302 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
TK-MP-Project 완전한 자동 DB 마이그레이션 시스템
- 모든 SQLAlchemy 모델을 기반으로 테이블 생성/업데이트
- 누락된 컬럼 자동 추가
- 인덱스 자동 생성
- 초기 데이터 삽입
- macOS Docker와 Synology Container Manager 모두 지원
"""
import os
import sys
import time
import psycopg2
from psycopg2 import OperationalError, sql
from sqlalchemy import create_engine, text, inspect
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import OperationalError as SQLAlchemyOperationalError
from datetime import datetime
import bcrypt
# 현재 디렉토리를 Python 경로에 추가
sys.path.insert(0, '/app')
from app.database import Base, get_db
from app.auth.models import User
from app.models import * # 모든 모델을 임포트하여 Base.metadata에 등록
# 환경 변수 로드
DB_HOST = os.getenv("DB_HOST", "postgres")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "tk_mp_bom")
DB_USER = os.getenv("DB_USER", "tkmp_user")
DB_PASSWORD = os.getenv("DB_PASSWORD", "tkmp_password_2025")
DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123")
ADMIN_NAME = os.getenv("ADMIN_NAME", "시스템 관리자")
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "admin@tkmp.com")
SYSTEM_USERNAME = os.getenv("SYSTEM_USERNAME", "system")
SYSTEM_PASSWORD = os.getenv("SYSTEM_PASSWORD", "admin123")
SYSTEM_NAME = os.getenv("SYSTEM_NAME", "시스템 계정")
SYSTEM_EMAIL = os.getenv("SYSTEM_EMAIL", "system@tkmp.com")
def wait_for_db(max_attempts=120, delay=2):
"""데이터베이스 연결 대기 - 더 긴 대기 시간과 상세한 로그"""
print(f"Waiting for database connection...")
print(f" Host: {DB_HOST}:{DB_PORT}")
print(f" Database: {DB_NAME}")
print(f" User: {DB_USER}")
for i in range(1, max_attempts + 1):
try:
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
connect_timeout=5
)
conn.close()
print(f"SUCCESS: Database connection established! ({i}/{max_attempts})")
return True
except OperationalError as e:
if i <= 5 or i % 10 == 0 or i == max_attempts:
print(f"Waiting for database... ({i}/{max_attempts}) - {str(e)[:100]}")
if i == max_attempts:
print(f"FAILED: Database connection timeout after {max_attempts * delay} seconds")
print(f"Error: {e}")
time.sleep(delay)
return False
def get_existing_columns(engine, table_name):
"""기존 테이블의 컬럼 목록 조회"""
inspector = inspect(engine)
try:
columns = inspector.get_columns(table_name)
return {col['name']: col for col in columns}
except Exception:
return {}
def get_model_columns(model):
"""SQLAlchemy 모델의 컬럼 정보 추출"""
columns = {}
for column in model.__table__.columns:
columns[column.name] = {
'name': column.name,
'type': str(column.type),
'nullable': column.nullable,
'default': column.default,
'primary_key': column.primary_key
}
return columns
def add_missing_columns(engine, model):
"""누락된 컬럼을 기존 테이블에 추가"""
table_name = model.__tablename__
existing_columns = get_existing_columns(engine, table_name)
model_columns = get_model_columns(model)
missing_columns = []
for col_name, col_info in model_columns.items():
if col_name not in existing_columns:
missing_columns.append((col_name, col_info))
if not missing_columns:
return True
print(f"📝 테이블 '{table_name}'에 누락된 컬럼 {len(missing_columns)}개 추가 중...")
try:
with engine.connect() as connection:
for col_name, col_info in missing_columns:
# 컬럼 타입 매핑
col_type = col_info['type']
if 'VARCHAR' in col_type:
sql_type = col_type
elif 'INTEGER' in col_type:
sql_type = 'INTEGER'
elif 'BOOLEAN' in col_type:
sql_type = 'BOOLEAN'
elif 'DATETIME' in col_type:
sql_type = 'TIMESTAMP'
elif 'TEXT' in col_type:
sql_type = 'TEXT'
elif 'NUMERIC' in col_type:
sql_type = col_type
elif 'JSON' in col_type:
sql_type = 'JSON'
else:
sql_type = 'TEXT' # 기본값
# NULL 허용 여부
nullable = "NULL" if col_info['nullable'] else "NOT NULL"
# 기본값 설정
default_clause = ""
if col_info['default'] is not None:
if col_info['type'] == 'BOOLEAN':
default_value = 'TRUE' if str(col_info['default']).lower() in ['true', '1'] else 'FALSE'
default_clause = f" DEFAULT {default_value}"
elif 'VARCHAR' in col_info['type'] or 'TEXT' in col_info['type']:
default_clause = f" DEFAULT '{col_info['default']}'"
else:
default_clause = f" DEFAULT {col_info['default']}"
alter_sql = f"ALTER TABLE {table_name} ADD COLUMN IF NOT EXISTS {col_name} {sql_type}{default_clause} {nullable};"
try:
connection.execute(text(alter_sql))
print(f" ✅ 컬럼 '{col_name}' ({sql_type}) 추가 완료")
except Exception as e:
print(f" ⚠️ 컬럼 '{col_name}' 추가 실패: {e}")
connection.commit()
return True
except Exception as e:
print(f"❌ 테이블 '{table_name}' 컬럼 추가 실패: {e}")
return False
def create_tables_and_migrate():
"""테이블 생성 및 마이그레이션"""
print("🔄 완전한 스키마 동기화 및 마이그레이션 시작...")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
try:
# 1. 모든 테이블 생성 (존재하지 않으면 생성)
print("📋 새 테이블 생성 중...")
Base.metadata.create_all(bind=engine)
print("✅ 새 테이블 생성 완료")
# 2. 기존 테이블에 누락된 컬럼 추가
print("🔧 기존 테이블 컬럼 동기화 중...")
# 모든 모델에 대해 컬럼 동기화
models_to_check = [
User, Project, File, Material, MaterialStandard,
MaterialCategory, MaterialSpecification, MaterialGrade,
MaterialPattern, SpecialMaterial, SpecialMaterialGrade,
SpecialMaterialPattern, PipeDetail, RequirementType,
UserRequirement, TubingCategory, TubingSpecification,
TubingManufacturer, TubingProduct, MaterialTubingMapping,
SupportDetails, PurchaseRequestItems, FittingDetails,
FlangeDetails, ValveDetails, GasketDetails, BoltDetails,
InstrumentDetails, PurchaseRequests, Jobs, PipeEndPreparations,
MaterialPurchaseTracking, ExcelExports, UserActivityLogs,
ExcelExportHistory, ExportedMaterials, PurchaseStatusHistory
]
for model in models_to_check:
if hasattr(model, '__tablename__'):
add_missing_columns(engine, model)
print("✅ 모든 테이블 컬럼 동기화 완료")
# 3. 초기 사용자 데이터 생성
with SessionLocal() as db:
# 관리자 계정 확인 및 생성
admin_user = db.query(User).filter(User.username == ADMIN_USERNAME).first()
if not admin_user:
hashed_password = bcrypt.hashpw(ADMIN_PASSWORD.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
admin_user = User(
username=ADMIN_USERNAME,
password=hashed_password,
name=ADMIN_NAME,
email=ADMIN_EMAIL,
role='admin',
access_level='admin',
department='IT',
position='시스템 관리자',
status='active'
)
db.add(admin_user)
print(f" 관리자 계정 '{ADMIN_USERNAME}' 생성 완료")
else:
print(f"☑️ 관리자 계정 '{ADMIN_USERNAME}' 이미 존재")
# 시스템 계정 확인 및 생성
system_user = db.query(User).filter(User.username == SYSTEM_USERNAME).first()
if not system_user:
hashed_password = bcrypt.hashpw(SYSTEM_PASSWORD.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
system_user = User(
username=SYSTEM_USERNAME,
password=hashed_password,
name=SYSTEM_NAME,
email=SYSTEM_EMAIL,
role='system',
access_level='system',
department='IT',
position='시스템 계정',
status='active'
)
db.add(system_user)
print(f" 시스템 계정 '{SYSTEM_USERNAME}' 생성 완료")
else:
print(f"☑️ 시스템 계정 '{SYSTEM_USERNAME}' 이미 존재")
db.commit()
print("✅ 초기 사용자 계정 확인 및 생성 완료")
# 4. 성능 인덱스 추가
print("🚀 성능 인덱스 생성 중...")
with engine.connect() as connection:
indexes = [
"CREATE INDEX IF NOT EXISTS idx_materials_main_nom ON materials(main_nom);",
"CREATE INDEX IF NOT EXISTS idx_materials_red_nom ON materials(red_nom);",
"CREATE INDEX IF NOT EXISTS idx_materials_full_material_grade ON materials(full_material_grade);",
"CREATE INDEX IF NOT EXISTS idx_materials_revision_status ON materials(revision_status);",
"CREATE INDEX IF NOT EXISTS idx_materials_file_id ON materials(file_id);",
"CREATE INDEX IF NOT EXISTS idx_materials_drawing_name ON materials(drawing_name);",
"CREATE INDEX IF NOT EXISTS idx_materials_classified_category ON materials(classified_category);",
"CREATE INDEX IF NOT EXISTS idx_files_job_no ON files(job_no);",
"CREATE INDEX IF NOT EXISTS idx_files_uploaded_by ON files(uploaded_by);",
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);",
"CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);",
]
for index_sql in indexes:
try:
connection.execute(text(index_sql))
except Exception as e:
print(f" ⚠️ 인덱스 생성 실패: {e}")
connection.commit()
print("✅ 성능 인덱스 생성 완료")
return True
except SQLAlchemyOperationalError as e:
print(f"❌ 데이터베이스 작업 실패: {e}")
return False
except Exception as e:
print(f"❌ 예상치 못한 오류 발생: {e}")
return False
if __name__ == "__main__":
print("🚀 TK-MP-Project 완전한 자동 DB 마이그레이션 시작")
print(f"⏰ 시작 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"🖥️ 환경: {os.uname().sysname} {os.uname().machine}")
print("🔧 DB 설정 확인:")
print(f" - DB_HOST: {DB_HOST}")
print(f" - DB_PORT: {DB_PORT}")
print(f" - DB_NAME: {DB_NAME}")
print(f" - DB_USER: {DB_USER}")
if not wait_for_db():
print("❌ DB 마이그레이션 실패. 서버 시작을 중단합니다.")
exit(1)
if not create_tables_and_migrate():
print("⚠️ DB 마이그레이션에서 일부 오류가 발생했지만 서버를 시작합니다.")
print(" (기존 스키마가 있거나 부분적으로 성공했을 수 있습니다)")
else:
print("✅ 완전한 DB 마이그레이션 성공")
print(f"⏰ 완료 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")