905 lines
39 KiB
Python
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)
|
|
|