AutoPercenty3/limited_gui.py

2263 lines
98 KiB
Python

from PySide6.QtWidgets import QApplication, QTabWidget, QMainWindow, QWidget, QMessageBox, QSpinBox, QPushButton, QGroupBox, QVBoxLayout, QGridLayout, QTextEdit, QLabel, QLineEdit, QHBoxLayout, QProgressBar, QSizePolicy, QDialog, QGraphicsDropShadowEffect
from PySide6.QtCore import Qt, Slot, QRect, QTimer
from PySide6.QtGui import QColor
from toggleSwitch import ToggleSwitch
from limited_browser_control import BrowserController
from src.limited_contents.bizDBManager import BizDBManager # 위 클래스를 별도 파일로 분리했다면
from src.limited_contents.business_dialog import BizDialog
import logging
import psutil
import sys
import json
import os, time
import asyncio
from datetime import datetime
from src.unwantedDiag.unwanted_words_dialog import UnwantedWordsDialog
from updateManager.__version__ import __gui_log_level__
from src.logDialog.log_dialog import LogDialog
class shuffleMAIN_GUI(QMainWindow):
def __init__(self, logger, user_info, supabase_manager, settings_manager, system_info=None, update_log="", app=None, version=None, log_paths=None):
"""
:param user: 로그인 후 SupabaseManager.login 또는 register 로부터 전달받은 사용자 정보 (dict)
:param log_paths: 로그 디렉토리 및 파일 경로를 담은 딕셔너리
"""
super().__init__()
self.logger = logger
# 로그 경로 설정
self.log_paths = log_paths
self.system_info = system_info or {}
self.settings_manager = settings_manager
self.version = version
# 사용자 정보 저장 (user_id 등)
self.user_info = user_info
self.sp_user_id = user_info.get("id") # 예를 들어, 'users' 테이블의 PK 값
self.supabase_manager = supabase_manager
self.initUI()
# 설정 관리자 초기화
self.load_settings()
self.initialize_user_session()
self.logger.log(f"로그기록이 설정되었습니다.", level=logging.INFO)
# DB 파일 경로 설정
self.base_dir = self.get_base_dir()
self.base_db_dir = os.path.join(self.base_dir, "user_data")
self.logger.log(f"base_db_dir 경로: {self.base_db_dir}", level=logging.DEBUG)
# 폴더가 존재하는지 확인하고 없으면 생성
if not os.path.exists(self.base_db_dir):
os.makedirs(self.base_db_dir)
self.logger.log(f"DB 폴더 생성됨: {self.base_db_dir}", level=logging.INFO)
self.initial_setting = True
else:
self.logger.log(f"DB 폴더 이미 존재함: {self.base_db_dir}", level=logging.INFO)
self.bizdb_path = os.path.join(self.base_db_dir, "bizinfo.db")
self.logger.log(f"bizdb_path 경로: {self.bizdb_path}", level=logging.DEBUG)
self.biz_dbManager = BizDBManager(self.bizdb_path)
self.biz_dialog = BizDialog(db_manager=self.biz_dbManager)
# 토글 위젯을 저장할 딕셔너리 초기화
self.toggle_widgets = {}
self.browser_controller = BrowserController(self, logger=self.logger, base_path=self.base_dir, login_infos=self.login_infos, toggle_states_for_limited=self.toggle_states_for_limited, biz_dbManager=self.biz_dbManager, sp_user_id=self.sp_user_id, supabase_manager=self.supabase_manager)
self.running = False
self.kill_autohotkey_process()
def init_settings(self):
self.login_infos={
'admin_id' : None,
'admin_pw' : None,
}
# 토글 상태 초기화
self.toggle_states_for_limited = {
'keyword_fix_toggle': False,
'keyword_fix_count_input': 3,
'thumbnail_change_toggle': False,
'title_length_limit': 35, # 상품명 최대 길이
}
def save_settings(self):
"""SettingsManager를 통해 모든 설정 저장"""
pass
def load_settings(self):
"""SettingsManager를 통해 모든 설정 불러오기"""
# self.settings_manager.load_settings(self)
self.logger.log("설정 불러옴", level=logging.DEBUG)
def append_log(self, message):
try:
self.log_display.append(message)
# 스크롤을 항상 아래로
self.log_display.verticalScrollBar().setValue(
self.log_display.verticalScrollBar().maximum()
)
except Exception as e:
self.logger.log(f"로그 추가 중 오류 발생: {str(e)}", level=logging.ERROR, exc_info=True)
def show_log_dialog(self):
"""로그 다이얼로그를 표시합니다."""
try:
# 로그 다이얼로그 생성
log_dialog = LogDialog(
parent=self,
logger=self.logger,
log_paths=self.log_paths,
user_info=self.user_info,
supabase_manager=self.supabase_manager,
system_info=self.system_info,
browser_controller=self.browser_controller
)
# 로그 다이얼로그 실행
log_dialog.exec_()
except Exception as e:
self.logger.log(f"로그 다이얼로그 표시 중 오류 발생: {str(e)}", level=logging.ERROR, exc_info=True)
QMessageBox.critical(self, "오류", f"로그 다이얼로그를 표시할 수 없습니다: {str(e)}")
def get_base_dir(self):
"""
실행 환경에 따라 base_dir을 설정하는 메서드.
cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정.
"""
if getattr(sys, 'frozen', False): # 패키징된 경우
base_dir = os.path.dirname(sys.executable)
internal_dir = os.path.join(base_dir, 'lib') # lib 디렉토리 포함
if os.path.exists(internal_dir): # lib 디렉토리가 존재하면 base_dir로 설정
return internal_dir
else: # 일반 Python 실행 환경
base_dir = os.path.dirname(os.path.abspath(__file__))
return base_dir
def creat_Business_tab(self):
"""
토글 설정을 탭 형식으로 생성하고 관리하는 메서드입니다.
기존 toggle_layout_widget 대신 사용됩니다.
"""
# 메인 위젯 및 레이아웃 생성
self.business_main_widget = QWidget()
self.business_main_widget.setFixedHeight(450)
self.business_main_widget.setFixedWidth(700)
self.business_main_layout = QVBoxLayout(self.business_main_widget)
self.business_main_layout.setContentsMargins(10, 10, 10, 10)
self.business_main_layout.setSpacing(10)
# 제목 영역 (10%)
title_layout = QHBoxLayout()
title_label = QLabel("사업자 설정")
title_label.setStyleSheet("font-size: 16px; font-weight: bold;")
title_layout.addWidget(title_label)
self.business_main_layout.addLayout(title_layout)
# 탭 영역 (90%)
self.business_tab_widget = QTabWidget()
self.business_tab_widget.setStyleSheet("""
QTabWidget::pane {
border: 1px solid #cccccc;
background: white;
}
QTabBar::tab {
background: #f0f0f0;
border: 1px solid #cccccc;
padding: 6px 12px;
margin-right: 2px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background: white;
border-bottom-color: white;
}
""")
# 각 탭 생성
self.create_business_1_tab()
self.create_business_2_tab()
self.create_product_name_tab()
self.create_product_name_tab()
self.create_product_name_tab()
self.create_product_name_tab()
self.business_main_layout.addWidget(self.business_tab_widget)
return self.business_main_widget
def set_group_style(self, group_box: QGroupBox, layout=None, theme: str="default"):
"""
QGroupBox와 레이아웃에 미리 정의된 테마 스타일을 적용합니다.
themes: "default", "modern", "smooth", "neumorphism", "dark"
"""
# 테마별 Stylesheet 사전
styles = {
"default": """
QGroupBox {
border: 1px solid #cccccc;
border-radius: 4px;
background-color: #f9f9f9;
color: #666666;
font-weight: bold;
font-size: 12px;
margin: 10px;
padding: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
background: transparent;
color: #666666;
font-weight: bold;
font-size: 12px;
}
""",
"modern": """
QGroupBox {
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
background-color: #ffffff;
color: #333333;
font-size: 13px;
font-weight: 600;
margin: 12px;
padding: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
background: #ffffff;
color: #0078d7;
padding: 0 8px;
font-size: 13px;
font-weight: 600;
}
""",
"smooth": """
QGroupBox {
border: 1px solid #e0e0e0;
border-radius: 12px;
background-color: #fafafa;
color: #555555;
font-size: 12px;
font-weight: normal;
margin: 14px;
padding: 14px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 14px;
padding: 2px 10px;
background-color: #fafafa;
color: #888888;
font-size: 12px;
}
""",
"neumorphism": """
QGroupBox {
border: 1px solid rgba(0,0,0,0.1);
border-radius: 10px;
background-color: #e0e0e0;
color: #333333;
font-size: 13px;
font-weight: bold;
margin: 10px;
padding: 15px;
}
QGroupBox::title {
subcontrol-origin: border;
subcontrol-position: top center;
background-color: #e0e0e0;
color: #333333;
padding: 0 10px;
font-size: 13px;
font-weight: bold;
}
""",
"dark": """
QGroupBox {
border: 1px solid #444444;
border-radius: 6px;
background-color: #2b2b2b;
color: #dddddd;
font-size: 12px;
font-weight: bold;
margin: 8px;
padding: 12px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 8px;
padding: 0 6px;
background-color: #2b2b2b;
color: #61afef;
font-size: 12px;
}
"""
}
# 스타일 적용
sheet = styles.get(theme, styles["default"])
group_box.setStyleSheet(sheet)
# 그림자 효과 추가
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(15)
shadow.setColor(QColor(0, 0, 0, 30))
shadow.setOffset(0, 2)
group_box.setGraphicsEffect(shadow)
# 레이아웃 마진/간격 조정 (테마별로 느낌 달리)
if layout:
if theme == "modern":
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(12)
elif theme == "smooth":
layout.setContentsMargins(24, 24, 24, 24)
layout.setSpacing(10)
elif theme == "neumorphism":
layout.setContentsMargins(18, 18, 18, 18)
layout.setSpacing(14)
elif theme == "dark":
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
else: # default
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(6)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
def set_PUSHBTN_style(self, toggle: QPushButton, theme: str = "default"):
"""
QPushButton 기반 ToggleSwitch 스타일을 설정합니다.
themes: "default", "modern", "smooth", "neumorphism", "dark"
"""
styles = {
"default": """
QPushButton {
background-color: #f0f0f0;
border: 1px solid #cccccc;
border-radius: 12px;
min-width: 40px;
min-height: 20px;
padding: 0;
text-align: center;
font-size: 12px;
color: #333333;
}
QPushButton:checked {
background-color: #e0e0e0;
color: #0078d7;
}
QPushButton:hover {
background-color: #e8e8e8;
}
QPushButton:pressed {
background-color: #d0d0d0;
}
QPushButton:disabled {
background-color: #f0f0f0;
border-color: #cccccc;
}
""",
"modern": """
QPushButton {
background-color: #61afef;
border: none;
border-radius: 14px;
min-width: 44px;
min-height: 24px;
padding: 0;
font-size: 12px;
color: #555555;
}
QPushButton:checked {
background-color: qlineargradient(
x1:0, y1:0, x2:1, y2:1,
stop:0 #0078d7, stop:1 #005fa3);
color: #ffffff;
}
QPushButton:hover {
background-color: #f5f5f5;
}
QPushButton:pressed {
background-color: #e0e0e0;
}
QPushButton:disabled {
background-color: #f0f0f0;
border-color: #cccccc;
}
""",
"smooth": """
QPushButton {
background-color: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 10px;
min-width: 42px;
min-height: 22px;
padding: 0;
font-size: 12px;
color: #666666;
}
QPushButton:checked {
background-color: #e8e8e8;
color: #333333;
}
QPushButton:hover {
background-color: #f2f2f2;
}
QPushButton:pressed {
background-color: #dcdcdc;
}
QPushButton:disabled {
background-color: #f0f0f0;
border-color: #cccccc;
}
""",
"neumorphism": """
QPushButton {
background-color: #e0e0e0;
border: none;
border-radius: 12px;
min-width: 44px;
min-height: 24px;
padding: 0;
font-size: 12px;
color: #444444;
box-shadow:
inset 2px 2px 5px rgba(255,255,255,0.7),
inset -2px -2px 5px rgba(0,0,0,0.15);
}
QPushButton:checked {
background-color: #d1d1d1;
color: #0078d7;
box-shadow:
inset 2px 2px 5px rgba(0,0,0,0.1),
inset -2px -2px 5px rgba(255,255,255,0.7);
}
QPushButton:hover {
box-shadow:
inset 1px 1px 3px rgba(255,255,255,0.8),
inset -1px -1px 3px rgba(0,0,0,0.1);
}
QPushButton:pressed {
box-shadow:
inset 4px 4px 8px rgba(0,0,0,0.2),
inset -4px -4px 8px rgba(255,255,255,0.6);
}
QPushButton:disabled {
background-color: #f0f0f0;
border-color: #cccccc;
}
""",
"dark": """
QPushButton {
background-color: #2b2b2b;
border: 1px solid #444444;
border-radius: 12px;
min-width: 42px;
min-height: 22px;
padding: 0;
font-size: 12px;
color: #cccccc;
}
QPushButton:checked {
background-color: #444444;
color: #61afef;
}
QPushButton:hover {
background-color: #333333;
}
QPushButton:pressed {
background-color: #1e1e1e;
}
QPushButton:disabled {
background-color: #f0f0f0;
border-color: #cccccc;
}
"""
}
sheet = styles.get(theme, styles["default"])
toggle.setStyleSheet(sheet)
def set_button_style(self, button, theme, width, height):
button.setFixedWidth(width)
button.setFixedHeight(height)
if theme == "blue":
button.setStyleSheet("""
QPushButton {
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
padding: 8px 15px;
font-size: 12px;
font-weight: bold;
min-width: 100px;
}
QPushButton:hover {
background-color: #1976D2;
}
QPushButton:pressed {
background-color: #0D47A1;
}
QPushButton:disabled {
background-color: #BDBDBD;
color: #757575;
}
QPushButton::icon {
margin-right: 8px;
}
""")
elif theme == "yellow":
button.setStyleSheet("""
QPushButton {
background-color: #FFF690;
color: black;
border: none;
border-radius: 4px;
padding: 8px 15px;
font-size: 12px;
font-weight: bold;
min-width: 100px;
}
QPushButton:hover {
background-color: #FFE93B;
}
QPushButton:pressed {
background-color: #FFFF00;
}
QPushButton:disabled {
background-color: #BDBDBD;
color: #757575;
}
QPushButton::icon {
margin-right: 8px;
}
""")
elif theme == "gray":
button.setStyleSheet("""
QPushButton {
background-color: #808080;
color: white;
border: none;
border-radius: 4px;
padding: 8px 15px;
font-size: 12px;
font-weight: bold;
min-width: 100px;
}
QPushButton:hover {
background-color: #666666;
}
QPushButton:pressed {
background-color: #444444;
}
QPushButton:disabled {
background-color: #BDBDBD;
color: #757575;
}
QPushButton::icon {
margin-right: 8px;
}
""")
def create_login_group(self):
"""로그인 그룹을 생성합니다."""
self.admin_group_layout = QVBoxLayout()
# 관리자 ID 및 PW
self.admin_id_label = QLabel("관리자 ID:", self)
self.admin_id_input = QLineEdit(self)
self.admin_id_input.setPlaceholderText("관리자 ID 입력")
self.admin_id_input.setObjectName("admin_id_input")
self.admin_id_input.textChanged.connect(lambda text: self.universal_input_handler(self.admin_id_input, text))
# 관리자 PW
self.admin_pw_label = QLabel("관리자 PW:", self)
self.admin_pw_input = QLineEdit(self)
self.admin_pw_input.setEchoMode(QLineEdit.Password)
self.admin_pw_input.setPlaceholderText("관리자 PW 입력")
self.admin_pw_input.setObjectName("admin_pw_input")
self.admin_pw_input.textChanged.connect(lambda text: self.universal_input_handler(self.admin_pw_input, text))
# 관리자 ID/PW 영역
self.admin_id_layout = QHBoxLayout()
self.admin_id_layout.addWidget(self.admin_id_label)
self.admin_id_layout.addWidget(self.admin_id_input)
self.admin_group_layout.addLayout(self.admin_id_layout)
self.admin_pw_layout = QHBoxLayout()
self.admin_pw_layout.addWidget(self.admin_pw_label)
self.admin_pw_layout.addWidget(self.admin_pw_input)
self.admin_group_layout.addLayout(self.admin_pw_layout)
# 왼쪽: 관리자 토글 및 ID/PW 그룹
self.admin_group = QGroupBox("관리자 로그인")
self.admin_group.setStyleSheet("""
QGroupBox {
border: 1px solid #222222;
border-radius: 5px;
margin-top: 1ex;
padding: 10px;
background-color: #fdfdff;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 5px;
background-color: #dddddd;
}
""")
self.admin_group.setLayout(self.admin_group_layout)
return self.admin_group
def create_user_info_group(self):
"""사용자 정보 그룹을 생성합니다."""
# 오른쪽: 사용자 정보 그룹
self.user_info_group = QGroupBox("사용자 정보")
self.user_info_group.setStyleSheet("""
QGroupBox {
border: 1px solid #cccccc;
border-radius: 5px;
margin-top: 1ex;
padding: 10px;
background-color: #f0f8ff;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 5px;
background-color: #f0f8ff;
}
QLabel {
padding: 2px;
}
""")
self.user_info_layout = QVBoxLayout()
# 사용자 상세 정보 추가
nickname = self.user_info.get('nickname', '불명')
email = self.user_info.get('email', '이메일 없음')
current_sessions = self.user_info.get('current_sessions', 0)
max_sessions = self.user_info.get('max_session_limit', 1)
membership_level = self.user_info.get('membership_level', 'free').upper()
# 만료일 계산
expiry_date = self.user_info.get('payment_period_end', None)
days_left = "알 수 없음"
days_left_num = -1 # 숫자로 된 남은 일수 저장
if expiry_date:
try:
# ISO 형식 문자열을 datetime으로 변환
expiry_date_obj = datetime.fromisoformat(expiry_date.replace('Z', '+00:00'))
days_left_num = (expiry_date_obj - datetime.now()).days
if days_left_num < 0:
days_left = "만료됨"
else:
days_left = f"{days_left_num}"
except Exception as e:
self.logger.log(f"만료일 계산 오류: {str(e)}", level=logging.ERROR)
# 정보 라벨 생성
self.user_name_label = QLabel(f"이름: {nickname}")
self.user_email_label = QLabel(f"이메일: {email}")
self.user_sessions_label = QLabel(f"접속수: {current_sessions}/{max_sessions}")
self.user_membership_label = QLabel(f"멤버십: {membership_level}")
self.user_version_label = QLabel(f"버전: {self.version}")
# 남은 기간에 따른 아이콘 및 스타일 설정
# 기본 라벨 텍스트 생성
expiry_text = f"남은 기간: {days_left}"
# 아이콘 추가를 위한 레이아웃
expiry_layout = QHBoxLayout()
expiry_layout.setSpacing(5)
# 만료 라벨 생성
self.user_expiry_label = QLabel(expiry_text)
# 남은 일수에 따른 스타일 설정
if isinstance(days_left_num, int):
if days_left_num <= 3 and days_left_num >= 0:
# 3일 이하: 빨간색, 굵은 글씨, 위험 아이콘
self.user_expiry_label.setStyleSheet("""
font-weight: bold;
color: #FF0000;
padding: 3px;
""")
# 위험 아이콘
danger_icon = QLabel()
danger_icon.setText("⚠️")
expiry_layout.addWidget(danger_icon, 2)
expiry_layout.addWidget(self.user_expiry_label, 8)
# 로그 기록
self.logger.log(f"사용자 멤버십 만료 임박: {days_left_num}일 남음", level=logging.WARNING)
elif days_left_num <= 7 and days_left_num > 3:
# 7일 이하: 주황색, 경고 아이콘
self.user_expiry_label.setStyleSheet("""
font-weight: bold;
color: #FF8C00;
padding: 3px;
""")
# 경고 아이콘
warning_icon = QLabel()
warning_icon.setText("")
expiry_layout.addWidget(warning_icon, 2)
expiry_layout.addWidget(self.user_expiry_label, 8)
# 로그 기록
self.logger.log(f"사용자 멤버십 만료 주의: {days_left_num}일 남음", level=logging.INFO)
else:
# 7일 초과: 기본 스타일
self.user_expiry_label.setStyleSheet("""
font-weight: bold;
color: #333333;
padding: 3px;
""")
expiry_layout.addWidget(self.user_expiry_label)
else:
# 날짜를 계산할 수 없는 경우: 기본 스타일
self.user_expiry_label.setStyleSheet("""
font-weight: bold;
color: #333333;
padding: 3px;
""")
expiry_layout.addWidget(self.user_expiry_label)
# 라벨에 폰트 및 스타일 적용
info_style = """
font-weight: bold;
color: #333333;
padding: 3px;
"""
self.user_name_label.setStyleSheet(info_style)
self.user_email_label.setStyleSheet(info_style)
self.user_sessions_label.setStyleSheet(info_style)
self.user_membership_label.setStyleSheet(info_style)
self.user_version_label.setStyleSheet(info_style)
# 레이아웃에 추가
self.user_info_layout.addWidget(self.user_name_label)
self.user_info_layout.addWidget(self.user_email_label)
self.user_info_layout.addWidget(self.user_sessions_label)
self.user_info_layout.addWidget(self.user_membership_label)
self.user_info_layout.addWidget(self.user_version_label)
# 만료 기간 레이아웃 추가
expiry_widget = QWidget()
expiry_widget.setLayout(expiry_layout)
self.user_info_layout.addWidget(expiry_widget)
self.user_info_group.setLayout(self.user_info_layout)
return self.user_info_group
def create_Settings_buttons(self):
"""버튼 그룹을 생성합니다."""
setting_group = QGroupBox()
setting_layout = QHBoxLayout()
# ==== 상품설정 그룹 ====
setting_products_group = QGroupBox("상품설정")
setting_products_layout = QVBoxLayout()
# 상품설정 그룹 스타일
setting_products_group.setStyleSheet("""
QGroupBox {
border: 1px solid #d0d0d0;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
font-size: 11pt;
color: #111111;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 0 0 0;
font-size: 12pt;
font-weight: bold;
color: #333333;
}
""")
# 키고정 토글 설정
self.fix_title_count_widget = QWidget()
self.keyword_fix_toggle_layout = QHBoxLayout(self.fix_title_count_widget)
self.keyword_fix_toggle_label = QLabel("키고정", self)
self.keyword_fix_toggle_label.setFixedWidth(80)
self.keyword_fix_toggle = ToggleSwitch(self)
self.keyword_fix_toggle.setFixedHeight(40)
self.keyword_fix_toggle.setObjectName("keyword_fix_toggle")
self.keyword_fix_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.keyword_fix_toggle, checked))
self.keyword_fix_toggle_layout.addWidget(self.keyword_fix_toggle_label)
self.keyword_fix_toggle_layout.addWidget(self.keyword_fix_toggle)
setting_products_layout.addWidget(self.fix_title_count_widget)
# 키고정 수 설정
self.keyword_count_widget = QWidget()
self.keyword_count_layout = QHBoxLayout(self.keyword_count_widget)
self.keyword_fix_count_label = QLabel("키고정 수", self)
self.keyword_fix_count_label.setFixedWidth(80)
self.keyword_fix_count_input = QSpinBox(self)
self.keyword_fix_count_input.setFixedHeight(40)
self.keyword_fix_count_input.setObjectName("keyword_fix_count_input")
self.keyword_fix_count_input.setMinimum(1)
self.keyword_fix_count_input.setMaximum(8)
# self.keyword_fix_count_input.setValue(self.settings_manager.get_value("fixed_keywords_count", 2))
self.keyword_fix_count_input.setToolTip("고정할 키워드 개수 (1~8)")
self.keyword_fix_count_input.setFixedWidth(80)
self.keyword_fix_count_input.valueChanged.connect(lambda value: self.universal_input_handler(self.keyword_fix_count_input, value))
self.keyword_count_layout.addWidget(self.keyword_fix_count_label)
self.keyword_count_layout.addWidget(self.keyword_fix_count_input)
setting_products_layout.addWidget(self.keyword_count_widget)
# 상품명 최대 길이 설정
self.title_length_limit_widget = QWidget()
self.title_length_limit_layout = QHBoxLayout(self.title_length_limit_widget)
self.title_length_limit_label = QLabel("상품명 최대 길이", self)
self.title_length_limit_input = QSpinBox(self)
self.title_length_limit_input.setObjectName("title_length_limit_input")
self.title_length_limit_input.setMinimum(10)
self.title_length_limit_input.setMaximum(50)
self.title_length_limit_input.setValue(self.settings_manager.get_value("title_length_limit", 35))
self.title_length_limit_input.setToolTip("상품명 최대 길이 (10~50)")
self.title_length_limit_input.setFixedWidth(80)
self.title_length_limit_input.valueChanged.connect(lambda value: self.universal_input_handler(self.title_length_limit_input, value))
setting_products_layout.addWidget(self.title_length_limit_widget)
# 썸네일 변경 토글 설정
self.thumbnail_change_widget = QWidget()
self.thumbnail_change_toggle_layout = QHBoxLayout(self.thumbnail_change_widget)
self.thumbnail_change_toggle_label = QLabel("썸네일 변경", self)
self.thumbnail_change_toggle_label.setFixedWidth(80)
self.thumbnail_change_toggle = ToggleSwitch(self)
self.thumbnail_change_toggle.setFixedHeight(40)
self.thumbnail_change_toggle.setObjectName("thumbnail_change_toggle")
self.thumbnail_change_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.thumbnail_change_toggle, checked))
self.thumbnail_change_toggle_layout.addWidget(self.thumbnail_change_toggle_label)
self.thumbnail_change_toggle_layout.addWidget(self.thumbnail_change_toggle)
setting_products_layout.addWidget(self.thumbnail_change_widget)
setting_products_group.setLayout(setting_products_layout)
setting_layout.addWidget(setting_products_group,7)
# ==== 주요설정 그룹 ====
setting_buttons_group = QGroupBox("주요설정")
setting_buttons_layout = QVBoxLayout()
# 설정열기 버튼 추가
self.biz_settings_button = QPushButton("사업자설정", self)
self.set_button_style(self.biz_settings_button, "yellow", 120, 50)
self.biz_settings_button.clicked.connect(self.on_click_biz_settings_button)
setting_buttons_layout.addWidget(self.biz_settings_button)
# 금지어 버튼 추가
self.forbbidenWord_button = QPushButton('금지어', self)
self.set_button_style(self.forbbidenWord_button, "yellow", 120, 50)
self.forbbidenWord_button.clicked.connect(self.on_forbbidenWord_button_clicked)
setting_buttons_layout.addWidget(self.forbbidenWord_button)
# 로그 버튼 추가
self.log_button = QPushButton('로그', self)
self.set_button_style(self.log_button, "yellow", 120, 50)
self.log_button.clicked.connect(self.show_log_dialog)
setting_buttons_layout.addWidget(self.log_button)
setting_buttons_group.setLayout(setting_buttons_layout)
setting_layout.addWidget(setting_buttons_group,3)
setting_group.setLayout(setting_layout)
return setting_group
def create_admin_layout(self):
"""관리자 레이아웃을 생성합니다."""
# 로그인 영역을 수정하여 관리자/사용자 정보를 그룹으로 묶고 좌우로 배치
self.login_info_layout = QHBoxLayout()
login_group = self.create_login_group()
user_info_group = self.create_user_info_group()
# 레이아웃에 추가
self.login_info_layout.addWidget(login_group)
self.login_info_layout.addWidget(user_info_group)
return self.login_info_layout
def create_job_group(self):
self.job_group = QGroupBox("편집 작업")
self.job_group.setFixedHeight(200)
self.job_group.setStyleSheet("""
QGroupBox {
border: 1px solid #d0d0d0;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
font-size: 11pt;
color: #111111;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 0 0 0;
font-size: 12pt;
font-weight: bold;
color: #333333;
}
""")
self.job_group_layout = QGridLayout()
# 크롬 실행 버튼 및 번역 버튼
self.start_chrome_button = QPushButton('로그인', self)
self.start_chrome_button.setToolTip("크롬 브라우저를 실행하여 업로드 알바생 로그인 후 등록상품 페이지로 이동합니다.")
self.start_chrome_button.setObjectName("start_chrome_button")
self.start_chrome_button.clicked.connect(self.start_browser_thread)
self.Change_Business_button = QPushButton('사업자변경', self)
self.Change_Business_button.setToolTip("현재 사업자 변경")
self.Change_Business_button.setEnabled(False)
self.Change_Business_button.clicked.connect(self.on_start_PercentyJob_clicked)
self.Delete_UploadStatus_button = QPushButton('업로드\n기록삭제', self)
self.Delete_UploadStatus_button.setToolTip("현재페이지 상품들이 업로드상태면 업로드 정보삭제")
self.Delete_UploadStatus_button.setEnabled(False)
self.Delete_UploadStatus_button.clicked.connect(self.on_start_PercentyJob_clicked)
self.Shuffle_ProductName_button = QPushButton('상품명셔플', self)
self.Shuffle_ProductName_button.setToolTip("상품명 셔플 시작")
self.Shuffle_ProductName_button.setEnabled(False)
self.Shuffle_ProductName_button.clicked.connect(self.on_start_PercentyJob_clicked)
self.set_PUSHBTN_style(self.start_chrome_button, "modern")
self.set_PUSHBTN_style(self.Change_Business_button, "neumorphism")
self.set_PUSHBTN_style(self.Delete_UploadStatus_button, "smooth")
self.set_PUSHBTN_style(self.Shuffle_ProductName_button, "default")
self.job_group_layout.addWidget(self.start_chrome_button, 0, 0, 1, 1)
self.job_group_layout.addWidget(self.Change_Business_button, 0, 1, 1, 1)
self.job_group_layout.addWidget(self.Delete_UploadStatus_button, 0, 2, 1, 1)
self.job_group_layout.addWidget(self.Shuffle_ProductName_button, 0, 3, 1, 1)
self.job_group.setLayout(self.job_group_layout)
return self.job_group
def create_progress_layout(self):
# 전체 프로그레스바 생성 및 스타일 적용
self.total_progress_bar = QProgressBar(self)
self.total_progress_bar.setFormat("상품 수정 대기")
self.total_progress_bar.setValue(0)
self.total_progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.total_progress_bar.setTextVisible(True)
self.total_progress_bar.setStyleSheet("""
QProgressBar {
border: none;
background-color: transparent;
text-align: center;
font: 12pt 'Segoe UI';
color: #4A4A4A;
height: 25px;
}
QProgressBar::chunk {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #4A90E2, stop:1 #007AFF);
border: none;
}
""")
# 스테이지 타임라인
self.stageTimeline_layout = QHBoxLayout()
self.stages = ["상품명", "옵션", "가격", "썸네일", "태그", "상페"]
self.stage_labels = []
for stage in self.stages:
# self.stage_layout = QHBoxLayout()
label = QLabel(stage)
label.setStyleSheet("background-color: lightgray; padding: 3px;")
self.stage_labels.append(label)
# self.stage_layout.addWidget(label)
# self.stageTimeline_layout.addLayout(self.stage_layout)
self.stageTimeline_layout.addWidget(label) # 수정: QLabel을 추가할 때 addWidget() 사용
# 디테일 프로그레스바
self.detail_progress_bar = QProgressBar(self)
self.detail_progress_bar.setValue(0)
self.detail_progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.detail_progress_bar.setVisible(False)
self.detail_progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #d0d0d0;
border-radius: 5px;
background-color: #f9f9f9;
text-align: center;
font: 10pt 'Segoe UI';
color: #333333;
height: 20px;
}
QProgressBar::chunk {
background-color: #3daee9;
border-radius: 5px;
}
""")
self.progress_layout = QVBoxLayout()
self.progress_layout.addWidget(self.total_progress_bar)
self.progress_layout.addLayout(self.stageTimeline_layout)
self.progress_layout.addWidget(self.detail_progress_bar)
return self.progress_layout
def create_log_layout(self):
# 로그
self.log_display = QTextEdit(self)
self.log_display.setReadOnly(True)
self.log_layout = QVBoxLayout()
self.log_layout.addWidget(self.log_display)
self.logger.set_gui_logger(self.append_log, __gui_log_level__)
return self.log_layout
def initUI(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
self.main_layout = QVBoxLayout(central_widget)
# self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.setGeometry(QRect(800, 100, 600, 800))
self.setWindowTitle('업로드알바생')
# 설정 초기화
self.init_settings()
# 메뉴바 생성
self.create_menu_bar()
# 관리자 레이아웃 생성
self.admin_layout = self.create_admin_layout()
self.main_layout.addLayout(self.admin_layout)
# 편집 작업 레이아웃 생성
self.job_group_widget = self.create_job_group()
self.main_layout.addWidget(self.job_group_widget)
# 버튼 레이아웃 생성
self.setting_Buttons_group = self.create_Settings_buttons()
self.main_layout.addWidget(self.setting_Buttons_group)
# 프로그레스 레이아웃 생성
self.progress_layout = self.create_progress_layout()
self.main_layout.addLayout(self.progress_layout)
# 로그 레이아웃 생성
self.log_layout = self.create_log_layout()
self.main_layout.addLayout(self.log_layout)
self.setLayout(self.main_layout)
def update_stage_timeline(self, active_stages: list):
"""
active_stages: 사용자가 선택한 작업 항목의 리스트 (예: ["상품명", "옵션"])
"""
# 기존 레이아웃의 모든 위젯 제거
while self.stageTimeline_layout.count():
item = self.stageTimeline_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
# 새로운 스테이지 레이블 리스트 초기화
self.stage_labels = []
# 선택한 작업 항목만 레이블로 추가
for stage in active_stages:
label = QLabel(stage)
label.setStyleSheet("background-color: lightgray; padding: 3px;")
self.stage_labels.append(label)
self.stageTimeline_layout.addWidget(label)
def kill_autohotkey_process(self):
"""
실행 중인 프로세스 중 이름이 "AutoHotkey.exe"인 프로세스가 있으면 종료시킵니다.
"""
self.logger.log("AutoHotkey 프로세스 종료 검사 시작", level=logging.INFO)
found = False
for proc in psutil.process_iter(['name', 'pid']):
try:
if proc.info['name'] and proc.info['name'].lower() == "autohotkey.exe":
found = True
pid = proc.info['pid']
self.logger.log(f"AutoHotkey 프로세스 발견 (PID: {pid}). 종료 시도합니다.", level=logging.INFO)
proc.terminate()
try:
proc.wait(timeout=5)
self.logger.log(f"프로세스 (PID: {pid}) 정상 종료됨.", level=logging.INFO)
except psutil.TimeoutExpired:
self.logger.log(f"프로세스 (PID: {pid})가 종료되지 않아 강제 종료합니다.", level=logging.WARNING)
proc.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
self.logger.log(f"프로세스 종료 중 에러 발생: {e}", level=logging.ERROR, exc_info=True)
if not found:
self.logger.log("실행 중인 AutoHotkey 프로세스가 없습니다.", level=logging.INFO)
def update_group_items(self, is_admin: bool):
"""관리자 여부에 따라 그룹 선택 항목 변경"""
self.group_selector.clear() # 기존 아이템 제거
if is_admin:
# 관리자 계정: 20개 그룹
self.group_selector.addItems([f"{i}" for i in range(1, 21)])
else:
# 직원 계정: 3개 그룹
self.group_selector.addItems(["1번그룹", "2번그룹", "3번그룹"])
self.group_selector.setCurrentIndex(0) # 기본값 설정
def update_client_info(self):
"""Client ID와 Client Secret을 업데이트"""
client_id = self.client_id_input.text()
client_secret = self.client_secret_input.text()
self.toggle_states['client_id'] = client_id
self.toggle_states['client_secret'] = client_secret
self.show_message("네이버 API 업데이트", "네이버 API 정보가 업데이트되었습니다.")
# def on_group_selected_ori(self):
# """그룹 선택 변경 시 호출"""
# import re
# try:
# # 정규식으로 숫자만 추출
# match = re.search(r'\d+', self.group_selector.currentText())
# if match:
# self.toggle_states['group_index'] = int(match.group())
# self.logger.log(f"선택된 그룹이 변경되었습니다: {self.toggle_states['group_index']}", level=logging.DEBUG)
# else:
# # 숫자가 없을 경우 처리
# self.logger.log(f"선택된 그룹에 숫자가 없습니다: {self.group_selector.currentText()}", level=logging.DEBUG)
# self.toggle_states['group_index'] = None
# except Exception as e:
# # 기타 예외 처리
# self.logger.log(f"그룹 선택 처리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
# self.toggle_states['group_index'] = None
def show_message(self, title: str, message: str):
"""
공통적으로 메시지 박스를 표시하는 메서드
:param title: 메시지 박스의 제목
:param message: 메시지 박스의 내용
"""
if not hasattr(self, "_message_box"):
self._message_box = QMessageBox(self) # 메시지 박스를 한 번만 생성
self._message_box.setIcon(QMessageBox.Information)
self._message_box.setWindowTitle(title)
self._message_box.setText(message)
self._message_box.setStandardButtons(QMessageBox.Ok)
self._message_box.exec_()
def set_layout_visibility(self, changelayout, visible):
"""레이아웃이나 그룹박스의 가시성을 설정"""
# QGroupBox인 경우
if isinstance(changelayout, QGroupBox):
changelayout.setVisible(visible)
# 레이아웃(QLayout)인 경우
elif hasattr(changelayout, 'count'):
for i in range(changelayout.count()):
widget = changelayout.itemAt(i).widget()
if widget:
widget.setVisible(visible)
# 다른 위젯인 경우
else:
changelayout.setVisible(visible)
def universal_input_handler(self, widget, *args):
"""
어떤 위젯에서든 호출되는 범용 입력 핸들러.
위젯 타입에 따라 알맞은 개별 핸들러로 분기.
"""
# 토글/체크박스/콤보박스 (on/off 또는 선택값)
if hasattr(widget, "isChecked") or hasattr(widget, "currentIndex"):
self.handle_toggle_state(widget)
# QSpinBox, QDoubleSpinBox (숫자)
elif hasattr(widget, "value") and hasattr(widget, "setValue"):
self.handle_spinbox_input(widget)
# QLineEdit, QTextEdit 등 텍스트
elif hasattr(widget, "text") or hasattr(widget, "toPlainText"):
self.handle_text_input(widget)
else:
self.logger.log(f"[universal_input_handler] '{widget.objectName()}' 지원되지 않는 위젯", level=logging.DEBUG)
def handle_toggle_state(self, widget, value=None):
widget_name = widget.objectName()
config = self.widget_map.get(widget_name, None)
if config is None:
self.logger.log(f"[handle_toggle_state] '{widget_name}' 위젯맵에 없음.", level=logging.DEBUG)
return
# value 자동 감지
if value is None:
if hasattr(widget, "isChecked"):
value = widget.isChecked()
elif hasattr(widget, "currentData"):
value = widget.currentData()
elif hasattr(widget, "currentText"):
value = widget.currentText()
else:
value = True # fallback
# on/off/값 분기
if isinstance(value, bool):
key = "on" if value else "off"
else:
key = value
# dependents enable 처리
dependents = config.get("dependents", {})
all_dependents = set(w for v in dependents.values() for w in v if w)
for dep in all_dependents:
dep_widget = getattr(self, dep, None)
if dep_widget:
dep_widget.setEnabled(False)
# self.logger.log(f"[handle_toggle_state] {dep} 비활성화", level=logging.DEBUG)
for dep in dependents.get(key, []):
dep_widget = getattr(self, dep, None)
if dep_widget:
dep_widget.setEnabled(True)
# self.logger.log(f"[handle_toggle_state] {dep} 활성화", level=logging.DEBUG)
# visible 처리
visible_map = config.get("visible", {})
all_vis = set(w for v in visible_map.values() for w in v if w)
for vis in all_vis:
vis_widget = getattr(self, vis, None)
if vis_widget:
vis_widget.setVisible(False)
# self.logger.log(f"[handle_toggle_state] {vis} 숨김", level=logging.DEBUG)
for vis in visible_map.get(key, []):
vis_widget = getattr(self, vis, None)
if vis_widget:
vis_widget.setVisible(True)
# self.logger.log(f"[handle_toggle_state] {vis} 보임", level=logging.DEBUG)
# settings_manager에 값 저장
set_key = config.get("key", widget_name)
self.settings_manager.save_value(set_key, value)
# toggle_states에 동기화
key_for_state = self.get_state_key(widget_name)
self.toggle_states[key_for_state] = value
self.logger.log(f"[handle_toggle_state] {widget_name} 상태변경: {key}", level=logging.DEBUG)
def handle_text_input(self, widget):
"""
QLineEdit/QTextEdit 등 텍스트 입력 위젯의 값이 바뀔 때 호출.
"""
widget_name = widget.objectName()
config = self.widget_map.get(widget_name, None)
if config is None:
self.logger.log(f"[handle_text_input] '{widget_name}' 위젯맵에 없음.", level=logging.DEBUG)
return
# 값 추출
if hasattr(widget, "text"): # QLineEdit
value = widget.text()
elif hasattr(widget, "toPlainText"): # QTextEdit
value = widget.toPlainText()
else:
value = ""
# 값 저장
set_key = config.get("key", widget_name)
self.settings_manager.save_value(set_key, value)
# toggle_states에 동기화
key_for_state = self.get_state_key(widget_name)
self.toggle_states[key_for_state] = value
self.logger.log(f"[handle_text_input] {widget_name} 값 저장: {value}", level=logging.DEBUG)
def handle_spinbox_input(self, widget):
"""
QSpinBox/QDoubleSpinBox 등 숫자 입력 위젯의 값이 바뀔 때 호출.
"""
widget_name = widget.objectName()
config = self.widget_map.get(widget_name, None)
if config is None:
self.logger.log(f"[handle_spinbox_input] '{widget_name}' 위젯맵에 없음.", level=logging.DEBUG)
return
# 값 추출
if hasattr(widget, "value"):
value = widget.value()
else:
value = 0
set_key = config.get("key", widget_name)
self.settings_manager.save_value(set_key, value)
self.logger.log(f"[handle_spinbox_input] {widget_name} 값 저장: {value}", level=logging.DEBUG)
# toggle_states에 동기화
key_for_state = self.get_state_key(widget_name)
self.toggle_states[key_for_state] = value
def close_pause_message(self):
"""일시정지 메시지 창을 닫는 메서드"""
if hasattr(self, 'pause_message_box') and self.pause_message_box:
self.pause_message_box.close()
self.pause_message_box = None
self.logger.log("일시정지 확인 - 안내 메시지 닫힘", level=logging.DEBUG)
def on_click_biz_settings_button(self):
"""크무비 설정 실행 버튼 클릭 시 호출"""
self.logger.log('사업자 설정 버튼 클릭됨', level=logging.DEBUG)
self.biz_dialog.show()
def on_forbbidenWord_button_clicked(self):
"""금지어 관리 버튼 클릭 시 호출"""
self.logger.log("금지어 관리 버튼 클릭됨", level=logging.DEBUG)
self.keyword_manager.exec()
def on_shuffle_upload_button_clicked(self):
"""셔플업로드 버튼 클릭 시 호출"""
self.logger.log("셔플업로드 버튼 클릭됨", level=logging.DEBUG)
self.shuffle_upload_widget.show()
def on_detail_text_button_clicked(self):
"""매뉴얼 버튼 클릭 시 호출"""
try:
self.logger.log("상페텍스트 버튼 클릭됨", level=logging.DEBUG)
self.detail_text_widget.show()
except Exception as e:
self.logger.log(f"상페텍스트 관리 중 오류 발생 {e}", level=logging.ERROR, exc_info=True)
def on_cmb_button_clicked(self):
"""크무비 설정 실행 버튼 클릭 시 호출"""
self.logger.log('크무비 설정 버튼 클릭됨', level=logging.DEBUG)
self.price_setting_diag.show()
# def get_toggle_states(self):
# """현재 토글 상태를 딕셔너리로 반환"""
# try:
# # 토글 상태 읽기
# self.toggle_states['title'] = self.title_toggle.isChecked()
# self.toggle_states['title_shuffle'] = self.title_shuffle_toggle.isChecked()
# self.toggle_states['title_trans_type'] = self.title_trans_type_toggle.isChecked()
# self.toggle_states['use_lens'] = self.use_lens_toggle.isChecked()
# self.toggle_states['ocr'] = self.ocr_toggle.isChecked()
# self.toggle_states['use_API'] = self.use_API_toggle.isChecked()
# self.toggle_states['optionTrnas'] = self.optionTrnas_toggle.isChecked()
# self.toggle_states['optionIMGTrans'] = self.optionIMGTrans_toggle.isChecked()
# self.toggle_states['optionIMGTrans_type'] = self.optionIMGTrans_type_toggle.isChecked()
# self.toggle_states['optionAutoSelect'] = self.optionAutoSelect_toggle.isChecked()
# self.toggle_states['price'] = self.price_toggle.isChecked()
# self.toggle_states['tag'] = self.tag_toggle.isChecked()
# self.toggle_states['thumb'] = self.thumb_toggle.isChecked()
# self.toggle_states['thumb_trans_type'] = self.thumb_trans_type_toggle.isChecked()
# self.toggle_states['thumb_nukki'] = self.thumb_nukki_toggle.isChecked() # 썸네일 누끼 토글 상태 저장
# self.toggle_states['detail_Option'] = self.detail_Option_toggle.isChecked()
# self.toggle_states['detail_IMGTrans'] = self.detail_IMGTrans_toggle.isChecked()
# self.toggle_states['detail_IMGTrans_type'] = False # 상태 설정 안함
# self.toggle_states['debug_mode'] = self.debug_toggle.isChecked()
# # self.toggle_states['ed_mode'] = self.ed_mode_toggle.isChecked()
# self.toggle_states['discord'] = self.discord_notify_toggle.isChecked()
# self.toggle_states['watermark'] = self.watermark_toggle.isChecked()
# self.toggle_states['cat_rec'] = self.cat_rec_toggle.isChecked() # 카테 추천 토글 상태 저장
# self.toggle_states['fixed_keywords'] = self.keyword_fix_toggle.isChecked() # 키고정 토글 상태 저장
# self.toggle_states['remove_overprice'] = self.remove_overprice_toggle.isChecked() # 가격초과제외 토글 상태 저장
# # 기타 설정 값들도 저장
# self.toggle_states['discord_webhook'] = self.webhook_input.text()
# self.toggle_states['watermark_text'] = self.watermark_text_input.text()
# self.toggle_states['thumb_rmb_count'] = int(self.thumb_rmb_count_input.text())
# self.toggle_states['max_option_count'] = int(self.max_option_count_input.text())
# self.toggle_states['opacity_percent'] = int(self.opacity_percent_input.text())
# self.toggle_states['group_index'] = self.group_selector.currentIndex()
# self.toggle_states['fixed_keywords_count'] = int(self.keyword_fix_count_input.text()) # 키고정 개수 저장
# return self.toggle_states
# except Exception as e:
# self.logger.log(f"토글 상태 저장 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
# return {}
def update_total_progress(self, current_value, total_value):
if current_value == 0 or total_value <= 0:
self.reset_stages()
self.total_progress_bar.setValue(0)
self.total_progress_bar.setFormat(f"상품 {current_value}/{total_value}개 완료 [{0}%]")
self.logger.log(f"전체 진행률: {current_value}/{total_value} (0%)", level=logging.DEBUG)
else:
percentage = int((current_value / total_value) * 100)
self.total_progress_bar.setValue(percentage)
self.total_progress_bar.setFormat(f"상품 {current_value}/{total_value}개 완료 [{percentage}%]")
self.logger.log(f"전체 진행률: {current_value}/{total_value} ({percentage}%)", level=logging.DEBUG)
# def update_total_progress(self, current_value, total_value):
# percentage = int((current_value / total_value) * 100)
# self.total_progress_bar.setValue(percentage)
# self.total_progress_bar.setFormat(f"상품 {current_value}/{total_value}개 완료 [{percentage}%]")
# self.logger.log(f"전체 진행률: {current_value}/{total_value} ({percentage}%)", level=logging.DEBUG)
def update_detail_progress(self, current_value, total_value):
if total_value <= 0:
# 전체 작업 수가 0이면 초기 상태로 설정
self.detail_progress_bar.setValue(0)
self.detail_progress_bar.setFormat("수정 대기")
else:
percentage = int((current_value / total_value) * 100)
self.detail_progress_bar.setValue(percentage)
# 진행률 포맷: "이미지 번역: 3/10 (30%) 완료"
self.detail_progress_bar.setFormat(f"이미지 번역: {current_value}/{total_value} ({percentage}%) 완료")
def on_captcha_detected(self, msg=None):
dlg = QDialog(self)
dlg.setWindowTitle("캡차발생")
label = QLabel("캡차를 해결하고 아래 해결버튼을 눌러주세요.", dlg)
btn_solve = QPushButton("해결", dlg)
btn_cancel = QPushButton("취소", dlg)
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(btn_solve)
layout.addWidget(btn_cancel)
dlg.setLayout(layout)
def handle_solve():
self.browser_controller.whale_translator.whale_ready_with_resolved_capcha = True
dlg.accept() # 다이얼로그 닫기
def handle_cancel():
label.setText("캡차가 해결되지 않았습니다.")
btn_solve.setEnabled(False)
btn_cancel.setEnabled(False)
QTimer.singleShot(3000, dlg.reject) # 3초 후 다이얼로그 닫기
btn_solve.clicked.connect(handle_solve)
btn_cancel.clicked.connect(handle_cancel)
dlg.exec() # 다이얼로그 실행(모달)
# dlg.accept()로 닫혔는지, reject()로 닫혔는지 구분할 수도 있음
def closeEvent(self, event):
"""창 닫기 시 스레드 및 리소스 종료"""
try:
# Supabase에 unwanted_words 동기화
self.sync_unwanted_words_to_supabase()
self.logger.log('프로그램을 종료합니다...', level=logging.INFO)
# 현재 설정 저장
self.save_settings()
# DB 동기화: 가격 설정과 카테고리 데이터를 Supabase에 동기화
if hasattr(self, 'db_manager') and self.db_manager:
# 가격 설정 동기화
user_id = getattr(self, 'sp_user_id', None)
if user_id:
success = self.db_manager.sync_price_settings_to_supabase(user_id)
if success:
self.logger.log(f"종료 시 가격 설정이 Supabase에 동기화되었습니다.", level=logging.INFO)
else:
self.logger.log(f"종료 시 가격 설정 Supabase 동기화 실패", level=logging.WARNING)
# base_category 동기화 (관리자만 가능)
is_admin = getattr(self, 'is_admin', False)
if is_admin:
success = self.db_manager.sync_base_categories_to_supabase()
if success:
self.logger.log(f"종료 시 카테고리 데이터가 Supabase에 동기화되었습니다.", level=logging.INFO)
else:
self.logger.log(f"종료 시 카테고리 데이터 Supabase 동기화 실패", level=logging.WARNING)
# Playwright 및 이벤트 루프 정리
# asyncio.run(self.cleanup_resources())
# 브라우저 컨트롤러 스레드 종료
if self.browser_controller.isRunning():
self.browser_controller.request_cleanup() # 아래 참고
self.browser_controller.terminate()
self.browser_controller.wait(3000)
if self.browser_controller.isRunning():
self.logger.log('스레드가 종료되지 않아 강제 종료를 시도합니다.', level=logging.WARNING)
self.browser_controller.terminate() # 강제 종료
# 세션 종료
self.supabase_manager.close_session()
self.logger.log("세션 종료 완료", level=logging.INFO)
if self.browser_controller.whale_translator:
self.browser_controller.whale_translator.close_trans_browser()
# Qt 메인 이벤트 루프 종료
QApplication.quit()
event.accept()
super().closeEvent(event)
except Exception as e:
self.logger.log(f"프로그램 종료 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
event.accept()
async def cleanup_resources(self):
"""Playwright 및 이벤트 루프 정리"""
try:
self.logger.log("Playwright 리소스 정리를 시작합니다.", level=logging.INFO)
# 모든 페이지 닫기
if self.browser_controller.browser:
self.logger.log("열린 페이지를 닫습니다...", level=logging.INFO)
for page in self.browser_controller.browser.pages:
try:
await asyncio.wait_for(page.close(), timeout=1) # 페이지 닫기에 타임아웃 적용
self.logger.log(f"페이지 {page.url} 닫기 완료.", level=logging.INFO)
except asyncio.TimeoutError:
self.logger.log(f"페이지 {page.url} 닫기 타임아웃 발생. 강제 종료를 시도합니다.", level=logging.WARNING)
# 브라우저 닫기
self.logger.log("브라우저를 닫습니다...", level=logging.INFO)
try:
await asyncio.wait_for(self.browser_controller.browser.close(), timeout=1)
self.logger.log("브라우저 종료 완료.", level=logging.INFO)
except asyncio.TimeoutError:
self.logger.log("브라우저 종료가 타임아웃되었습니다. 강제 종료를 시도합니다.", level=logging.WARNING)
self.browser_controller.force_terminate_browser()
# Playwright 종료
if self.browser_controller.playwright:
self.logger.log('Playwright 종료 중...', level=logging.INFO)
await self.browser_controller.playwright.stop()
self.logger.log('Playwright 종료 완료.', level=logging.INFO)
# 이벤트 루프 종료
if self.browser_controller.loop and not self.browser_controller.loop.is_closed():
self.browser_controller.loop.call_soon_threadsafe(self.browser_controller.loop.stop)
self.logger.log('이벤트 루프 종료 완료.', level=logging.INFO)
except Exception as e:
self.logger.log(f"리소스 정리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
@Slot(int)
def start_stage(self, stage_index):
"""지정한 단계에 작업중 상태(노란색)를 적용"""
if 0 <= stage_index < len(self.stage_labels):
label = self.stage_labels[stage_index]
label.setStyleSheet("background-color: yellow; padding: 5px;")
@Slot(int)
def complete_stage(self, stage_index):
"""단계 완료 시 해당 단계의 색상을 초록색으로 변경하고 다음 단계로 진행"""
if 0 <= stage_index < len(self.stage_labels):
label = self.stage_labels[stage_index]
label.setStyleSheet("background-color: green; padding: 5px;")
self.current_stage_index += 1
# 다음 단계가 있다면 자동으로 작업중 상태 적용
if self.current_stage_index < len(self.stage_labels):
self.start_stage(self.current_stage_index)
def reset_stages(self):
"""
스테이지 진행 상태를 초기화합니다.
- 현재 진행 중인 스테이지 인덱스를 0으로 재설정
- 모든 스테이지 레이블의 스타일을 기본 상태(대기: lightgray)로 변경
"""
self.logger.log("스테이지 초기화", level=logging.DEBUG)
self.current_stage_index = 0
for label in self.stage_labels:
label.setStyleSheet("background-color: lightgray; padding: 5px;")
@Slot()
def start_browser_thread(self):
"""브라우저 스레드 시작 및 GUI 상태 전달"""
self.start_chrome_button.setEnabled(False)
self.browser_controller.start()
time.sleep(1)
if self.browser_controller.isRunning():
# 스레드를 처음 시작하여 이벤트 루프를 실행
# self.browser_controller.start() # QThread의 start() 호출로 run() 실행
self.logger.log("브라우저 스레드가 시작되었습니다.", level=logging.DEBUG)
self.browser_controller.login_infos = {
'admin_id': self.admin_id_input.text(),
'admin_pw': self.admin_pw_input.text(),
}
# 로그인 정보 저장
self.save_settings()
# 스레드 시작
self.browser_controller.start_browser_task()
else:
self.logger.log("브라우저 스레드가 실행중이지 않습니다.", level=logging.WARNING)
@Slot()
def on_browser_started(self):
"""브라우저 시작 완료 시 처리할 로직"""
self.logger.log("브라우저가 성공적으로 시작되었습니다.", level=logging.INFO)
# 버튼 상태 활성화&비활성화
# self.start_chrome_button.setEnabled(False)
@Slot(str)
def on_unknown_browser_error(self, error_message):
"""브라우저 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 시작 중 알수없는 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.critical(self, "오류", f"브라우저 시작 중 알수없는 오류 발생 \n 관리자에게 로그를 전송해주세요. \n 에러메세지\n{error_message}")
@Slot(str)
def on_browser_create_error(self, error_message):
"""브라우저 생성 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 생성 중 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.critical(self, "오류", f"브라우저 생성 중 오류 발생 \n프로그램 재설치 또는 Microfost 재배포가능 패키지 모두 삭제 후 재부팅&재설치 해주세요. \n에러메세지\n{error_message}")
sys.exit(1)
@Slot(str)
def on_browser_login_error(self, error_message):
"""브라우저 로그인 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 로그인 중 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.warning(self, "오류", f"브라우저 로그인 중 오류 발생\n 로그인 정보를 확인해 주세요.\n [에러메세지]\n{error_message}")
self.start_chrome_button.setEnabled(True)
@Slot(str)
def on_browser_ad1_close_error(self, error_message):
"""브라우저 광고1 닫기 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 광고1 닫기 중 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.warning(self, "오류", f"로그인 후 첫페이지 광고가 닫히지 않았습니다.\n 사용하시는 브라우저에서 로그인 후 해당 광고를 '다시보지않기'로 해 주세요 \n 에러메세지\n{error_message}")
@Slot(str)
def on_browser_ad2_close_error(self, error_message):
"""브라우저 광고2 닫기 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 광고2 닫기 중 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.warning(self, "오류", f"신규상품 페이지로 이동 전 광고가 닫히지 않았습니다.\n 1. 사용하시는 브라우저에서 로그인 후 해당 광고를 '다시보지않기'로 해 주세요 \n 2. 디버그모드를 켜고 알바생 로그인을 해 주세요, 그런 후 신규상품페이지로 이동할때까지 발생하는 모든 광고를 다시보지 않기로 해주세요. \n 에러메세지\n{error_message}")
@Slot(str)
def on_browser_group_list_error(self, error_message):
"""브라우저 그룹 목록 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 그룹 목록 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.warning(self, "오류", f"브라우저 그룹 목록 오류 발생\n 직원계정에 그룹배정이 되어있는지 확인해 주세요. \n 에러메세지\n{error_message}")
@Slot(str)
def on_browser_handler_update_error(self, error_message):
"""브라우저 핸들러 업데이트 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 핸들러 업데이트 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.critical(self, "오류", f"브라우저 핸들러 업데이트 오류 발생\n 프로그램 재설치 또는 관리자에게 로그를 전송해주세요. \n 에러메세지\n{error_message}")
sys.exit(1)
@Slot(str)
def on_browser_parsing_page_error(self, error_message):
"""브라우저 페이지 파싱 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 파싱페이지 생성 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.warning(self, "오류", f"브라우저 파싱페이지 생성 오류 발생: {error_message}")
@Slot(str)
def on_browser_registered_product_page_error(self, error_message):
"""브라우저 등록된 상품 페이지 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 등록된 상품 페이지 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.warning(self, "오류", f"브라우저 등록된 상품 페이지 이동 중 오류 발생: {error_message}")
@Slot(str)
def on_browser_new_product_page_error(self, error_message):
"""브라우저 신상품 페이지 오류 발생 시 처리할 로직"""
self.logger.log(f"브라우저 신상품 페이지 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
QMessageBox.warning(self, "오류", f"브라우저 신상품 페이지 이동 중 오류 발생: {error_message}")
# @Slot(str)
# def on_ocr_processor_error(self, error_message):
# """OCR 프로세서 오류 발생 시 처리할 로직"""
# self.logger.log(f"OCR 프로세서 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
# QMessageBox.warning(self, "경고", f"OCR 프로세서 생성 오류 발생\n OCR 프로세서 생성오류로 OCR기능이 비활성화됩니다. {error_message}")
def on_group_selected(self):
"""그룹 선택 변경 시 호출"""
group_index = self.group_selector.currentIndex()
self.toggle_states['group_index'] = group_index
self.logger.log(f"선택된 그룹이 변경되었습니다: {group_index}", level=logging.DEBUG)
# @Slot(list)
# def update_group_list(self, group_names_list: list):
# """
# 그룹 이름 리스트를 업데이트합니다.
# """
# if not isinstance(group_names_list, list) or not group_names_list:
# QMessageBox.critical(self, "오류", "그룹 선택에 실패했습니다. 프로그램을 재실행 해주세요.")
# sys.exit(1)
# else:
# self.group_selector.clear()
# self.group_selector.addItems(group_names_list)
# self.group_selector.setEnabled(True)
# self.group_change_button.setEnabled(True)
# @Slot(list)
# def update_group_list(self, group_names_list: list):
# # supabase에서 현재 작업중인 그룹 목록 받아오기
# active_sessions = self.supabase_manager.get_user_active_sessions(self.sp_user_id)
# # NULL 값과 빈 문자열을 제대로 필터링하여 실제 작업중인 그룹만 추출
# active_groups = set()
# for session in active_sessions:
# selected_group = session.get("selected_group")
# # NULL, None, 빈 문자열이 아닌 실제 값만 추가
# if selected_group and selected_group.strip():
# active_groups.add(selected_group.strip())
# self.logger.log(f"활성 세션에서 추출한 작업중인 그룹: {active_groups}", level=logging.DEBUG)
# # 이미 작업중인 그룹을 제외
# filtered_groups = [g for g in group_names_list if g not in active_groups]
# if not filtered_groups:
# QMessageBox.critical(self, "오류", f"그룹목록을 가져오는데 실패했습니다. \n 관리자에게 로그를 전송해주세요. \n 활성그룹 : {active_groups} \n 사용가능그룹 : {filtered_groups}")
# sys.exit(1)
# else:
# self.group_selector.clear()
# self.group_selector.addItems(filtered_groups)
# self.group_selector.setEnabled(True)
# self.group_change_button.setEnabled(True)
def clean_value(self, val):
"""
None, 빈 문자열, "null", "None", "NULL" 등 모두 빈 문자열로 처리.
그 외는 strip() 후 반환
"""
if val is None:
return ""
if isinstance(val, str):
val_stripped = val.strip().lower()
if val_stripped in ("", "none", "null"):
return ""
return val.strip()
return str(val).strip()
@Slot(list)
def update_group_list(self, group_names_list: list):
"""
활성 세션 목록을 참고해, 작업 중인 그룹이면 (작업중) 표시를 붙이고 선택도 비활성화.
"""
my_admin_id = self.admin_id_input.text().strip()
my_session_id = self.supabase_manager.current_session_id
# 활성 세션들
active_sessions = self.supabase_manager.get_user_active_sessions(self.sp_user_id)
self.group_selector.clear()
any_group_available = False
for group in group_names_list:
is_working = False
for session in active_sessions:
session_group = self.clean_value(session.get("selected_group"))
session_admin_id = self.clean_value(session.get("selected_group_userid"))
session_id = session.get("id")
# 내 세션이 아니면서, 그룹명+관리자ID 모두 동일하면 "작업중"
if (
session_group == group
and session_admin_id == my_admin_id
and session_id != my_session_id
):
is_working = True
break
if is_working:
display_name = f"{group} (작업중)"
else:
display_name = group
any_group_available = True
self.group_selector.addItem(display_name)
idx = self.group_selector.count() - 1
if is_working:
# 현재 flags 값 가져오기 (None일 경우 기본값)
flags = self.group_selector.itemData(idx, 9)
if flags is None:
flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
# Enabled 비트만 제거
flags &= ~Qt.ItemIsEnabled
self.group_selector.setItemData(idx, flags, 9)
self.group_selector.setEnabled(any_group_available)
self.group_change_button.setEnabled(any_group_available)
if not any_group_available:
QMessageBox.critical(
self,
"오류",
"선택 가능한 그룹이 없습니다. 모두 작업 중입니다.\n다른 PC에서 그룹 작업이 끝날 때까지 기다려주세요.",
)
sys.exit(1)
@Slot(str, int)
def update_selected_group_label(self, group_name: str, total_products: int):
"""
선택된 그룹 이름을 QLabel에 업데이트합니다.
그룹 선택에 실패했으면(예: group_name이 빈 문자열이라면) 오류 메시지를 띄우고 프로그램을 종료합니다.
"""
if not self.group_selector.currentText().strip() == group_name.strip():
self.selected_group.setText("그룹 선택 실패")
self.logger.log(f"그룹 선택 실패 - 선택한 그룹 : {self.group_selector.currentText()}, 선택된 그룹 : {group_name}", level=logging.WARNING)
QMessageBox.warning(self, "오류", "그룹 선택에 실패했습니다. 그룹선택 버튼을 다시 눌러주세요. \n 오류가 지속되면 프로그램을 재실행해주세요.")
self.group_change_button.setEnabled(True)
else:
self.selected_group.setText(f"{group_name}")
self.logger.log(f"선택된 그룹 이름 업데이트: {group_name}", level=logging.INFO)
self.PercentyJob_button.setEnabled(True)
self.group_change_button.setEnabled(True)
self.selected_group_total_products.setText(f"{total_products}개 상품")
@Slot()
def on_start_PercentyJob_clicked(self):
QMessageBox.information(self, "정보", "알바생 실행중 자동으로 생성되는 웨일 / 크롬등의 브라우저 창은 종료하지 말아주세요.")
if self.selected_group.text() == "없음":
QMessageBox.warning(self, "오류", "그룹을 선택해주세요.")
return
result = self.supabase_manager.check_group_conflict(
target_group=self.selected_group.text().strip(),
admin_id=self.admin_id_input.text().strip()
)
has_conflict = result["has_conflict"]
conflicting_sessions = result["conflicting_sessions"]
message = result["message"]
self.logger.log(f"has_conflict: {has_conflict}", level=logging.INFO)
self.logger.log(f"conflicting_sessions: {conflicting_sessions}", level=logging.INFO)
self.logger.log(f"message: {message}", level=logging.INFO)
if has_conflict:
QMessageBox.warning(self, "오류", "선택한 그룹이 작업중입니다. 다른 그룹을 선택하세요.")
return
self.supabase_manager.update_session_selected_group(selected_group=self.selected_group.text())
self.logger.log(f"그룹세션 업데이트", level=logging.INFO)
self.supabase_manager.update_session_selected_group_userid(admin_id=self.admin_id_input.text().strip())
self.logger.log(f"작업그룹 유저세션 업데이트", level=logging.INFO)
self.supabase_manager.update_session_active_toggles(active_toggles=self.toggle_states)
self.logger.log(f"토글세션 업데이트", level=logging.INFO)
self.pause_button.setEnabled(True)
# 프로그래스바 초기화
self.update_total_progress(0,0)
# 예시: 토글 상태에 따라 활성화된 스테이지 목록 생성 (실제 구현은 토글 상태에 맞춰서 수정)
active_stages = []
if self.title_toggle.isChecked() or self.title_shuffle_toggle.isChecked():
active_stages.append("상품명")
if self.optionTrnas_toggle.isChecked() or self.optionAutoSelect_toggle.isChecked() or self.optionIMGTrans_toggle.isChecked():
active_stages.append("옵션")
if self.price_toggle.isChecked():
active_stages.append("가격")
if self.thumb_toggle.isChecked():
active_stages.append("썸네일")
if self.tag_toggle.isChecked():
active_stages.append("태그")
if self.detail_Option_toggle.isChecked() or self.detail_IMGTrans_toggle.isChecked():
active_stages.append("상페")
# 스테이지 타임라인 업데이트
self.update_stage_timeline(active_stages)
"""상품수정 스레드 시작 및 상태 전달"""
if self.browser_controller.isRunning():
# 스레드 시작
self.browser_controller.start_PercentyJob_task()
self.logger.log("상품수정 작업 스레드가 시작되었습니다.", level=logging.INFO)
else:
self.logger.log("브라우저 스레드가 없습니다.", level=logging.INFO)
def on_group_change_button_clicked(self):
selected_text = self.group_selector.currentText()
# "작업중" 문자열이 포함된 그룹을 선택했다면
if "(작업중)" in selected_text:
QMessageBox.warning(self, "오류", "작업중인 그룹은 선택할 수 없습니다. 다른 그룹을 선택하세요.")
# 자동으로 첫 번째 사용 가능한 항목으로 변경
for i in range(self.group_selector.count()):
if "(작업중)" not in self.group_selector.itemText(i):
self.group_selector.setCurrentIndex(i)
break
return
if self.browser_controller.isRunning():
self.logger.log(f"{self.group_selector.currentText()} 그룹 선택", level=logging.DEBUG)
self.browser_controller.select_group_task(group_index=self.group_selector.currentIndex())
self.logger.log("그룹선택 작업이 QThread에서 시작되었습니다.", level=logging.INFO)
self.group_change_button.setEnabled(False)
else:
self.logger.log("브라우저 스레드가 없습니다.", level=logging.INFO)
@Slot()
def on_PercentyJob_started(self):
"""상품수정 작업이 시작되었을 때 처리할 로직"""
self.job_start_time = datetime.now()
self.logger.log("상품수정 작업이 시작되었습니다.", level=logging.INFO)
self.PercentyJob_button.setEnabled(False)
self.update_webhook_url()
self.logger.log(f"self.discord_notify_toggle 토글 상태: {self.discord_notify_toggle.isChecked()}", level=logging.INFO)
# 디스코드 알림 전송 (토글이 활성화된 경우에만)
if self.discord_notify_toggle.isChecked():
# 수정 대상 확인
modification_targets = []
if self.toggle_states.get('title', False):
modification_targets.append("상품명")
if self.toggle_states.get('optionTrnas', False):
modification_targets.append("옵션명AI번역")
if self.toggle_states.get('optionIMGTrans', False):
modification_targets.append("옵션-이미지번역")
if self.toggle_states.get('optionAutoSelect', False):
modification_targets.append("옵션-자동선택")
if self.toggle_states.get('price', False):
modification_targets.append("가격")
if self.toggle_states.get('thumb', False):
modification_targets.append("썸네일")
if self.toggle_states.get('tag', False):
modification_targets.append("태그")
if self.toggle_states.get('detail_Option', False):
modification_targets.append("상페 설명 및 옵션명 추가")
if self.toggle_states.get('thumb', False):
modification_targets.append("상페 이미지 워터마크")
if self.toggle_states.get('group_index', False):
modification_targets.append(self.selected_group.text())
# self.selected_group.text()
# 수정 대상이 없으면 기본값 설정
if not modification_targets:
modification_targets = ["상품 정보"]
# 디스코드 알림 전송
self.discord_manager.send_job_start_notification(
self.discord_notify_toggle.isChecked(),
self.job_start_time,
self.group_selector.currentText(),
self.selected_group.text(),
modification_targets
)
@Slot()
def on_PercentyJob_completed(self, total_products):
"""상품수정 완료 시 처리할 로직"""
self.total_progress_bar.setValue(100)
# 디스코드 알림 전송
if self.discord_notify_toggle.isChecked():
self.discord_manager.send_job_complete_notification(
self.discord_notify_toggle.isChecked(),
self.job_start_time,
self.group_selector.currentText(),
self.selected_group.text(),
total_products
)
self.PercentyJob_button.setEnabled(True)
self.logger.log("상품수정 작업이 완료되었습니다.", level=logging.INFO)
QMessageBox.information(self, "작업 완료", f"{total_products}개의 상품 수정이 완료되었습니다.")
@Slot(str)
def on_PercentyJob_error(self, error_message):
"""상품수정 중 오류 발생 시 처리할 로직"""
self.logger.log(f"상품수정 작업 중 오류 발생: {error_message}", level=logging.ERROR, exc_info=True)
self.PercentyJob_button.setEnabled(True)
@Slot(bool)
def set_progress_visibility(self, visible):
self.detail_progress_bar.setVisible(visible)
self.detail_progress_bar.setValue(0)
@Slot(bool)
def update_detail_progress_value(self, current, total):
self.update_detail_progress(current, total)
@Slot(bool)
def percentyJob_button_Enable(self, Enable):
self.PercentyJob_button.setEnabled(Enable)
def initialize_user_session(self):
"""
사용자 세션을 초기화하고, 로그인 후 추가 검증(예: 멤버십 등급, 사용 기간 확인, 할인 이벤트 안내 등)을 진행합니다.
MAIN_GUI의 __init__의 마지막 부분에서 호출
"""
# 멤버십 등급 유효성 확인 (free 등급 제한)
membership_valid = self.supabase_manager.check_membership_validity(self.user_info)
if not membership_valid:
QMessageBox.warning(self, "로그인 제한", "현재 등급으로는 프로그램을 사용할 수 없습니다.\n멤버십을 업그레이드해주세요.")
# 프로그램 종료
sys.exit(1)
# full_user_info를 기반으로 사용 기간과 할인 이벤트 메시지를 확인
valid = self.supabase_manager.check_membership_period_validity(self.user_info)
if not valid:
QMessageBox.warning(self, "기간 만료", "사용 기간이 만료되었습니다. 재결제가 필요합니다.")
# 프로그램의 주요 기능 사용을 제한하는 로직을 추가할 수 있음.
sys.exit(1)
else:
discount_msg = self.supabase_manager.get_membership_message(self.user_info)
if discount_msg:
QMessageBox.information(self, "재결제 할인 안내", discount_msg)
def is_premium_or_higher(self):
"""사용자 등급이 Premium 이상인지 확인"""
membership_level = self.user_info.get('membership_level', 'free')
self.logger.log(f"사용자 등급: {membership_level}", level=logging.INFO)
return membership_level in ['premium', 'vip', 'admin']
def set_default_unwanted_words(self):
"""불필요한 단어 리스트 기본값 설정"""
# 기본값 설정
default_korean = ["할인", "무료", "증정", "이벤트", "특가", "세일", "사은품", "보증", "품절", "행사", "할인가", "무료배송", "가격설명"]
default_chinese = ["折扣", "免费", "赠品", "活动", "特价", "促销", "赠品", "保证", "售罄", "活动", "折扣价", "免费配送", "价格说明"]
default_combined = [f"{k}({c})" for k, c in zip(default_korean, default_chinese)]
# 기본값 저장
default_words = {
'korean': default_korean,
'chinese': default_chinese,
'combined': default_combined
}
# toggle_states에 설정
self.toggle_states['unwanted_words'] = default_words
self.logger.log("불필요한 단어 리스트 기본값이 설정되었습니다.", level=logging.INFO)
return default_words
def on_unwanted_words_button_clicked(self):
"""불필요한 단어 설정 다이얼로그 표시"""
try:
# 현재 unwanted_words 가져오기
current_words = self.toggle_states.get('unwanted_words', {})
# 데이터가 비어있을 경우 기본값 설정
if not current_words or (isinstance(current_words, dict) and not current_words.get('combined')):
current_words = self.set_default_unwanted_words()
elif isinstance(current_words, dict):
# combined 형식 사용
current_words = current_words.get('combined', [])
# 다이얼로그 생성 및 표시
dialog = UnwantedWordsDialog(self, current_words)
result = dialog.exec_()
# 다이얼로그가 Accepted로 종료된 경우 (확인 버튼 또는 저장 후 종료)
if result == QDialog.Accepted:
# 새로운 단어 리스트 가져오기 - 이미 딕셔너리 형태로 반환됨
unwanted_words = dialog.get_words()
# 단어 리스트 저장
self.toggle_states['unwanted_words'] = unwanted_words
# Supabase에 동기화
self.sync_unwanted_words_to_supabase()
self.logger.log(f"불필요한 단어 리스트가 업데이트되었습니다: {unwanted_words['combined']}", level=logging.INFO)
else:
self.logger.log("불필요한 단어 설정이 취소되었습니다.", level=logging.INFO)
except Exception as e:
self.logger.log(f"불필요한 단어 설정 오류: {str(e)}", level=logging.ERROR, exc_info=True)
def sync_unwanted_words_to_supabase(self):
"""unwanted_words 리스트를 Supabase에 동기화 (TEXT 필드)"""
try:
# 현재 unwanted_words 딕셔너리 가져오기
unwanted_words = self.toggle_states.get('unwanted_words', {})
# 데이터가 딕셔너리 형태가 아니라면 초기화
if not isinstance(unwanted_words, dict):
unwanted_words = {'korean': [], 'chinese': [], 'combined': []}
# unwanted_words가 없거나 빈 딕셔너리인 경우 기본값 설정
if not unwanted_words or not unwanted_words.get('combined'):
self.set_default_unwanted_words()
unwanted_words = self.toggle_states.get('unwanted_words', {})
# 딕셔너리를 JSON 문자열로 변환 (ensure_ascii=False로 한글 직접 저장)
json_data = json.dumps(unwanted_words, ensure_ascii=False)
# Supabase에 TEXT 필드로 JSON 문자열 저장
self.supabase_manager.update_user_field(
self.sp_user_id,
'unwanted_words',
json_data
)
self.logger.log(f"불필요한 단어 리스트가 Supabase에 동기화되었습니다: {unwanted_words['combined']}", level=logging.INFO)
except Exception as e:
self.logger.log(f"Supabase 동기화 중 오류 발생: {str(e)}", level=logging.ERROR, exc_info=True)
QMessageBox.warning(self, "동기화 오류", "불필요한 단어 리스트 동기화에 실패했습니다.")
def load_unwanted_words(self):
"""Supabase에서 unwanted_words 리스트를 로드"""
try:
# Supabase에서 unwanted_words 필드 가져오기 (TEXT 형식)
json_data = self.supabase_manager.get_user_field(
self.sp_user_id,
'unwanted_words'
)
self.logger.log(f"unwanted_words: {json_data}", level=logging.INFO)
# 데이터가 있는 경우
if json_data:
try:
# JSON 문자열 파싱
unwanted_words = json.loads(json_data)
# 모든 필요한 키가 있는지 확인
if not all(key in unwanted_words for key in ['korean', 'chinese', 'combined']):
# 필요한 키가 없으면 기본 구조로 초기화
unwanted_words = {
'korean': unwanted_words.get('korean', []),
'chinese': unwanted_words.get('chinese', []),
'combined': unwanted_words.get('combined', [])
}
self.toggle_states['unwanted_words'] = unwanted_words
self.logger.log(f"불필요한 단어 리스트가 로드되었습니다: {unwanted_words['combined']}", level=logging.INFO)
except json.JSONDecodeError as e:
self.logger.log(f"unwanted_words JSON 파싱 오류: {str(e)}", level=logging.ERROR, exc_info=True)
# 파싱 오류 시 기본값으로 설정
self.set_default_unwanted_words()
# 데이터가 없으면 기본값 설정
else:
self.set_default_unwanted_words()
self.sync_unwanted_words_to_supabase() # 기본값으로 초기화 및 저장
except Exception as e:
self.logger.log(f"불필요한 단어 리스트 로드 중 오류 발생: {str(e)}", level=logging.ERROR, exc_info=True)
def decode_unicode_strings(self, string_list):
"""유니코드 이스케이프 시퀀스를 읽기 쉬운 문자로 변환"""
if not string_list:
return []
decoded_list = []
for s in string_list:
# 문자열인 경우에만 처리
if isinstance(s, str):
# Python의 string literals에서는 이미 유니코드가 디코딩 되지만,
# JSON에서 가져온 경우 추가 처리가 필요할 수 있음
try:
# 이미 디코딩된 문자열이지만, 명시적으로 유니코드 이스케이프를 처리
# \uXXXX 형태의 문자열을 실제 유니코드 문자로 변환
s = s.encode('utf-8').decode('unicode_escape')
except (UnicodeError, AttributeError):
# 디코딩 오류 시 원본 사용
pass
decoded_list.append(s)
return decoded_list
def create_menu_bar(self):
"""메뉴바를 생성합니다."""
menubar = self.menuBar()
# 파일 메뉴
file_menu = menubar.addMenu("파일")
# 설정 저장
save_action = file_menu.addAction("설정 저장")
save_action.triggered.connect(self.save_settings)
# 설정 불러오기
load_action = file_menu.addAction("설정 불러오기")
load_action.triggered.connect(self.load_settings)
# 종료
exit_action = file_menu.addAction("종료")
exit_action.triggered.connect(self.close)
# 도움말 메뉴
help_menu = menubar.addMenu("도움말")
# 금지어
forbidden_word_action = help_menu.addAction("금지어 설정")
forbidden_word_action.triggered.connect(self.on_forbbidenWord_button_clicked)
# 로그 뷰어
log_viewer_action = help_menu.addAction("로그 뷰어")
log_viewer_action.triggered.connect(self.show_log_dialog)