# 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"
현재 버전: {self.version}
"
f"최신 버전: {update_info.version}
"
f"업데이트 등급: {update_info.update_level.value}
이 업데이트는 필수 업데이트입니다. 프로그램을 계속 사용하려면 업데이트가 필요합니다.
") mandatory_label.setTextFormat(Qt.RichText) layout.addWidget(mandatory_label) # 업데이트 내용 라벨 release_note_label = QLabel(update_dialog) release_note_label.setText("