🔄 전반적인 시스템 리팩토링 완료
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: 배포 시 자동 실행 순서 최적화
This commit is contained in:
Hyungi Ahn
2025-10-20 08:41:06 +09:00
parent 0c99697a6f
commit 3398f71b80
61 changed files with 3370 additions and 4512 deletions

View 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)

View 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')}")

View 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)

View 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()

View 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()

View 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("❌ 최적화 실패")

View 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()