AutoPercenty3/signup_dialog.py

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