610 lines
28 KiB
Python
610 lines
28 KiB
Python
from PySide6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QLabel, QTextEdit, QCheckBox, QLineEdit,
|
|
QPushButton, QMessageBox, QScrollArea, QWidget, QTabWidget, QGridLayout, QProgressBar
|
|
)
|
|
from PySide6.QtCore import Qt, QTimer, QTime
|
|
from PySide6.QtGui import QFont
|
|
from datetime import datetime, timezone
|
|
from collapsible_announcement import CollapsibleAnnouncement
|
|
import webbrowser
|
|
import logging
|
|
|
|
class SignupDialog(QDialog):
|
|
def __init__(self, logger, supabase_manager, parent=None):
|
|
super().__init__(parent)
|
|
self.logger = logger
|
|
self.supabase_manager = supabase_manager
|
|
self.setWindowTitle("회원가입")
|
|
self.setFixedSize(700, 850)
|
|
self.setup_ui()
|
|
self.load_announcements_content()
|
|
|
|
def setup_ui(self):
|
|
self.layout = QVBoxLayout(self)
|
|
self.layout.setContentsMargins(20, 20, 20, 20)
|
|
self.layout.setSpacing(15)
|
|
|
|
# 회원가입 타이틀
|
|
title_label = QLabel("회원가입")
|
|
title_label.setAlignment(Qt.AlignCenter)
|
|
title_label.setStyleSheet("font-size: 20px; font-weight: bold;")
|
|
self.layout.addWidget(title_label)
|
|
|
|
# 공지사항 아코디언 영역 (ScrollArea)
|
|
announcement_container = QWidget()
|
|
self.ann_layout = QVBoxLayout(announcement_container)
|
|
self.ann_layout.setSpacing(10)
|
|
announcement_container.setLayout(self.ann_layout)
|
|
scroll_area = QScrollArea()
|
|
scroll_area.setWidgetResizable(True)
|
|
scroll_area.setWidget(announcement_container)
|
|
scroll_area.setFixedHeight(300)
|
|
self.layout.addWidget(QLabel("공지사항"))
|
|
self.layout.addWidget(scroll_area)
|
|
|
|
# 탭 위젯 생성 (두 개의 탭: 개인정보 동의, 프로그램 라이센스 동의)
|
|
self.tab_widget = QTabWidget()
|
|
self.layout.addWidget(self.tab_widget)
|
|
|
|
# 개인정보 동의 탭 구성
|
|
self.privacy_tab = QWidget()
|
|
privacy_layout = QVBoxLayout(self.privacy_tab)
|
|
privacy_layout.setSpacing(10)
|
|
privacy_info = QTextEdit()
|
|
privacy_info.setReadOnly(True)
|
|
privacy_info.setFixedHeight(130)
|
|
privacy_content = self.supabase_manager.get_privacy_content()
|
|
privacy_info.setHtml(privacy_content if privacy_content else "개인정보 동의 내용이 없습니다.")
|
|
privacy_layout.addWidget(QLabel("개인정보 수집 및 이용 동의"))
|
|
privacy_layout.addWidget(privacy_info)
|
|
self.privacy_checkbox = QCheckBox("위 내용을 모두 읽고 동의합니다.")
|
|
privacy_layout.addWidget(self.privacy_checkbox)
|
|
self.tab_widget.addTab(self.privacy_tab, "개인정보 동의")
|
|
|
|
# 프로그램 라이센스 동의 탭 구성
|
|
self.license_tab = QWidget()
|
|
license_layout = QVBoxLayout(self.license_tab)
|
|
license_layout.setSpacing(10)
|
|
license_info = QTextEdit()
|
|
license_info.setReadOnly(True)
|
|
license_info.setFixedHeight(130)
|
|
program_content = self.supabase_manager.get_program_license_content()
|
|
license_info.setHtml(program_content if program_content else "프로그램 라이센스 내용이 없습니다.")
|
|
license_layout.addWidget(QLabel("프로그램 라이센스 동의"))
|
|
license_layout.addWidget(license_info)
|
|
self.program_license_checkbox = QCheckBox("위 내용을 모두 읽고 동의합니다.")
|
|
license_layout.addWidget(self.program_license_checkbox)
|
|
self.tab_widget.addTab(self.license_tab, "프로그램 라이센스 동의")
|
|
# 처음에는 라이센스 탭을 비활성화
|
|
self.tab_widget.setTabEnabled(1, False)
|
|
|
|
# 회원가입 정보 입력 영역
|
|
input_widget = QWidget()
|
|
grid = QGridLayout(input_widget)
|
|
grid.setSpacing(10)
|
|
|
|
# Row 0: 이메일, 비밀번호
|
|
email_label = QLabel("이메일 (필수):")
|
|
self.email_input = QLineEdit()
|
|
self.email_input.setPlaceholderText("이메일 입력")
|
|
grid.addWidget(email_label, 0, 0)
|
|
grid.addWidget(self.email_input, 0, 1)
|
|
|
|
password_label = QLabel("비밀번호 (필수):")
|
|
self.password_input = QLineEdit()
|
|
self.password_input.setPlaceholderText("비밀번호 입력")
|
|
self.password_input.setEchoMode(QLineEdit.Password)
|
|
grid.addWidget(password_label, 1, 0)
|
|
grid.addWidget(self.password_input, 1, 1)
|
|
|
|
verify_label = QLabel("비밀번호 확인 (필수):")
|
|
self.verify_password_input = QLineEdit()
|
|
self.verify_password_input.setPlaceholderText("비밀번호 확인 입력")
|
|
self.verify_password_input.setEchoMode(QLineEdit.Password)
|
|
grid.addWidget(verify_label, 1, 2)
|
|
grid.addWidget(self.verify_password_input, 1, 3)
|
|
|
|
self.pw_match_label = QLabel("")
|
|
# 초기에는 두 비밀번호 필드가 비어있으므로 아무 텍스트도 표시하지 않음.
|
|
self.pw_match_label.setStyleSheet("font-size: 12px;")
|
|
grid.setRowMinimumHeight(1, 20)
|
|
|
|
grid.addWidget(self.pw_match_label,2,0,1,4)
|
|
|
|
# Row 1: 이름, 비밀번호 확인
|
|
name_label = QLabel("이름 (필수):")
|
|
self.name_input = QLineEdit()
|
|
self.name_input.setPlaceholderText("이름 입력")
|
|
grid.addWidget(name_label, 3, 0)
|
|
grid.addWidget(self.name_input, 3, 1)
|
|
|
|
|
|
# Row 2: 닉네임, 비밀번호 일치 여부 메시지 (span 2 columns)
|
|
nickname_label = QLabel("닉네임 (선택):")
|
|
self.nickname_input = QLineEdit()
|
|
self.nickname_input.setPlaceholderText("닉네임 입력 (선택)")
|
|
grid.addWidget(nickname_label, 3, 2)
|
|
grid.addWidget(self.nickname_input, 3, 3)
|
|
|
|
|
|
self.layout.addWidget(input_widget)
|
|
|
|
# 회원가입 버튼 (초기에는 비활성화)
|
|
self.signup_button = QPushButton("회원가입")
|
|
self.signup_button.setEnabled(False)
|
|
self.signup_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #1877f2;
|
|
color: white;
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
font-size: 14px;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #cccccc;
|
|
color: #888888;
|
|
}
|
|
QPushButton:hover:!disabled {
|
|
background-color: #166fe5;
|
|
}
|
|
""")
|
|
self.signup_button.clicked.connect(self.handle_signup)
|
|
self.layout.addWidget(self.signup_button)
|
|
|
|
self.setLayout(self.layout)
|
|
|
|
# 입력 필드의 변경 신호 연결 (비밀번호 일치 및 필수 필드 확인)
|
|
self.password_input.textChanged.connect(self.validate_inputs)
|
|
self.verify_password_input.textChanged.connect(self.validate_inputs)
|
|
self.email_input.textChanged.connect(self.validate_inputs)
|
|
self.name_input.textChanged.connect(self.validate_inputs)
|
|
|
|
# 체크박스 상태 변화 연결 (toggled 시그널을 사용하여 boolean 값을 받음)
|
|
self.privacy_checkbox.toggled.connect(self.on_privacy_toggled)
|
|
self.program_license_checkbox.toggled.connect(self.check_tabs_status)
|
|
|
|
def is_password_valid(self, password: str) -> bool:
|
|
"""
|
|
비밀번호가 다음 조건을 충족하는지 검사합니다:
|
|
- 최소 8자 이상
|
|
- 최소 하나의 대문자 포함
|
|
- 최소 하나의 소문자 포함
|
|
- 최소 하나의 특수문자 포함 (예: !@#$%^&*(),.?":{}|<>)
|
|
"""
|
|
import re
|
|
|
|
if len(password) < 8:
|
|
return False
|
|
if not re.search(r"[A-Z]", password):
|
|
return False
|
|
if not re.search(r"[a-z]", password):
|
|
return False
|
|
if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password):
|
|
return False
|
|
return True
|
|
|
|
def on_privacy_toggled(self, checked: bool):
|
|
if checked:
|
|
# 개인정보 동의가 체크되면 라이센스 탭 활성화 및 자동 전환
|
|
self.tab_widget.setTabEnabled(1, True)
|
|
self.tab_widget.setCurrentWidget(self.license_tab)
|
|
else:
|
|
self.tab_widget.setTabEnabled(1, False)
|
|
self.tab_widget.setCurrentWidget(self.privacy_tab)
|
|
self.check_tabs_status()
|
|
|
|
def check_tabs_status(self):
|
|
"""
|
|
개인정보와 프로그램 라이센스 동의 체크박스가 모두 체크되고,
|
|
필수 필드가 모두 채워지며, 비밀번호가 일치할 경우 회원가입 버튼이 활성화됩니다.
|
|
"""
|
|
if (self.privacy_checkbox.isChecked() and
|
|
self.program_license_checkbox.isChecked() and
|
|
self.are_required_fields_filled() and
|
|
self.is_password_match()):
|
|
self.signup_button.setEnabled(True)
|
|
else:
|
|
self.signup_button.setEnabled(False)
|
|
|
|
def are_required_fields_filled(self) -> bool:
|
|
return (self.email_input.text().strip() != "" and
|
|
self.password_input.text().strip() != "" and
|
|
self.verify_password_input.text().strip() != "" and
|
|
self.name_input.text().strip() != "")
|
|
|
|
def is_password_match(self) -> bool:
|
|
return self.password_input.text() == self.verify_password_input.text()
|
|
|
|
def validate_inputs(self):
|
|
"""
|
|
비밀번호 일치 여부와 필수 입력 필드를 확인하여,
|
|
두 비밀번호 필드가 모두 비어있으면 아무것도 표시하지 않고,
|
|
값이 있을 경우 일치하면 긍정 메시지, 불일치하면 부정 메시지를 표시합니다.
|
|
그리고 check_tabs_status()를 호출합니다.
|
|
"""
|
|
pwd = self.password_input.text()
|
|
verify_pwd = self.verify_password_input.text()
|
|
|
|
if pwd == "" and verify_pwd == "":
|
|
self.pw_match_label.setText("")
|
|
# 비밀번호 조건 검증
|
|
elif not self.is_password_valid(pwd):
|
|
self.pw_match_label.setText("비밀번호는 최소 8자, 대문자, 소문자, 특수문자를 포함해야 합니다.")
|
|
self.pw_match_label.setStyleSheet("font-size: 12px; color: red;")
|
|
elif pwd != verify_pwd:
|
|
self.pw_match_label.setText("비밀번호가 일치하지 않습니다.")
|
|
self.pw_match_label.setStyleSheet("font-size: 12px; color: red;")
|
|
else:
|
|
self.pw_match_label.setText("비밀번호가 조건에 맞고 일치합니다.")
|
|
self.pw_match_label.setStyleSheet("font-size: 12px; color: green;")
|
|
self.check_tabs_status()
|
|
|
|
def _is_valid_email(self, email: str) -> bool:
|
|
"""간단한 이메일 형식 검증"""
|
|
import re
|
|
pattern = r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
|
|
return bool(re.match(pattern, email))
|
|
|
|
def load_announcements_content(self):
|
|
"""서버에서 공지사항을 불러와 CollapsibleAnnouncement 위젯으로 추가합니다."""
|
|
try:
|
|
announcements = self.supabase_manager.get_announcements()
|
|
if announcements:
|
|
announcements = sorted(announcements, key=lambda x: x["position"])
|
|
for ann in announcements:
|
|
widget = CollapsibleAnnouncement(ann["position"], ann["title"], ann["content"])
|
|
self.ann_layout.addWidget(widget)
|
|
else:
|
|
self.ann_layout.addWidget(QLabel("현재 공지사항이 없습니다."))
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "오류", f"공지사항 로드 중 오류 발생: {e}")
|
|
|
|
def open_mail_provider(self, email: str):
|
|
"""
|
|
입력한 이메일의 도메인에 맞춰 기본 웹 브라우저에서 메일 로그인 페이지를 엽니다.
|
|
한국 사용자들이 주로 사용하는 도메인을 최대한 추가했습니다.
|
|
만약 매칭되는 도메인이 없다면 기본 페이지를 열고, 사용자가 직접 해당 메일에 접속하라는 안내를 합니다.
|
|
"""
|
|
domain = email.split('@')[-1].lower()
|
|
url = None
|
|
|
|
if "gmail" in domain:
|
|
url = "https://mail.google.com"
|
|
elif "naver" in domain:
|
|
url = "https://mail.naver.com"
|
|
elif "daum" in domain or "hanmail" in domain:
|
|
url = "https://mail.daum.net"
|
|
elif "nate" in domain:
|
|
url = "https://mail.nate.com"
|
|
elif "outlook" in domain or "hotmail" in domain or "live" in domain:
|
|
url = "https://outlook.live.com/owa/"
|
|
elif "yahoo" in domain:
|
|
url = "https://mail.yahoo.com"
|
|
# 추가 도메인이 필요하다면 아래에 조건을 추가하세요.
|
|
# 예: if "empas" in domain: url = "..."
|
|
|
|
if url:
|
|
webbrowser.open(url)
|
|
else:
|
|
# 매칭되는 이메일 공급자가 없을 경우,
|
|
# 사용자에게 직접 해당 메일 웹사이트에 접속하라는 안내 메시지를 출력하고 기본 페이지를 엽니다.
|
|
QMessageBox.information(None, "메일 로그인 안내",
|
|
f"입력하신 이메일({email})에 해당하는 메일 공급자를 찾을 수 없습니다.\n"
|
|
"직접 해당 메일 서비스에 접속하여 로그인해 주세요.")
|
|
webbrowser.open("https://www.google.com") # 또는 다른 기본 페이지
|
|
|
|
def handle_signup(self):
|
|
if not (self.privacy_checkbox.isChecked() and self.program_license_checkbox.isChecked()):
|
|
QMessageBox.warning(self, "동의 필요", "회원가입을 진행하려면 개인정보 및 라이센스 동의가 필요합니다.")
|
|
return
|
|
|
|
if not self.are_required_fields_filled():
|
|
QMessageBox.warning(self, "입력 오류", "모든 필수 필드를 입력해 주세요.")
|
|
return
|
|
|
|
if not self.is_password_match():
|
|
QMessageBox.warning(self, "비밀번호 오류", "비밀번호가 일치하지 않습니다.")
|
|
return
|
|
|
|
email = self.email_input.text().strip()
|
|
|
|
# 간단한 이메일 형식 검증 (@가 하나만 있고, @前後에 문자가 있는지)
|
|
if not self._is_valid_email(email):
|
|
QMessageBox.warning(self, "입력 오류", "올바른 이메일 형식이 아닙니다.\n예: example@domain.com")
|
|
return
|
|
|
|
password = self.password_input.text().strip()
|
|
username = self.name_input.text().strip()
|
|
nickname = self.nickname_input.text().strip()
|
|
now_iso = datetime.now(timezone.utc).isoformat()
|
|
|
|
metadata = {
|
|
"username": username,
|
|
"nickname": nickname,
|
|
"privacy_consent": True,
|
|
"privacy_consent_date": now_iso,
|
|
"license_consent": True,
|
|
"license_consent_date": now_iso
|
|
}
|
|
|
|
user = self.supabase_manager.register(email, password, metadata=metadata)
|
|
|
|
# 반환값이 error를 포함한 dict인 경우
|
|
if isinstance(user, dict) and user.get("error"):
|
|
QMessageBox.warning(self, "회원가입 실패", user.get("error"))
|
|
return
|
|
|
|
# 타임아웃으로 응답은 못 받았지만 서버에서 처리되었을 경우
|
|
if isinstance(user, dict) and user.get("pending_confirmation"):
|
|
email = user["email"]
|
|
self.logger.log(f"회원가입 타임아웃 처리 - 이메일 인증 진행: {email}", level=logging.INFO)
|
|
# 인증코드 입력 다이얼로그 표시 (user_id 없음 → OTP 입력 모드)
|
|
verification_dialog = EmailVerificationDialog(
|
|
self.logger, self.supabase_manager, email, metadata, parent=self
|
|
)
|
|
self.open_mail_provider(email)
|
|
if verification_dialog.exec() == QDialog.Accepted:
|
|
self.accept()
|
|
return
|
|
|
|
if user:
|
|
# 회원가입 성공 후 EmailVerificationDialog 실행
|
|
verification_dialog = EmailVerificationDialog(self.logger, self.supabase_manager, email, metadata, user_id=user.id, parent=self)
|
|
|
|
result = verification_dialog.exec() # 모달 창 실행
|
|
|
|
if result == QDialog.Accepted:
|
|
# 이메일 인증이 완료되었으므로, auth.users의 email_confirmed_at에 값이 채워졌다고 가정
|
|
# 이 시점에서 public.users 테이블에 추가 메타데이터를 동기화합니다.
|
|
sync_success = self.supabase_manager.sync_public_user_profile(user.id, metadata)
|
|
if sync_success:
|
|
QMessageBox.information(self, "회원가입 성공",
|
|
"회원가입에 성공했습니다.\n이메일 인증이 완료되었고, 프로필 동기화가 완료되었습니다.")
|
|
self.accept()
|
|
else:
|
|
QMessageBox.warning(self, "동기화 실패", "회원 프로필 동기화에 실패했습니다. 관리자에게 문의하세요.")
|
|
else:
|
|
QMessageBox.warning(self, "이메일 인증 실패", "이메일 인증이 완료되지 않아 회원가입이 취소되었습니다.")
|
|
else:
|
|
QMessageBox.warning(self, "회원가입 실패", "회원가입에 실패했습니다. 입력 정보를 다시 확인해주세요.")
|
|
|
|
|
|
|
|
|
|
import webbrowser
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton, QMessageBox
|
|
from PySide6.QtCore import QTimer, QTime
|
|
from datetime import datetime, timezone
|
|
import logging
|
|
class EmailVerificationDialog(QDialog):
|
|
"""
|
|
EmailVerificationDialog는 회원가입 후 이메일 인증을 처리합니다.
|
|
- user_id가 제공되면 auth.users의 email_confirmed_at을 자동 폴링합니다.
|
|
- 이메일로 받은 6자리 OTP 코드를 입력하여 직접 인증할 수 있습니다.
|
|
인증 성공 시 public.users에 프로필을 동기화하고 QDialog.Accepted를 반환합니다.
|
|
"""
|
|
def __init__(self, logger, supabase_manager, email: str, metadata: dict, user_id: str = None, parent=None):
|
|
super().__init__(parent)
|
|
self.logger = logger
|
|
self.supabase_manager = supabase_manager
|
|
self.email = email
|
|
self.metadata = metadata
|
|
self.user_id = user_id
|
|
self.verified_user_id = None # OTP 인증 후 설정
|
|
self.setWindowTitle("이메일 인증")
|
|
self.setFixedSize(420, 320)
|
|
self.setup_ui()
|
|
self.start_email_verification()
|
|
|
|
def setup_ui(self) -> None:
|
|
self.layout = QVBoxLayout(self)
|
|
self.layout.setSpacing(10)
|
|
|
|
# 상태 레이블
|
|
self.verification_status_label = QLabel("이메일 인증 대기 중...")
|
|
self.verification_status_label.setAlignment(Qt.AlignCenter)
|
|
self.layout.addWidget(self.verification_status_label)
|
|
|
|
# 프로그레스 바
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setTextVisible(True)
|
|
self.layout.addWidget(self.progress_bar)
|
|
|
|
# OTP 코드 입력 영역
|
|
otp_label = QLabel("인증 코드:")
|
|
self.otp_input = QLineEdit()
|
|
self.otp_input.setPlaceholderText("이메일로 받은 6자리 코드를 입력하세요")
|
|
self.otp_input.setMaxLength(6)
|
|
self.otp_input.setAlignment(Qt.AlignCenter)
|
|
self.otp_input.setStyleSheet("font-size: 18px; letter-spacing: 4px; padding: 6px;")
|
|
|
|
otp_layout = QVBoxLayout()
|
|
otp_layout.setSpacing(4)
|
|
otp_layout.addWidget(otp_label)
|
|
otp_layout.addWidget(self.otp_input)
|
|
|
|
verify_button = QPushButton("인증 확인")
|
|
verify_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #1877f2;
|
|
color: white;
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
font-size: 14px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #166fe5;
|
|
}
|
|
""")
|
|
verify_button.clicked.connect(self.handle_verify_otp)
|
|
otp_layout.addWidget(verify_button)
|
|
self.layout.addLayout(otp_layout)
|
|
|
|
# 안내 레이블
|
|
self.instructions_label = QLabel(
|
|
"이메일로 전송된 6자리 인증 코드를 입력해주세요.\n"
|
|
"메일이 보이지 않으면 스팸함도 확인해주세요."
|
|
)
|
|
self.instructions_label.setWordWrap(True)
|
|
self.layout.addWidget(self.instructions_label)
|
|
|
|
# 버튼 영역
|
|
button_layout = QVBoxLayout()
|
|
self.resend_button = QPushButton("인증 메일 재전송")
|
|
self.resend_button.clicked.connect(self.handle_resend_email)
|
|
button_layout.addWidget(self.resend_button)
|
|
self.layout.addLayout(button_layout)
|
|
|
|
def start_email_verification(self) -> None:
|
|
self.verification_timeout = 300 # 5분 (300초)
|
|
self.verification_start_time = QTime.currentTime()
|
|
self.progress_bar.setMaximum(self.verification_timeout)
|
|
self.progress_bar.setValue(0)
|
|
|
|
self.countdown_timer = QTimer(self)
|
|
self.countdown_timer.setInterval(1000) # 1초 간격
|
|
self.countdown_timer.timeout.connect(self.update_countdown)
|
|
self.countdown_timer.start()
|
|
|
|
# user_id가 있을 때만 email_confirmed_at 폴링 시작
|
|
if self.user_id:
|
|
self.logger.log(f"자동 인증 폴링 시작 (user_id: {self.user_id})", level=logging.DEBUG)
|
|
self.verification_timer = QTimer(self)
|
|
self.verification_timer.setInterval(3000) # 3초 간격
|
|
self.verification_timer.timeout.connect(self.poll_verification)
|
|
self.verification_timer.start()
|
|
else:
|
|
self.logger.log("user_id가 없어 폴링 없이 OTP 입력 모드로 동작", level=logging.DEBUG)
|
|
|
|
def update_countdown(self) -> None:
|
|
elapsed = self.verification_start_time.secsTo(QTime.currentTime())
|
|
remaining = self.verification_timeout - elapsed
|
|
if remaining >= 0:
|
|
self.verification_status_label.setText(f"남은 시간: {remaining}초")
|
|
self.progress_bar.setValue(elapsed)
|
|
else:
|
|
self._on_timeout()
|
|
|
|
def _on_timeout(self) -> None:
|
|
"""인증 시간 만료 처리"""
|
|
self.verification_status_label.setText("인증 시간이 만료되었습니다.")
|
|
if hasattr(self, 'verification_timer'):
|
|
self.verification_timer.stop()
|
|
self.countdown_timer.stop()
|
|
QMessageBox.warning(self, "인증 실패", "제한 시간 내에 이메일 인증이 완료되지 않았습니다.\n다시 가입해 주세요.")
|
|
if self.user_id:
|
|
self.supabase_manager.delete_user(self.user_id)
|
|
self.reject()
|
|
|
|
def poll_verification(self) -> None:
|
|
"""user_id로 email_confirmed_at 폴링"""
|
|
elapsed = self.verification_start_time.secsTo(QTime.currentTime())
|
|
if elapsed >= self.verification_timeout:
|
|
self.verification_timer.stop()
|
|
self.countdown_timer.stop()
|
|
QMessageBox.warning(self, "인증 실패", "제한 시간 내에 이메일 인증이 완료되지 않아 회원정보가 삭제되었습니다.\n다시 가입해 주세요.")
|
|
self.supabase_manager.delete_user(self.user_id)
|
|
self.reject()
|
|
return
|
|
|
|
auth_info = self.supabase_manager.get_auth_user_info(self.user_id)
|
|
self.logger.log(f"auth_info : {auth_info}", level=logging.DEBUG)
|
|
|
|
if auth_info and auth_info.get("email_confirmed_at"):
|
|
self.verification_timer.stop()
|
|
self.countdown_timer.stop()
|
|
self.verification_status_label.setText("이메일 인증이 완료되었습니다.")
|
|
self._sync_and_accept(self.user_id)
|
|
else:
|
|
value = self.progress_bar.value()
|
|
value = (value + 10) % self.verification_timeout
|
|
self.progress_bar.setValue(value)
|
|
|
|
def handle_verify_otp(self) -> None:
|
|
"""OTP 코드로 직접 인증"""
|
|
token = self.otp_input.text().strip()
|
|
if not token:
|
|
QMessageBox.warning(self, "입력 오류", "인증 코드를 입력해주세요.")
|
|
return
|
|
if len(token) != 6 or not token.isdigit():
|
|
QMessageBox.warning(self, "입력 오류", "인증 코드는 6자리 숫자입니다.")
|
|
return
|
|
|
|
self.logger.log(f"OTP 인증 시도: {self.email}", level=logging.INFO)
|
|
result = self.supabase_manager.verify_signup_otp(self.email, token)
|
|
|
|
if result.get("success"):
|
|
user_id = result.get("user_id")
|
|
self.logger.log(f"OTP 인증 성공: {self.email} (user_id: {user_id})", level=logging.INFO)
|
|
self.verified_user_id = user_id
|
|
self.verification_status_label.setText("이메일 인증이 완료되었습니다.")
|
|
|
|
# 타이머 정지
|
|
if hasattr(self, 'verification_timer'):
|
|
self.verification_timer.stop()
|
|
self.countdown_timer.stop()
|
|
|
|
# 프로필 동기화 및 종료
|
|
self._sync_and_accept(user_id)
|
|
else:
|
|
error_msg = result.get("message", "인증 코드가 올바르지 않습니다.")
|
|
QMessageBox.warning(self, "인증 실패", error_msg)
|
|
self.otp_input.clear()
|
|
self.otp_input.setFocus()
|
|
|
|
def _sync_and_accept(self, user_id: str) -> None:
|
|
"""프로필 동기화 후 다이얼로그 종료"""
|
|
sync_result = self.supabase_manager.sync_public_user_profile(user_id, self.metadata)
|
|
if isinstance(sync_result, int) and sync_result in (200, 204):
|
|
msg_box = QMessageBox(self)
|
|
msg_box.setWindowTitle("인증 완료")
|
|
msg_box.setText("이메일 인증 및 프로필 동기화가 완료되었습니다.")
|
|
msg_box.setIcon(QMessageBox.Information)
|
|
msg_box.setWindowModality(Qt.ApplicationModal)
|
|
msg_box.setWindowFlag(Qt.WindowStaysOnTopHint, True)
|
|
msg_box.show()
|
|
msg_box.raise_()
|
|
msg_box.activateWindow()
|
|
msg_box.exec()
|
|
self.accept()
|
|
else:
|
|
error_msg = sync_result.get("error") if isinstance(sync_result, dict) else "알 수 없는 오류"
|
|
QMessageBox.warning(self, "동기화 실패", f"회원 프로필 동기화에 실패했습니다: {error_msg}")
|
|
self.reject()
|
|
|
|
def handle_resend_email(self) -> None:
|
|
success = self.supabase_manager.resend_verification_email(self.email)
|
|
if success:
|
|
QMessageBox.information(self, "재전송 성공", "인증 메일이 재전송되었습니다.\n스팸함도 확인해주세요.")
|
|
else:
|
|
QMessageBox.warning(self, "재전송 실패", "인증 메일 재전송 중 오류가 발생했습니다.\n관리자에게 문의하세요.")
|
|
|
|
def open_mail_provider(self, email: str) -> None:
|
|
"""
|
|
입력한 이메일 도메인에 따라 기본 웹 브라우저에서 메일 로그인 페이지를 엽니다.
|
|
매칭되는 도메인이 없으면 기본 페이지를 열고, 사용자가 직접 로그인하도록 안내합니다.
|
|
"""
|
|
domain = email.split('@')[-1].lower()
|
|
url = None
|
|
|
|
if "gmail" in domain:
|
|
url = "https://mail.google.com"
|
|
elif "naver" in domain:
|
|
url = "https://mail.naver.com"
|
|
elif "daum" in domain or "hanmail" in domain:
|
|
url = "https://mail.daum.net"
|
|
elif "nate" in domain:
|
|
url = "https://mail.nate.com"
|
|
elif "outlook" in domain or "hotmail" in domain or "live" in domain:
|
|
url = "https://outlook.live.com/owa/"
|
|
elif "yahoo" in domain:
|
|
url = "https://mail.yahoo.com"
|
|
# 필요시 추가 도메인 조건
|
|
if url:
|
|
webbrowser.open(url)
|
|
else:
|
|
QMessageBox.information(None, "메일 로그인 안내",
|
|
f"입력하신 이메일({email})에 해당하는 메일 공급자를 찾을 수 없습니다.\n"
|
|
"직접 해당 메일 서비스에 접속하여 로그인해 주세요.")
|
|
webbrowser.open("https://www.google.com")
|