✅ 백엔드 구조 개선: - 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: 배포 시 자동 실행 순서 최적화
This commit is contained in:
138
backend/scripts/analyze_and_fix_schema.py
Normal file
138
backend/scripts/analyze_and_fix_schema.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
백엔드 코드 전체 분석 후 누락된 모든 컬럼과 테이블을 한 번에 수정하는 스크립트
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import psycopg2
|
||||
from psycopg2 import sql
|
||||
|
||||
# 현재 디렉토리를 Python 경로에 추가
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
# 환경 변수 로드
|
||||
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")
|
||||
|
||||
def fix_all_missing_columns():
|
||||
"""백엔드 코드 분석 결과를 바탕으로 모든 누락된 컬럼 추가"""
|
||||
print("🔍 백엔드 코드 분석 결과를 바탕으로 모든 누락된 컬럼 수정 시작...")
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD
|
||||
)
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
# 1. materials 테이블 누락된 컬럼들
|
||||
print("📝 materials 테이블 컬럼 추가 중...")
|
||||
materials_columns = [
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS brand VARCHAR(100);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS user_requirement TEXT;",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed BOOLEAN DEFAULT FALSE;",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS confirmed_quantity NUMERIC(10,3);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_status VARCHAR(20);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed_by VARCHAR(100);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed_at TIMESTAMP;",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS material_hash VARCHAR(64);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS normalized_description TEXT;",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS revision_status VARCHAR(20);",
|
||||
]
|
||||
|
||||
for sql_cmd in materials_columns:
|
||||
cursor.execute(sql_cmd)
|
||||
print("✅ materials 테이블 컬럼 추가 완료")
|
||||
|
||||
# 2. material_purchase_tracking 테이블 누락된 컬럼들 (백엔드 코드에서 사용됨)
|
||||
print("📝 material_purchase_tracking 테이블 컬럼 추가 중...")
|
||||
mpt_columns = [
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS job_no VARCHAR(50);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS material_hash VARCHAR(64);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS revision VARCHAR(20);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS description TEXT;",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS size_spec VARCHAR(50);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS unit VARCHAR(10);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS bom_quantity NUMERIC(10,3);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS calculated_quantity NUMERIC(10,3);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS supplier_name VARCHAR(100);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS unit_price NUMERIC(12,2);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS total_price NUMERIC(15,2);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS order_date DATE;",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS delivery_date DATE;",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS confirmed_by VARCHAR(100);",
|
||||
"ALTER TABLE material_purchase_tracking ADD COLUMN IF NOT EXISTS confirmed_at TIMESTAMP;",
|
||||
]
|
||||
|
||||
for sql_cmd in mpt_columns:
|
||||
cursor.execute(sql_cmd)
|
||||
print("✅ material_purchase_tracking 테이블 컬럼 추가 완료")
|
||||
|
||||
# 3. files 테이블 누락된 컬럼들
|
||||
print("📝 files 테이블 컬럼 추가 중...")
|
||||
files_columns = [
|
||||
"ALTER TABLE files ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;",
|
||||
"ALTER TABLE files ADD COLUMN IF NOT EXISTS project_type VARCHAR(50);",
|
||||
]
|
||||
|
||||
for sql_cmd in files_columns:
|
||||
cursor.execute(sql_cmd)
|
||||
print("✅ files 테이블 컬럼 추가 완료")
|
||||
|
||||
# 4. pipe_details 테이블 누락된 컬럼들 (백엔드 코드에서 사용됨)
|
||||
print("📝 pipe_details 테이블 컬럼 추가 중...")
|
||||
pipe_details_columns = [
|
||||
"ALTER TABLE pipe_details ADD COLUMN IF NOT EXISTS material_id INTEGER REFERENCES materials(id);",
|
||||
"ALTER TABLE pipe_details ADD COLUMN IF NOT EXISTS outer_diameter VARCHAR(50);",
|
||||
"ALTER TABLE pipe_details ADD COLUMN IF NOT EXISTS material_spec VARCHAR(100);",
|
||||
"ALTER TABLE pipe_details ADD COLUMN IF NOT EXISTS classification_confidence NUMERIC(3,2);",
|
||||
]
|
||||
|
||||
for sql_cmd in pipe_details_columns:
|
||||
cursor.execute(sql_cmd)
|
||||
print("✅ pipe_details 테이블 컬럼 추가 완료")
|
||||
|
||||
# 5. 기타 누락된 테이블들의 컬럼 추가
|
||||
print("📝 기타 테이블들 컬럼 추가 중...")
|
||||
|
||||
# fitting_details 테이블 컬럼 추가
|
||||
cursor.execute("ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS main_size VARCHAR(50);")
|
||||
cursor.execute("ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS reduced_size VARCHAR(50);")
|
||||
cursor.execute("ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS length_mm NUMERIC(10,3);")
|
||||
|
||||
# gasket_details 테이블 컬럼 추가
|
||||
cursor.execute("ALTER TABLE gasket_details ADD COLUMN IF NOT EXISTS gasket_subtype VARCHAR(50);")
|
||||
cursor.execute("ALTER TABLE gasket_details ADD COLUMN IF NOT EXISTS material_type VARCHAR(50);")
|
||||
cursor.execute("ALTER TABLE gasket_details ADD COLUMN IF NOT EXISTS size_inches VARCHAR(50);")
|
||||
cursor.execute("ALTER TABLE gasket_details ADD COLUMN IF NOT EXISTS temperature_range VARCHAR(50);")
|
||||
cursor.execute("ALTER TABLE gasket_details ADD COLUMN IF NOT EXISTS fire_safe BOOLEAN DEFAULT FALSE;")
|
||||
|
||||
print("✅ 기타 테이블들 컬럼 추가 완료")
|
||||
|
||||
conn.commit()
|
||||
print("✅ 모든 누락된 컬럼 추가 완료")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 컬럼 추가 실패: {e}")
|
||||
return False
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 백엔드 코드 분석 기반 스키마 수정 시작")
|
||||
success = fix_all_missing_columns()
|
||||
if success:
|
||||
print("✅ 모든 스키마 수정 완료")
|
||||
else:
|
||||
print("❌ 스키마 수정 실패")
|
||||
sys.exit(1)
|
||||
301
backend/scripts/complete_migrate.py
Normal file
301
backend/scripts/complete_migrate.py
Normal file
@@ -0,0 +1,301 @@
|
||||
#!/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')}")
|
||||
142
backend/scripts/fix_missing_tables.py
Normal file
142
backend/scripts/fix_missing_tables.py
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
누락된 테이블 수동 생성 스크립트
|
||||
배포 시 자동으로 실행되도록 설계
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import psycopg2
|
||||
from psycopg2 import sql
|
||||
|
||||
# 현재 디렉토리를 Python 경로에 추가
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
# 환경 변수 로드
|
||||
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")
|
||||
|
||||
def create_missing_tables():
|
||||
"""누락된 테이블들을 직접 생성"""
|
||||
print("🔧 누락된 테이블 생성 시작...")
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD
|
||||
)
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
# purchase_requests 테이블 생성
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS purchase_requests (
|
||||
request_id VARCHAR(50) PRIMARY KEY,
|
||||
request_no VARCHAR(100) NOT NULL,
|
||||
file_id INTEGER REFERENCES files(id),
|
||||
job_no VARCHAR(50) NOT NULL,
|
||||
category VARCHAR(50),
|
||||
material_count INTEGER,
|
||||
excel_file_path VARCHAR(500),
|
||||
requested_by INTEGER REFERENCES users(user_id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""")
|
||||
print("✅ purchase_requests 테이블 생성 완료")
|
||||
|
||||
# materials 테이블에 누락된 컬럼들 추가
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS brand VARCHAR(100);")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS user_requirement TEXT;")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed BOOLEAN DEFAULT FALSE;")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS confirmed_quantity NUMERIC(10,3);")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_status VARCHAR(20);")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed_by VARCHAR(100);")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed_at TIMESTAMP;")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS material_hash VARCHAR(64);")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS normalized_description TEXT;")
|
||||
cursor.execute("ALTER TABLE materials ADD COLUMN IF NOT EXISTS revision_status VARCHAR(20);")
|
||||
print("✅ materials 테이블 누락된 컬럼들 추가 완료")
|
||||
|
||||
# 기타 누락될 수 있는 테이블들
|
||||
missing_tables = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS excel_exports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
file_id INTEGER REFERENCES files(id),
|
||||
export_type VARCHAR(50),
|
||||
file_path VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_activity_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(user_id),
|
||||
activity_type VARCHAR(50),
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS excel_export_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
request_id VARCHAR(50) REFERENCES purchase_requests(request_id),
|
||||
export_path VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS exported_materials (
|
||||
id SERIAL PRIMARY KEY,
|
||||
export_id INTEGER REFERENCES excel_exports(id),
|
||||
material_id INTEGER REFERENCES materials(id),
|
||||
quantity NUMERIC(10, 3),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS purchase_status_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
material_id INTEGER REFERENCES materials(id),
|
||||
old_status VARCHAR(50),
|
||||
new_status VARCHAR(50),
|
||||
changed_by INTEGER REFERENCES users(user_id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
]
|
||||
|
||||
for table_sql in missing_tables:
|
||||
try:
|
||||
cursor.execute(table_sql)
|
||||
table_name = table_sql.split("CREATE TABLE IF NOT EXISTS ")[1].split(" (")[0]
|
||||
print(f"✅ {table_name} 테이블 생성 완료")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 테이블 생성 중 오류 (무시하고 계속): {e}")
|
||||
|
||||
conn.commit()
|
||||
print("✅ 모든 누락된 테이블 생성 완료")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 테이블 생성 실패: {e}")
|
||||
return False
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 누락된 테이블 생성 스크립트 시작")
|
||||
success = create_missing_tables()
|
||||
if success:
|
||||
print("✅ 누락된 테이블 생성 완료")
|
||||
else:
|
||||
print("❌ 누락된 테이블 생성 실패")
|
||||
sys.exit(1)
|
||||
113
backend/scripts/legacy/auto_migrate.py
Normal file
113
backend/scripts/legacy/auto_migrate.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TK-MP-Project 자동 DB 마이그레이션 스크립트
|
||||
배포 시 백엔드 시작 전에 자동으로 실행되어 DB 스키마를 동기화
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# 백엔드 모듈 경로 추가
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
def wait_for_db():
|
||||
"""데이터베이스 연결 대기"""
|
||||
max_retries = 30
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
from app.database import engine
|
||||
# 간단한 연결 테스트
|
||||
with engine.connect() as conn:
|
||||
conn.execute("SELECT 1")
|
||||
print("✅ 데이터베이스 연결 성공")
|
||||
return True
|
||||
except Exception as e:
|
||||
retry_count += 1
|
||||
print(f"⏳ 데이터베이스 연결 대기 중... ({retry_count}/{max_retries})")
|
||||
time.sleep(2)
|
||||
|
||||
print("❌ 데이터베이스 연결 실패")
|
||||
return False
|
||||
|
||||
def sync_database_schema():
|
||||
"""데이터베이스 스키마 동기화"""
|
||||
try:
|
||||
from app.models import Base
|
||||
from app.database import engine
|
||||
|
||||
print("🔄 데이터베이스 스키마 동기화 중...")
|
||||
|
||||
# 모든 테이블 생성/업데이트
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
print("✅ 데이터베이스 스키마 동기화 완료")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 스키마 동기화 실패: {str(e)}")
|
||||
return False
|
||||
|
||||
def ensure_required_data():
|
||||
"""필수 데이터 확인 및 생성"""
|
||||
try:
|
||||
from app.database import get_db
|
||||
from app.auth.models import User
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
print("🔄 필수 데이터 확인 중...")
|
||||
|
||||
db = next(get_db())
|
||||
|
||||
# 관리자 계정 확인
|
||||
admin_user = db.query(User).filter(User.username == 'admin').first()
|
||||
if not admin_user:
|
||||
print("📝 기본 관리자 계정 생성 중...")
|
||||
admin_user = User(
|
||||
username='admin',
|
||||
password='$2b$12$ld4LDOW5mxkiRQEkXfMUIep/aIzFleQZ4yoL10ZQkUxGqnkYuhNMW', # admin123
|
||||
name='시스템 관리자',
|
||||
email='admin@tkmp.com',
|
||||
role='admin',
|
||||
access_level='admin',
|
||||
department='IT',
|
||||
position='시스템 관리자',
|
||||
status='active'
|
||||
)
|
||||
db.add(admin_user)
|
||||
db.commit()
|
||||
print("✅ 기본 관리자 계정 생성 완료")
|
||||
|
||||
db.close()
|
||||
print("✅ 필수 데이터 확인 완료")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 필수 데이터 확인 실패: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
print("🚀 TK-MP-Project 자동 DB 마이그레이션 시작")
|
||||
print(f"⏰ 시작 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 1. 데이터베이스 연결 대기
|
||||
if not wait_for_db():
|
||||
sys.exit(1)
|
||||
|
||||
# 2. 스키마 동기화
|
||||
if not sync_database_schema():
|
||||
sys.exit(1)
|
||||
|
||||
# 3. 필수 데이터 확인
|
||||
if not ensure_required_data():
|
||||
sys.exit(1)
|
||||
|
||||
print("🎉 자동 DB 마이그레이션 완료!")
|
||||
print(f"⏰ 완료 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
144
backend/scripts/legacy/generate_complete_schema.py
Normal file
144
backend/scripts/legacy/generate_complete_schema.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TK-MP-Project 완전한 DB 스키마 생성 스크립트
|
||||
백엔드 SQLAlchemy 모델을 기반으로 PostgreSQL 스키마를 자동 생성
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# 백엔드 모듈 경로 추가
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from sqlalchemy import create_engine, MetaData
|
||||
from sqlalchemy.schema import CreateTable, CreateIndex
|
||||
from app.models import Base
|
||||
from app.auth.models import User, LoginLog, UserSession, Permission, RolePermission
|
||||
|
||||
def generate_schema_sql():
|
||||
"""SQLAlchemy 모델을 기반으로 완전한 PostgreSQL 스키마 생성"""
|
||||
|
||||
# 메모리 내 SQLite 엔진 생성 (스키마 생성용)
|
||||
engine = create_engine('sqlite:///:memory:', echo=False)
|
||||
|
||||
# 모든 테이블 생성
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# PostgreSQL 방언으로 변환
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
schema_lines = []
|
||||
schema_lines.append("-- ================================")
|
||||
schema_lines.append("-- TK-MP-Project 완전한 데이터베이스 스키마")
|
||||
schema_lines.append(f"-- 자동 생성일: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
schema_lines.append("-- SQLAlchemy 모델 기반 자동 생성")
|
||||
schema_lines.append("-- ================================")
|
||||
schema_lines.append("")
|
||||
|
||||
# 테이블 생성 SQL 생성
|
||||
for table in Base.metadata.sorted_tables:
|
||||
create_table_sql = str(CreateTable(table).compile(dialect=postgresql.dialect()))
|
||||
|
||||
# SQLite -> PostgreSQL 변환
|
||||
create_table_sql = create_table_sql.replace('DATETIME', 'TIMESTAMP')
|
||||
create_table_sql = create_table_sql.replace('BOOLEAN', 'BOOLEAN')
|
||||
create_table_sql = create_table_sql.replace('TEXT', 'TEXT')
|
||||
create_table_sql = create_table_sql.replace('JSON', 'JSONB')
|
||||
|
||||
schema_lines.append(f"-- {table.name} 테이블")
|
||||
schema_lines.append(f"CREATE TABLE IF NOT EXISTS {create_table_sql[13:]};") # "CREATE TABLE " 제거
|
||||
schema_lines.append("")
|
||||
|
||||
# 인덱스 생성
|
||||
schema_lines.append("-- ================================")
|
||||
schema_lines.append("-- 인덱스 생성")
|
||||
schema_lines.append("-- ================================")
|
||||
schema_lines.append("")
|
||||
|
||||
for table in Base.metadata.sorted_tables:
|
||||
for index in table.indexes:
|
||||
create_index_sql = str(CreateIndex(index).compile(dialect=postgresql.dialect()))
|
||||
schema_lines.append(f"CREATE INDEX IF NOT EXISTS {create_index_sql[13:]};") # "CREATE INDEX " 제거
|
||||
|
||||
# 필수 데이터 삽입
|
||||
schema_lines.append("")
|
||||
schema_lines.append("-- ================================")
|
||||
schema_lines.append("-- 필수 기본 데이터 삽입")
|
||||
schema_lines.append("-- ================================")
|
||||
schema_lines.append("")
|
||||
|
||||
# 기본 관리자 계정
|
||||
schema_lines.append("-- 기본 관리자 계정 (비밀번호: admin123)")
|
||||
schema_lines.append("INSERT INTO users (username, password, name, email, role, access_level, department, position, status) VALUES")
|
||||
schema_lines.append("('admin', '$2b$12$ld4LDOW5mxkiRQEkXfMUIep/aIzFleQZ4yoL10ZQkUxGqnkYuhNMW', '시스템 관리자', 'admin@tkmp.com', 'admin', 'admin', 'IT', '시스템 관리자', 'active'),")
|
||||
schema_lines.append("('system', '$2b$12$ld4LDOW5mxkiRQEkXfMUIep/aIzFleQZ4yoL10ZQkUxGqnkYuhNMW', '시스템 계정', 'system@tkmp.com', 'system', 'system', 'IT', '시스템 계정', 'active')")
|
||||
schema_lines.append("ON CONFLICT (username) DO NOTHING;")
|
||||
schema_lines.append("")
|
||||
|
||||
# 기본 권한 데이터
|
||||
schema_lines.append("-- 기본 권한 데이터")
|
||||
permissions = [
|
||||
('bom.view', 'BOM 조회 권한', 'bom'),
|
||||
('bom.create', 'BOM 생성 권한', 'bom'),
|
||||
('bom.edit', 'BOM 수정 권한', 'bom'),
|
||||
('bom.delete', 'BOM 삭제 권한', 'bom'),
|
||||
('project.view', '프로젝트 조회 권한', 'project'),
|
||||
('project.create', '프로젝트 생성 권한', 'project'),
|
||||
('project.edit', '프로젝트 수정 권한', 'project'),
|
||||
('file.upload', '파일 업로드 권한', 'file'),
|
||||
('file.download', '파일 다운로드 권한', 'file'),
|
||||
('user.view', '사용자 조회 권한', 'user'),
|
||||
('user.create', '사용자 생성 권한', 'user'),
|
||||
('system.admin', '시스템 관리 권한', 'system')
|
||||
]
|
||||
|
||||
schema_lines.append("INSERT INTO permissions (permission_name, description, module) VALUES")
|
||||
for i, (name, desc, module) in enumerate(permissions):
|
||||
comma = "," if i < len(permissions) - 1 else ""
|
||||
schema_lines.append(f"('{name}', '{desc}', '{module}'){comma}")
|
||||
schema_lines.append("ON CONFLICT (permission_name) DO NOTHING;")
|
||||
schema_lines.append("")
|
||||
|
||||
# 완료 메시지
|
||||
schema_lines.append("-- ================================")
|
||||
schema_lines.append("-- 스키마 생성 완료")
|
||||
schema_lines.append("-- ================================")
|
||||
schema_lines.append("")
|
||||
schema_lines.append("DO $$")
|
||||
schema_lines.append("BEGIN")
|
||||
schema_lines.append(" RAISE NOTICE '✅ TK-MP-Project 완전한 데이터베이스 스키마가 성공적으로 생성되었습니다!';")
|
||||
schema_lines.append(" RAISE NOTICE '👤 기본 계정: admin/admin123, system/admin123';")
|
||||
schema_lines.append(" RAISE NOTICE '🔐 권한 시스템: 모듈별 세분화된 권한 적용';")
|
||||
schema_lines.append(" RAISE NOTICE '📊 자동 생성: SQLAlchemy 모델 기반 완전 동기화';")
|
||||
schema_lines.append("END $$;")
|
||||
|
||||
return '\n'.join(schema_lines)
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
try:
|
||||
print("🔄 SQLAlchemy 모델 분석 중...")
|
||||
schema_sql = generate_schema_sql()
|
||||
|
||||
# 스키마 파일 저장
|
||||
output_file = os.path.join(os.path.dirname(__file__), '..', '..', 'database', 'init', '00_auto_generated_schema.sql')
|
||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(schema_sql)
|
||||
|
||||
print(f"✅ 완전한 DB 스키마가 생성되었습니다: {output_file}")
|
||||
print("📋 포함된 내용:")
|
||||
print(" - 모든 SQLAlchemy 모델 기반 테이블")
|
||||
print(" - 필수 인덱스")
|
||||
print(" - 기본 관리자 계정")
|
||||
print(" - 기본 권한 데이터")
|
||||
print("🚀 배포 시 이 파일이 자동으로 실행됩니다.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 스키마 생성 실패: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
195
backend/scripts/optimize_database.py
Normal file
195
backend/scripts/optimize_database.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
데이터베이스 성능 최적화 스크립트
|
||||
인덱스 생성, 쿼리 최적화, 통계 업데이트
|
||||
"""
|
||||
|
||||
import os
|
||||
import psycopg2
|
||||
from psycopg2 import sql
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 환경 변수 로드
|
||||
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")
|
||||
|
||||
|
||||
def optimize_database():
|
||||
"""데이터베이스 성능 최적화"""
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD
|
||||
)
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
# 1. 핵심 인덱스 생성
|
||||
print("🔧 핵심 인덱스 생성 중...")
|
||||
|
||||
indexes = [
|
||||
# materials 테이블 인덱스
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_file_id ON materials(file_id);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_category ON materials(classified_category);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_drawing_name ON materials(drawing_name);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_line_no ON materials(line_no);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_revision_status ON materials(revision_status);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_purchase_confirmed ON materials(purchase_confirmed);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_material_hash ON materials(material_hash);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_main_nom ON materials(main_nom);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_material_grade ON materials(material_grade);",
|
||||
|
||||
# files 테이블 인덱스
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_files_job_no ON files(job_no);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_files_revision ON files(revision);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_files_uploaded_by ON files(uploaded_by);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_files_upload_date ON files(upload_date);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_files_is_active ON files(is_active);",
|
||||
|
||||
# 복합 인덱스 (자주 함께 사용되는 컬럼들)
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_file_category ON materials(file_id, classified_category);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_drawing_line ON materials(drawing_name, line_no);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_files_job_revision ON files(job_no, revision);",
|
||||
|
||||
# material_purchase_tracking 인덱스
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_mpt_material_hash ON material_purchase_tracking(material_hash);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_mpt_job_revision ON material_purchase_tracking(job_no, revision);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_mpt_purchase_status ON material_purchase_tracking(purchase_status);",
|
||||
|
||||
# 상세 테이블들 인덱스
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pipe_details_material_id ON pipe_details(material_id);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_fitting_details_material_id ON fitting_details(material_id);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_flange_details_material_id ON flange_details(material_id);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_valve_details_material_id ON valve_details(material_id);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_bolt_details_material_id ON bolt_details(material_id);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_gasket_details_material_id ON gasket_details(material_id);",
|
||||
|
||||
# 사용자 관련 인덱스
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_username ON users(username);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_status ON users(status);",
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_role ON users(role);",
|
||||
]
|
||||
|
||||
for index_sql in indexes:
|
||||
try:
|
||||
cursor.execute(index_sql)
|
||||
conn.commit()
|
||||
print(f"✅ 인덱스 생성: {index_sql.split('idx_')[1].split(' ')[0] if 'idx_' in index_sql else 'unknown'}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 인덱스 생성 실패: {e}")
|
||||
conn.rollback()
|
||||
|
||||
# 2. 통계 업데이트
|
||||
print("📊 테이블 통계 업데이트 중...")
|
||||
|
||||
tables_to_analyze = [
|
||||
'materials', 'files', 'projects', 'users',
|
||||
'material_purchase_tracking', 'pipe_details',
|
||||
'fitting_details', 'flange_details', 'valve_details',
|
||||
'bolt_details', 'gasket_details'
|
||||
]
|
||||
|
||||
for table in tables_to_analyze:
|
||||
try:
|
||||
cursor.execute(f"ANALYZE {table};")
|
||||
conn.commit()
|
||||
print(f"✅ 통계 업데이트: {table}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 통계 업데이트 실패 ({table}): {e}")
|
||||
conn.rollback()
|
||||
|
||||
# 3. VACUUM 실행 (선택적)
|
||||
print("🧹 데이터베이스 정리 중...")
|
||||
try:
|
||||
# VACUUM은 트랜잭션 외부에서 실행해야 함
|
||||
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
cursor.execute("VACUUM ANALYZE;")
|
||||
print("✅ VACUUM ANALYZE 완료")
|
||||
except Exception as e:
|
||||
print(f"⚠️ VACUUM 실패: {e}")
|
||||
|
||||
print("✅ 데이터베이스 최적화 완료")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 데이터베이스 최적화 실패: {e}")
|
||||
return False
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_database_stats():
|
||||
"""데이터베이스 통계 조회"""
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD
|
||||
)
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
# 테이블별 레코드 수
|
||||
print("📊 테이블별 레코드 수:")
|
||||
|
||||
tables = ['materials', 'files', 'projects', 'users', 'material_purchase_tracking']
|
||||
|
||||
for table in tables:
|
||||
try:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table};")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" {table}: {count:,}개")
|
||||
except Exception as e:
|
||||
print(f" {table}: 조회 실패 ({e})")
|
||||
|
||||
# 인덱스 사용률 확인
|
||||
print("\n🔍 인덱스 사용률:")
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname,
|
||||
idx_tup_read,
|
||||
idx_tup_fetch
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE idx_tup_read > 0
|
||||
ORDER BY idx_tup_read DESC
|
||||
LIMIT 10;
|
||||
""")
|
||||
|
||||
for row in cursor.fetchall():
|
||||
print(f" {row[1]}.{row[2]}: {row[3]:,} reads, {row[4]:,} fetches")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 통계 조회 실패: {e}")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 데이터베이스 성능 최적화 시작")
|
||||
|
||||
# 현재 통계 확인
|
||||
get_database_stats()
|
||||
|
||||
# 최적화 실행
|
||||
if optimize_database():
|
||||
print("✅ 최적화 완료")
|
||||
|
||||
# 최적화 후 통계 확인
|
||||
print("\n📊 최적화 후 통계:")
|
||||
get_database_stats()
|
||||
else:
|
||||
print("❌ 최적화 실패")
|
||||
126
backend/scripts/simple_migrate.py
Normal file
126
backend/scripts/simple_migrate.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TK-MP-Project 간단하고 안정적인 DB 마이그레이션 스크립트
|
||||
macOS Docker와 Synology Container Manager 모두 지원
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import psycopg2
|
||||
from datetime import datetime
|
||||
|
||||
def get_db_config():
|
||||
"""환경변수에서 DB 설정 가져오기"""
|
||||
return {
|
||||
'host': os.getenv('DB_HOST', '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')
|
||||
}
|
||||
|
||||
def wait_for_database(max_retries=60, retry_interval=2):
|
||||
"""데이터베이스 연결 대기 (더 긴 대기시간과 짧은 간격)"""
|
||||
db_config = get_db_config()
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
conn = psycopg2.connect(**db_config)
|
||||
conn.close()
|
||||
print(f"✅ 데이터베이스 연결 성공 (시도 {attempt}/{max_retries})")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"⏳ DB 연결 대기 중... ({attempt}/{max_retries}) - {str(e)[:50]}")
|
||||
time.sleep(retry_interval)
|
||||
|
||||
print("❌ 데이터베이스 연결 실패")
|
||||
return False
|
||||
|
||||
def execute_sql(sql_commands):
|
||||
"""SQL 명령어 실행"""
|
||||
db_config = get_db_config()
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(**db_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
for sql in sql_commands:
|
||||
if sql.strip():
|
||||
cursor.execute(sql)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ SQL 실행 실패: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_migration_sql():
|
||||
"""필요한 마이그레이션 SQL 반환"""
|
||||
return [
|
||||
# materials 테이블 컬럼 추가
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS row_number INTEGER;",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS main_nom VARCHAR(50);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS red_nom VARCHAR(50);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS full_material_grade TEXT;",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS length NUMERIC(10,3);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed BOOLEAN DEFAULT FALSE;",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS confirmed_quantity NUMERIC(10,3);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_status VARCHAR(20);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed_by VARCHAR(100);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS purchase_confirmed_at TIMESTAMP;",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS revision_status VARCHAR(20);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS material_hash VARCHAR(64);",
|
||||
"ALTER TABLE materials ADD COLUMN IF NOT EXISTS normalized_description TEXT;",
|
||||
|
||||
# users 테이블 status 컬럼 확인 및 추가
|
||||
"ALTER TABLE users ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'active';",
|
||||
"UPDATE users SET status = 'active' WHERE status IS NULL AND is_active = TRUE;",
|
||||
"UPDATE users SET status = 'inactive' WHERE status IS NULL AND is_active = FALSE;",
|
||||
|
||||
# 기본 관리자 계정 확인 및 생성
|
||||
"""
|
||||
INSERT INTO users (username, password, name, email, role, access_level, department, position, status)
|
||||
VALUES ('admin', '$2b$12$ld4LDOW5mxkiRQEkXfMUIep/aIzFleQZ4yoL10ZQkUxGqnkYuhNMW', '시스템 관리자', 'admin@tkmp.com', 'admin', 'admin', 'IT', '시스템 관리자', 'active')
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
password = EXCLUDED.password,
|
||||
status = 'active';
|
||||
""",
|
||||
|
||||
# 인덱스 생성
|
||||
"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_revision_status ON materials(revision_status);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_materials_purchase_status ON materials(purchase_status);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);",
|
||||
]
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
print("🚀 TK-MP-Project 간단 DB 마이그레이션 시작")
|
||||
print(f"⏰ 시작 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 1. 데이터베이스 연결 대기
|
||||
print("🔄 데이터베이스 연결 확인 중...")
|
||||
if not wait_for_database():
|
||||
print("❌ 데이터베이스 연결 실패. 마이그레이션을 중단합니다.")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. 마이그레이션 실행
|
||||
print("🔄 데이터베이스 마이그레이션 실행 중...")
|
||||
migration_sql = get_migration_sql()
|
||||
|
||||
if execute_sql(migration_sql):
|
||||
print("✅ 데이터베이스 마이그레이션 완료")
|
||||
else:
|
||||
print("❌ 데이터베이스 마이그레이션 실패")
|
||||
sys.exit(1)
|
||||
|
||||
print("🎉 간단 DB 마이그레이션 완료!")
|
||||
print(f"⏰ 완료 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("👤 기본 계정: admin/admin123")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user