AutoPercenty3/login_dialog.py

905 lines
39 KiB
Python

# login_dialog.py
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton, QHBoxLayout,
QMessageBox, QCheckBox, QFrame, QTextBrowser, QSizePolicy, QRadioButton, QButtonGroup
)
from PySide6.QtCore import Qt, QSize
from src.sp_manager import SupabaseManager
from announcement_widget import AnnouncementDialog
from signup_dialog import SignupDialog
from datetime import datetime, timezone
import logging
from PySide6.QtCore import QTimer
from updateManager.version_manager import VersionManager
import sys
import os
import markdown2
import time
import threading
import platform
import socket
import uuid
import psutil
from uuid import getnode as get_mac
import urllib.request
import urllib.error
class LoginDialog(QDialog):
def __init__(self, logger, settings_manager, parent=None):
super().__init__(parent)
self.logger = logger
self.supabase_manager = SupabaseManager(logger) # Supabase 클라이언트 초기화
# 버전 관리자 초기화
from updateManager.__version__ import __version__
self.version = __version__
self.version_manager = VersionManager(
logger=logger,
supabase_manager=self.supabase_manager,
current_version=__version__
)
self.system_info = self.get_system_info()
self.setWindowTitle("로그인")
self.setFixedSize(380, 400) # 높이 증가
self.settings_manager = settings_manager
self.user = None # 로그인 성공한 사용자 정보를 저장
self.update_selection_data = None # 업데이트 선택 정보 저장
self.init_ui()
self.load_saved_user_info()
# 로그인 전 버전 체크 제거 - 로그인 후에 수행
def check_internet_connection(self, timeout=5):
"""
인터넷 연결 상태를 확인합니다.
Args:
timeout (int): 연결 시도 시간 제한 (초)
Returns:
bool: 인터넷 연결 가능 여부
"""
test_urls = [
'https://www.google.com',
'https://www.naver.com',
'https://8.8.8.8',
'https://1.1.1.1'
]
for url in test_urls:
try:
# HTTP 요청으로 연결 테스트
response = urllib.request.urlopen(url, timeout=timeout)
if response.getcode() == 200:
self.logger.log(f"인터넷 연결 확인됨: {url}", level=logging.DEBUG)
return True
except (urllib.error.URLError, urllib.error.HTTPError, socket.timeout, OSError) as e:
self.logger.log(f"연결 실패 {url}: {str(e)}", level=logging.DEBUG)
continue
# 모든 URL 테스트 실패 시 소켓 연결로 재시도
try:
# Google DNS 서버로 소켓 연결 테스트
sock = socket.create_connection(("8.8.8.8", 53), timeout)
sock.close()
self.logger.log("소켓 연결로 인터넷 연결 확인됨", level=logging.DEBUG)
return True
except (socket.error, socket.timeout, OSError) as e:
self.logger.log(f"소켓 연결 실패: {str(e)}", level=logging.DEBUG)
return False
def show_internet_connection_error(self):
"""
인터넷 연결 오류 메시지를 표시합니다.
"""
msg = QMessageBox(self)
msg.setIcon(QMessageBox.Warning)
msg.setWindowTitle("인터넷 연결 오류")
msg.setText("인터넷 연결을 확인할 수 없습니다.")
msg.setInformativeText(
"로그인을 위해서는 인터넷 연결이 필요합니다.\n\n"
"다음 사항을 확인해 주세요:\n"
"• 네트워크 연결 상태\n"
"• 방화벽 설정\n"
"• 프록시 설정\n"
"• DNS 설정"
)
retry_button = msg.addButton("다시 시도", QMessageBox.ActionRole)
cancel_button = msg.addButton("취소", QMessageBox.RejectRole)
msg.setDefaultButton(retry_button)
msg.exec_()
if msg.clickedButton() == retry_button:
return True # 다시 시도
else:
return False # 취소
def init_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15)
# 타이틀 레이블
title_label = QLabel("로그인")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("font-size: 20px; font-weight: bold;")
layout.addWidget(title_label)
# 이메일 입력
self.email_input = QLineEdit()
self.email_input.setPlaceholderText("이메일을 입력하세요")
layout.addWidget(self.email_input)
# 비밀번호 입력
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("비밀번호를 입력하세요")
self.password_input.setEchoMode(QLineEdit.Password)
layout.addWidget(self.password_input)
# 체크박스 영역: 정보 저장 및 비밀번호 보기
checkbox_layout = QHBoxLayout()
self.remember_checkbox = QCheckBox("정보 저장")
self.show_password_checkbox = QCheckBox("비밀번호 보기")
checkbox_layout.addWidget(self.remember_checkbox)
checkbox_layout.addStretch()
checkbox_layout.addWidget(self.show_password_checkbox)
layout.addLayout(checkbox_layout)
self.show_password_checkbox.stateChanged.connect(self.toggle_password_visibility)
# 버튼 영역
button_layout = QHBoxLayout()
self.login_button = QPushButton("로그인")
self.signup_button = QPushButton("회원가입")
self.reset_button = QPushButton("비밀번호 찾기")
button_layout.addWidget(self.login_button)
button_layout.addWidget(self.signup_button)
button_layout.addWidget(self.reset_button)
layout.addLayout(button_layout)
# 실행타입 영역
run_type_layout = QHBoxLayout()
self.radio_arbait = QRadioButton("편집알바생")
self.radio_shuffle = QRadioButton("업로드알바생")
self.radio_shuffle.setEnabled(False)
self.set_radio_style()
self.run_type_group = QButtonGroup(self)
self.run_type_group.addButton(self.radio_arbait)
self.run_type_group.addButton(self.radio_shuffle)
self.radio_arbait.setChecked(True)
run_type_layout.addWidget(self.radio_arbait)
run_type_layout.addWidget(self.radio_shuffle)
layout.addLayout(run_type_layout)
# 버전 정보 영역 추가
self.version_info_label = QLabel()
self.version_info_label.setAlignment(Qt.AlignCenter)
self.version_info_label.setStyleSheet("font-size: 11px; color: #666; margin-top: 5px;")
self.version_info_label.setText(f"현재 버전: {self.version}")
layout.addWidget(self.version_info_label)
self.login_button.clicked.connect(self.handle_login)
self.signup_button.clicked.connect(self.handle_register)
self.reset_button.clicked.connect(self.handle_password_reset)
self.setLayout(layout)
# 스타일 적용 (모던한 디자인)
self.setStyleSheet("""
QDialog { background-color: #f0f2f5; }
QLabel { font-size: 14px; color: #333; }
QLineEdit { padding: 8px; font-size: 14px; border: 1px solid #ccc; border-radius: 4px; }
QPushButton { background-color: #1877f2; color: white; padding: 8px; border: none; border-radius: 4px; font-size: 14px; }
QPushButton:hover { background-color: #166fe5; }
QCheckBox { font-size: 13px; color: #333; }
""")
def load_saved_user_info(self):
"""저장된 사용자 정보를 SettingsManager에서 불러와 입력란에 채웁니다."""
user_info = self.settings_manager.load_user_info()
if user_info.get("email"):
self.email_input.setText(user_info.get("email"))
if user_info.get("password"):
self.password_input.setText(user_info.get("password"))
def toggle_password_visibility(self, state):
"""비밀번호 보기 체크박스에 따라 비밀번호 에코 모드를 전환합니다."""
# print(f"toggle_password_visibility : {state}")
# if state == Qt.Checked:
if state == 2:
self.password_input.setEchoMode(QLineEdit.Normal)
else:
self.password_input.setEchoMode(QLineEdit.Password)
def get_run_type(self):
"""
라디오버튼의 선택값을 반환합니다.
"편집알바생" 또는 "업로드알바생" 문자열을 반환
"""
if self.radio_arbait.isChecked():
return "편집알바생"
else:
return "업로드알바생"
def get_device_info(self):
"""
시스템의 주요 정보를 딕셔너리 형태로 수집하여 반환합니다.
(운영체제, 호스트명, 사용자, CPU, 메모리, 디스크, 네트워크 등)
"""
info = {}
# 운영체제 정보
info['os_type'] = platform.system()
info['os_release'] = platform.release()
info['os_version'] = platform.version()
info['os_detail'] = platform.platform()
# 호스트명, 사용자명
info['hostname'] = socket.gethostname()
try:
info['username'] = os.getlogin()
except Exception:
info['username'] = os.environ.get('USERNAME') or os.environ.get('USER') or 'Unknown'
# CPU 정보
info['cpu_arch'] = platform.machine()
info['cpu_count'] = os.cpu_count()
# 파이썬 버전
info['python_version'] = platform.python_version()
# 네트워크 정보 (IP, MAC)
try:
info['ip_address'] = socket.gethostbyname(info['hostname'])
except Exception:
info['ip_address'] = 'Unknown'
try:
info['mac_address'] = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
for elements in range(0, 8*6, 8)][::-1])
except Exception:
info['mac_address'] = 'Unknown'
info['ram_gb'] = round(psutil.virtual_memory().total / (1024 ** 3), 2)
info['disk_gb'] = round(psutil.disk_usage('/').total / (1024 ** 3), 2)
info_str = (
f"OS: {info['os_type']} {info['os_release']} ({info['os_version']})\n"
f"Detail: {info['os_detail']}\n"
f"Host: {info['hostname']}\n"
f"User: {info['username']}\n"
f"CPU: {info['cpu_arch']} ({info['cpu_count']} cores)\n"
f"Python: {info['python_version']}\n"
f"IP: {info['ip_address']}\n"
f"MAC: {info['mac_address']}\n"
f"RAM: {info['ram_gb']} GB\n"
f"Disk: {info['disk_gb']} GB"
)
return info_str
def get_public_ip(self):
"""
공인 IP 주소를 가져옵니다.
여러 서비스를 시도하여 안정성을 높입니다.
Returns:
str: 공인 IP 주소 또는 'Unknown'
"""
ip_services = [
'https://api.ipify.org',
'https://ipinfo.io/ip',
'https://icanhazip.com',
'https://ident.me'
]
for service in ip_services:
try:
response = urllib.request.urlopen(service, timeout=5)
ip = response.read().decode('utf8').strip()
if ip:
self.logger.log(f"공인 IP 주소 확인됨: {ip} (서비스: {service})", level=logging.DEBUG)
return ip
except (urllib.error.URLError, urllib.error.HTTPError, socket.timeout, OSError) as e:
self.logger.log(f"공인 IP 확인 실패 {service}: {str(e)}", level=logging.DEBUG)
continue
self.logger.log("모든 공인 IP 서비스 접근 실패", level=logging.WARNING)
return 'Unknown'
def handle_login(self):
self.reset_button.setEnabled(False)
self.signup_button.setEnabled(False)
self.login_button.setEnabled(False)
email = self.email_input.text().strip()
password = self.password_input.text().strip()
if not email or not password:
QMessageBox.warning(self, "입력 오류", "이메일과 비밀번호를 모두 입력하세요.")
return
# 인터넷 연결 확인
if not self.check_internet_connection():
self.logger.log("인터넷 연결 없음 - 로그인 시도 중단", level=logging.WARNING)
# 인터넷 연결 오류 메시지 표시 및 재시도 옵션 제공
while True:
if self.show_internet_connection_error():
# 다시 시도 선택 시 인터넷 연결 재확인
if self.check_internet_connection():
self.logger.log("인터넷 연결 복구됨 - 로그인 계속 진행", level=logging.INFO)
break
else:
continue # 여전히 연결 안됨, 다시 메시지 표시
else:
# 취소 선택 시 로그인 중단
self.logger.log("사용자가 인터넷 연결 오류로 로그인 취소", level=logging.INFO)
return
try:
self.logger.log("로그인 시도 시작", level=logging.INFO)
base_user_info = self.supabase_manager.login(email, password)
if base_user_info:
full_user_info = self.supabase_manager.get_full_user_info(base_user_info["id"], base_user_info)
if full_user_info is None:
QMessageBox.warning(self, "오류", "사용자 정보를 가져오지 못했습니다.")
return
else:
# self.logger.log(f"로그인 성공 full_user_info : {full_user_info}", level=logging.DEBUG)
self.logger.log(f"로그인 성공", level=logging.DEBUG)
# 멤버십 레벨에 따른 최대 세션 수 설정
membership_data = full_user_info.get("membership_level_data", {})
max_sessions = membership_data.get("max_session_limit", 1)
full_user_info.update({"max_session_limit": max_sessions})
# 현재 활성 세션 수 가져오기
current_sessions = self.supabase_manager.get_active_sessions_count(base_user_info["id"])
full_user_info.update({"current_sessions": current_sessions})
# 세션 정보 로깅
self.logger.log(f"최대접속제한: {max_sessions}", level=logging.INFO)
self.logger.log(f"현재접속수: {current_sessions}", level=logging.INFO)
# 세션 생성 가능 여부 확인
if not self.supabase_manager.can_create_new_session(base_user_info["id"], max_sessions):
QMessageBox.warning(self, "세션 제한", f"최대 {max_sessions}개의 동시 접속만 허용됩니다.\n다른 기기에서 로그아웃 후 다시 시도해주세요.")
return
# 마지막 로그인 시간 업데이트
self.supabase_manager.update_last_login(base_user_info["id"])
# 로그인 후 버전 체크 및 업데이트 선택 처리
self.update_selection_data = self.handle_post_login_version_check(full_user_info)
# 시스템 정보 수집
device_info = self.get_device_info()
# 외부(공인) IP
ip_address = self.get_public_ip()
mac_address = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
for elements in range(0, 8*6, 8)][::-1])
# 세션 생성 (업데이트 선택 정보 포함)
session_id = self.supabase_manager.create_user_session(
base_user_info["id"],
device_info=device_info,
ip_address=ip_address,
mac_address=mac_address,
version=self.version,
update_selection=self.update_selection_data
)
if not session_id:
QMessageBox.warning(self, "오류", "세션을 생성할 수 없습니다.")
return
# 세션 생성 후 다시 현재 세션 수 확인하여 로깅
new_current_sessions = self.supabase_manager.get_active_sessions_count(base_user_info["id"])
full_user_info.update({"current_sessions": new_current_sessions})
self.logger.log(f"로그인 후 현재접속수: {new_current_sessions}/{max_sessions}", level=logging.INFO)
self.user = full_user_info
self.logger.log(f"로그인 성공 : {email}", level=logging.INFO)
# 마지막 로그인 시간 업데이트
self.supabase_manager.update_last_login(base_user_info["id"])
# 세션 활성 상태 유지를 위한 타이머 시작
self.start_session_watchdog(session_id)
# 저장 여부 체크
if self.remember_checkbox.isChecked():
user_to_save = {
"email": email,
"password": password,
"id": base_user_info.get("id", ""),
"membership_level": base_user_info.get("membership_level", ""),
"name": base_user_info.get("name", ""),
"nickname": base_user_info.get("nickname", ""),
"username": base_user_info.get("username", ""),
"payment_period_end": base_user_info.get("payment_period_end", ""),
"current_sessions": base_user_info.get("current_sessions", ""),
"new_current_sessions": new_current_sessions,
"max_session_limit": base_user_info.get("max_session_limit", "")
}
self.settings_manager.save_user_info(user_to_save)
QMessageBox.information(self, "성공", "로그인 성공!")
self.accept() # 다이얼로그 종료
else:
# 로그인 실패 시, 추가로 이메일 존재 여부를 확인
existing = self.supabase_manager.get_user_by_email(email)
if existing is None:
reply = QMessageBox.question(
self, "이메일 확인",
"해당 이메일은 회원정보에 존재하지 않습니다.\n회원가입 하시겠습니까?",
QMessageBox.Yes | QMessageBox.No
)
else:
# 이메일은 존재하나 비밀번호가 틀린 경우
QMessageBox.warning(self, "실패", "로그인 실패! 비밀번호가 틀렸습니다.")
# "비밀번호 찾기" 버튼은 이미 UI에 있으므로, 사용자가 누를 수 있습니다.
except urllib.error.URLError as e:
# 네트워크 관련 오류
error_message = "네트워크 연결 오류가 발생했습니다."
detailed_error = f"네트워크 오류: {str(e)}"
self.logger.log(detailed_error, level=logging.ERROR, exc_info=True)
msg = QMessageBox(self)
msg.setIcon(QMessageBox.Critical)
msg.setWindowTitle("네트워크 오류")
msg.setText(error_message)
msg.setInformativeText(
"서버에 연결할 수 없습니다.\n\n"
"다음 사항을 확인해 주세요:\n"
"• 인터넷 연결 상태\n"
"• 서버 상태\n"
"• 방화벽 설정"
)
msg.setDetailedText(detailed_error)
msg.exec_()
except ConnectionError as e:
# 연결 오류
error_message = "서버에 연결할 수 없습니다."
detailed_error = f"연결 오류: {str(e)}"
self.logger.log(detailed_error, level=logging.ERROR, exc_info=True)
QMessageBox.critical(self, "연결 실패",
f"{error_message}\n\n"
"서버 상태를 확인하고 잠시 후 다시 시도해 주세요.")
except socket.timeout as e:
# 타임아웃 오류
error_message = "서버 응답 시간이 초과되었습니다."
detailed_error = f"타임아웃 오류: {str(e)}"
self.logger.log(detailed_error, level=logging.ERROR, exc_info=True)
QMessageBox.critical(self, "타임아웃 오류",
f"{error_message}\n\n"
"네트워크 상태를 확인하고 다시 시도해 주세요.")
except socket.error as e:
# 소켓 오류
error_message = "네트워크 연결에 문제가 발생했습니다."
detailed_error = f"소켓 오류: {str(e)}"
self.logger.log(detailed_error, level=logging.ERROR, exc_info=True)
QMessageBox.critical(self, "연결 오류",
f"{error_message}\n\n"
"네트워크 설정을 확인하고 다시 시도해 주세요.")
except Exception as e:
# 기타 예외
error_message = f"로그인 실패: {str(e)}"
self.logger.log(error_message, level=logging.ERROR, exc_info=True)
# 네트워크 관련 키워드가 포함된 경우 네트워크 오류로 처리
error_str = str(e).lower()
if any(keyword in error_str for keyword in ['network', 'connection', 'timeout', 'unreachable', 'dns', 'resolve']):
QMessageBox.critical(self, "네트워크 오류",
"네트워크 연결에 문제가 발생했습니다.\n\n"
"인터넷 연결 상태를 확인하고 다시 시도해 주세요.")
else:
QMessageBox.critical(self, "로그인 실패", error_message)
finally:
self.reset_button.setEnabled(True)
self.signup_button.setEnabled(True)
self.login_button.setEnabled(True)
def handle_register(self):
"""회원가입 버튼 클릭 시 즉시 SignupDialog를 실행합니다."""
# 인터넷 연결 확인
if not self.check_internet_connection():
self.logger.log("인터넷 연결 없음 - 회원가입 시도 중단", level=logging.WARNING)
if not self.show_internet_connection_error():
return
# 다시 시도 후에도 연결 안됨
if not self.check_internet_connection():
return
signup_dialog = SignupDialog(self.logger, self.supabase_manager, self)
if signup_dialog.exec() == QDialog.Accepted:
QMessageBox.information(self, "회원가입 완료", "회원가입이 완료되었습니다.\n이메일 인증 후 로그인해 주세요.")
else:
QMessageBox.information(self, "회원가입 취소", "회원가입이 취소되었습니다.")
def handle_password_reset(self):
"""비밀번호 찾기 버튼 클릭 시 Supabase의 비밀번호 재설정 이메일을 발송합니다."""
email = self.email_input.text().strip()
if not email:
QMessageBox.warning(self, "입력 오류", "비밀번호 재설정을 위해 이메일을 입력하세요.")
return
# 인터넷 연결 확인
if not self.check_internet_connection():
self.logger.log("인터넷 연결 없음 - 비밀번호 재설정 시도 중단", level=logging.WARNING)
if not self.show_internet_connection_error():
return
# 다시 시도 후에도 연결 안됨
if not self.check_internet_connection():
return
try:
# Supabase Manager에 비밀번호 재설정 메서드가 있다고 가정합니다.
result = self.supabase_manager.reset_password(email)
if result:
QMessageBox.information(self, "전송 완료", "비밀번호 재설정 이메일을 전송했습니다. 이메일을 확인하세요.")
else:
QMessageBox.warning(self, "전송 실패", "비밀번호 재설정 이메일 전송에 실패했습니다.")
except Exception as e:
error_message = f"비밀번호 재설정 실패: {str(e)}"
self.logger.log(error_message, level=logging.ERROR, exc_info=True)
QMessageBox.critical(self, "오류", error_message)
def get_user_info(self):
"""로그인 또는 회원가입에 성공한 사용자 정보를 반환합니다."""
# self.logger.log(f"full_user_info : {self.user}", level=logging.DEBUG)
return self.user
def get_update_selection_data(self):
"""업데이트 선택 정보를 반환합니다."""
return self.update_selection_data
def get_supabase_manager(self):
return self.supabase_manager
def start_session_watchdog(self, session_id):
"""
정기적으로 세션 활성 상태를 갱신하는 타이머를 시작합니다.
"""
def update_session():
try:
while True:
self.supabase_manager.update_session_activity(session_id)
time.sleep(5 * 60) # 5분마다 갱신
except Exception as e:
self.logger.log(f"세션 워치독 오류: {e}", level=logging.ERROR)
watchdog_thread = threading.Thread(target=update_session, daemon=True)
watchdog_thread.start()
def get_system_info(self):
"""시스템 정보 수집"""
try:
info = {
"os": platform.system(),
"os_version": platform.version(),
"platform": platform.platform(),
"machine": platform.machine(),
"hostname": socket.gethostname(),
"ip_address": self._get_ip_address(),
"mac_address": self._get_mac_address(),
"timestamp": datetime.now().isoformat(),
"python_version": platform.python_version()
}
return info
except Exception as e:
self.logger.log(f"시스템 정보 수집 중 오류 발생: {str(e)}", level=logging.ERROR)
return {}
def _get_ip_address(self):
"""IP 주소 가져오기"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return "127.0.0.1"
def _get_mac_address(self):
"""MAC 주소 가져오기"""
try:
mac = get_mac()
mac_hex = ':'.join(['{:02x}'.format((mac >> elements) & 0xff)
for elements in range(0, 8*6, 8)][::-1])
return mac_hex
except Exception:
return "00:00:00:00:00:00"
def closeEvent(self, event):
"""
로그인 다이얼로그 종료시 호출됩니다.
메인 애플리케이션에서 세션을 관리하도록 하기 위해,
여기서는 session을 닫지 않습니다.
"""
# 기본 종료 처리 진행
super().closeEvent(event)
def set_radio_style(self):
radio_style = """
QRadioButton {
spacing: 10px;
font-size: 16px;
color: #444;
font-weight: bold;
padding: 8px 18px;
}
QRadioButton::indicator {
width: 20px;
height: 20px;
border-radius: 10px;
border: 2px solid #1976d2;
background: #fafcff;
margin-right: 8px;
}
QRadioButton::indicator:checked {
background-color: #1976d2;
border: 2px solid #1976d2;
}
QRadioButton::indicator:checked:hover {
border: 2px solid #1250a7;
}
QRadioButton:hover::indicator {
border: 2px solid #90caf9;
}
/* Disabled 상태 스타일 */
QRadioButton:disabled {
color: #bbb;
font-weight: normal;
}
QRadioButton:disabled::indicator {
border: 2px solid #ddd;
background: #f5f5f5;
}
QRadioButton:disabled::indicator:checked {
background-color: #ccc;
border: 2px solid #bbb;
}
"""
self.radio_arbait.setStyleSheet(radio_style)
self.radio_shuffle.setStyleSheet(radio_style)
def handle_post_login_version_check(self, full_user_info):
"""로그인 후 버전 체크 및 업데이트 선택 처리"""
update_selection_data = {
"current_version": self.version,
"latest_version": None,
"update_available": False,
"user_choice": None,
"choice_timestamp": None,
"is_mandatory": False,
"update_authorized": full_user_info.get("update_authenticated_by_admin", False)
}
try:
# 인터넷 연결 확인
if not self.check_internet_connection():
self.logger.log("인터넷 연결 없음 - 버전 체크 건너뜀", level=logging.WARNING)
update_selection_data["user_choice"] = "offline"
update_selection_data["choice_timestamp"] = datetime.now(timezone.utc).isoformat()
return update_selection_data
# 업데이트 확인
update_info = self.version_manager.check_for_updates()
if update_info:
update_selection_data.update({
"latest_version": update_info.version,
"update_available": True,
"is_mandatory": update_info.is_mandatory
})
# 업데이트 권한 확인
if not update_selection_data["update_authorized"]:
self.logger.log("업데이트 권한 없음 - 업데이트 건너뜀", level=logging.WARNING)
update_selection_data["user_choice"] = "unauthorized"
update_selection_data["choice_timestamp"] = datetime.now(timezone.utc).isoformat()
return update_selection_data
# 사용자 정의 업데이트 대화상자 생성
update_dialog = QDialog(self)
update_dialog.setWindowTitle("업데이트 알림")
update_dialog.setMinimumSize(500, 400)
# 필수 업데이트인 경우 닫기 버튼(X)을 눌러도 프로그램 종료
if update_info.is_mandatory:
update_dialog.closeEvent = lambda event: self.handle_mandatory_close(event)
update_dialog.setStyleSheet("""
QDialog {
background-color: #f0f2f5;
font-size: 13px;
}
QLabel {
color: #333;
}
QPushButton {
background-color: #1877f2;
color: white;
padding: 8px 15px;
border: none;
border-radius: 4px;
font-size: 13px;
min-width: 100px;
}
QPushButton:hover {
background-color: #166fe5;
}
QPushButton#cancelButton {
background-color: #f0f2f5;
color: #333;
border: 1px solid #ccc;
}
QPushButton#cancelButton:hover {
background-color: #e4e6e9;
}
QTextBrowser {
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
padding: 10px;
}
QFrame#separator {
background-color: #ddd;
max-height: 1px;
}
""")
# 메인 레이아웃
layout = QVBoxLayout(update_dialog)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15)
# 업데이트 정보 레이블
update_info_label = QLabel(update_dialog)
update_info_label.setText(f"<h2>새로운 버전이 있습니다</h2>")
update_info_label.setTextFormat(Qt.RichText)
layout.addWidget(update_info_label)
# 버전 정보
version_info = QLabel(update_dialog)
version_info.setText(
f"<p><b>현재 버전:</b> {self.version}<br>"
f"<b>최신 버전:</b> {update_info.version}<br>"
f"<b>업데이트 등급:</b> {update_info.update_level.value}</p>"
)
version_info.setTextFormat(Qt.RichText)
layout.addWidget(version_info)
# 구분선
separator = QFrame(update_dialog)
separator.setObjectName("separator")
separator.setFrameShape(QFrame.HLine)
layout.addWidget(separator)
# 필수 업데이트 알림
if update_info.is_mandatory:
mandatory_label = QLabel(update_dialog)
mandatory_label.setText("<p style='color: #f44336; font-weight: bold;'>이 업데이트는 필수 업데이트입니다. 프로그램을 계속 사용하려면 업데이트가 필요합니다.</p>")
mandatory_label.setTextFormat(Qt.RichText)
layout.addWidget(mandatory_label)
# 업데이트 내용 라벨
release_note_label = QLabel(update_dialog)
release_note_label.setText("<h3>업데이트 내용</h3>")
release_note_label.setTextFormat(Qt.RichText)
layout.addWidget(release_note_label)
# 업데이트 내용 텍스트 브라우저 (마크다운 지원)
release_note_browser = QTextBrowser(update_dialog)
release_note_browser.setOpenExternalLinks(True)
# 마크다운을 HTML로 변환
html_content = markdown2.markdown(update_info.release_notes)
release_note_browser.setHtml(html_content)
# 텍스트 브라우저가 충분한 공간을 차지하도록 설정
release_note_browser.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
release_note_browser.setMinimumHeight(150)
layout.addWidget(release_note_browser)
# 버튼 영역
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
update_button = QPushButton("지금 업데이트", update_dialog)
update_button.setDefault(True)
button_layout.addWidget(update_button)
if not update_info.is_mandatory:
cancel_button = QPushButton("나중에", update_dialog)
cancel_button.setObjectName("cancelButton")
button_layout.addWidget(cancel_button)
cancel_button.clicked.connect(update_dialog.reject)
button_layout.addStretch(1)
layout.addLayout(button_layout)
# 버튼 이벤트 연결
update_button.clicked.connect(update_dialog.accept)
# 대화상자 표시
result = update_dialog.exec_()
choice_timestamp = datetime.now(timezone.utc).isoformat()
if result == QDialog.Accepted:
# 사용자가 업데이트를 선택한 경우
update_selection_data["user_choice"] = "accepted"
update_selection_data["choice_timestamp"] = choice_timestamp
# 업데이트 다운로드 및 실행
file_path = self.version_manager.download_update(update_info, self)
if file_path:
self.version_manager.run_update_file(file_path)
sys.exit(0)
else:
# 다운로드 취소 또는 실패
if update_info.is_mandatory:
QMessageBox.critical(self, "필수 업데이트", "필수 업데이트가 취소되었습니다. 프로그램을 종료합니다.")
sys.exit(0)
else:
QMessageBox.critical(self, "오류", "업데이트 다운로드가 취소되었거나 실패했습니다.")
update_selection_data["user_choice"] = "download_failed"
elif update_info.is_mandatory:
# 필수 업데이트인데 거부한 경우
update_selection_data["user_choice"] = "mandatory_rejected"
update_selection_data["choice_timestamp"] = choice_timestamp
QMessageBox.critical(self, "필수 업데이트", "필수 업데이트가 필요합니다. 프로그램을 종료합니다.")
sys.exit(0)
else:
# 선택적 업데이트 거부
update_selection_data["user_choice"] = "rejected"
update_selection_data["choice_timestamp"] = choice_timestamp
else:
# 최신 버전인 경우
update_selection_data["user_choice"] = "latest"
update_selection_data["choice_timestamp"] = datetime.now(timezone.utc).isoformat()
except Exception as e:
# 버전 체크 실패
self.logger.log(f"버전 체크 중 오류 발생: {str(e)}", level=logging.ERROR, exc_info=True)
update_selection_data["user_choice"] = "check_failed"
update_selection_data["choice_timestamp"] = datetime.now(timezone.utc).isoformat()
return update_selection_data
def handle_mandatory_close(self, event):
"""필수 업데이트 대화상자를 닫으면 프로그램을 종료합니다."""
QMessageBox.critical(self, "필수 업데이트", "필수 업데이트가 필요합니다. 프로그램을 종료합니다.")
sys.exit(0)